├── sleth ├── __init__.py ├── constants.py ├── reels.py └── cli.py ├── frontend ├── .gitignore ├── sounds │ ├── win.wav │ └── reel_stop.wav ├── images │ ├── background.png │ ├── five_lines.png │ ├── one_line.png │ ├── reels_bg.png │ ├── three_lines.png │ └── reddit_icons_small.png ├── .jshintrc ├── templates │ ├── reveal-value.html │ ├── progress-bar.html │ ├── stats-panel.html │ ├── prev-rounds-panel.html │ ├── player-bar.html │ ├── home.html │ ├── round-panel.html │ └── sleth.html ├── README.md ├── js │ ├── app.js │ ├── home.js │ ├── config.js │ ├── sounds.js │ ├── directives.js │ ├── slots.js │ ├── reels.js │ ├── game.js │ ├── sleth.js │ └── lib │ │ └── web3.min.js ├── bower.json ├── sleth.abi.json ├── css │ └── slots.css ├── index.html ├── ng-blockwatch.html ├── slots.html └── paytable.html ├── dev_requirements.txt ├── contracts ├── simple.yaml ├── sleth.yaml ├── simple.se ├── sleth.se └── commit_reveal_entropy.se ├── cli.py ├── requirements.txt ├── tox.ini ├── setup.py ├── .travis.yml ├── gulpfile.js ├── test ├── conftest.py ├── eth_tools.py ├── test_simple.py ├── test_stops.py ├── test_ecrecover.py ├── test_reels.py ├── test_log.py ├── test_commit_reveal_entropy_contract.py └── test_sleth_contract.py ├── .gitignore └── README.md /sleth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | pytest 3 | pytest-xdist 4 | -------------------------------------------------------------------------------- /frontend/sounds/win.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorisbontje/sleth/HEAD/frontend/sounds/win.wav -------------------------------------------------------------------------------- /contracts/simple.yaml: -------------------------------------------------------------------------------- 1 | - 2 | deploy: 3 | simple: 4 | contract: simple.se 5 | gas: 100000 6 | -------------------------------------------------------------------------------- /frontend/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorisbontje/sleth/HEAD/frontend/images/background.png -------------------------------------------------------------------------------- /frontend/images/five_lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorisbontje/sleth/HEAD/frontend/images/five_lines.png -------------------------------------------------------------------------------- /frontend/images/one_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorisbontje/sleth/HEAD/frontend/images/one_line.png -------------------------------------------------------------------------------- /frontend/images/reels_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorisbontje/sleth/HEAD/frontend/images/reels_bg.png -------------------------------------------------------------------------------- /frontend/sounds/reel_stop.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorisbontje/sleth/HEAD/frontend/sounds/reel_stop.wav -------------------------------------------------------------------------------- /frontend/images/three_lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorisbontje/sleth/HEAD/frontend/images/three_lines.png -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sleth.cli 4 | 5 | if __name__ == '__main__': 6 | sleth.cli.main() 7 | -------------------------------------------------------------------------------- /frontend/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "browserify": true, 4 | "devel": true, 5 | "predef": ["angular"] 6 | } 7 | -------------------------------------------------------------------------------- /frontend/images/reddit_icons_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorisbontje/sleth/HEAD/frontend/images/reddit_icons_small.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | https://github.com/ethereum/serpent/tarball/develop 2 | https://github.com/ethereum/pyethereum/tarball/develop 3 | pyepm 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E302 3 | max-line-length = 160 4 | exclude = frontend/* 5 | 6 | [pytest] 7 | norecursedirs = .git frontend 8 | -------------------------------------------------------------------------------- /sleth/constants.py: -------------------------------------------------------------------------------- 1 | CONTRACT_FILE = 'contracts/sleth.se' 2 | 3 | CONTRACT_GAS = 1028711 4 | SPIN_GAS = 163000 5 | CLAIM_GAS = 120000 6 | 7 | ETHER = 10 ** 18 8 | -------------------------------------------------------------------------------- /frontend/templates/reveal-value.html: -------------------------------------------------------------------------------- 1 | 2 | {{value}} 3 | ? 4 | 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='sleth', 6 | version='1.0', 7 | description='Ethereum Slot Machine', 8 | packages=['sleth']) 9 | -------------------------------------------------------------------------------- /contracts/sleth.yaml: -------------------------------------------------------------------------------- 1 | - 2 | deploy: 3 | sleth: 4 | contract: sleth.se 5 | gas: 60000 6 | # 7 | #- 8 | # deploy: 9 | # commit_reveal_entropy: 10 | # contract: commit_reveal_entropy.se 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | install: 5 | - pip install -r requirements.txt 6 | - pip install -r dev_requirements.txt 7 | - pip install -e . 8 | script: 9 | - py.test -vvrs 10 | - flake8 11 | -------------------------------------------------------------------------------- /contracts/simple.se: -------------------------------------------------------------------------------- 1 | data owner 2 | data counter 3 | 4 | def init(): 5 | self.owner = msg.sender 6 | self.counter = 1 7 | 8 | def incr(): 9 | self.counter += 1 10 | 11 | def get_counter(): 12 | return(self.counter) 13 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | License Info 2 | 3 | The original Karma-Slots code is (C) 2012 Clint Bellanger. Released under the MIT license. 4 | The original Karma-Slots art is (C) 2012 Clint Bellanger. Released under CC-BY 3.0. 5 | The original Karma-Slots sounds are (C) 2012 Brandon Morris. Released under CC-BY 3.0 6 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var deploy = require('gulp-gh-pages'); 3 | 4 | /** 5 | * Push build to gh-pages 6 | */ 7 | gulp.task('deploy', function () { 8 | return gulp.src(['./frontend/**/*', '!./frontend/node_modules/**']) 9 | .pipe(deploy({cacheDir: '../sleth-gh-pages/'})) 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/templates/progress-bar.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | spinning... 4 |
5 |
6 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | # content of conftest.py 2 | 3 | import pytest 4 | def pytest_addoption(parser): 5 | parser.addoption("--runslow", action="store_true", help="run slow tests") 6 | 7 | def pytest_runtest_setup(item): 8 | if 'slow' in item.keywords and not item.config.getoption("--runslow"): 9 | pytest.skip("need --runslow option to run") 10 | -------------------------------------------------------------------------------- /frontend/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = angular.module('slethApp', ['homeController', 'slethController', 'ngRoute']); 4 | app.config(['$routeProvider', function($routeProvider) { 5 | $routeProvider. 6 | when('/', { 7 | templateUrl: 'templates/home.html', 8 | controller: 'HomeController', 9 | }). 10 | when('/sleth/:contractAddress', { 11 | templateUrl: 'templates/sleth.html', 12 | controller: 'SlethController', 13 | }). 14 | otherwise({redirectTo: '/'}); 15 | }]); 16 | -------------------------------------------------------------------------------- /test/eth_tools.py: -------------------------------------------------------------------------------- 1 | from ethereum import tester as t 2 | 3 | def address(returned_value): 4 | return hex(returned_value).lstrip("0x").rstrip("L") 5 | 6 | class Contract(object): 7 | def __init__(self, filepath, state): 8 | self.state = state 9 | self.contract = self.state.contract(filepath) 10 | 11 | @classmethod 12 | def from_address(cls, address, state): 13 | result = cls("", state) 14 | result.contract = address 15 | return result 16 | 17 | def call(self, function, arguments=[], ether=0, sender=t.k0): 18 | return self.state.send(sender, self.contract, ether, funid=function, abi=arguments) 19 | -------------------------------------------------------------------------------- /frontend/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sleth", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/jorisbontje/sleth", 5 | "authors": [ 6 | "Joris Bontje " 7 | ], 8 | "description": "Ethereum Slot Machine DApp", 9 | "license": "MIT", 10 | "private": true, 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "angular": "~1.3.13", 20 | "angular-animate": "~1.3.13", 21 | "angular-route": "~1.3.13", 22 | "angular-lodash": "~0.1.2", 23 | "angular-moment": "~0.9.0", 24 | "bignumber.js": "~2.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/js/home.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var app = angular.module('homeController', []); 4 | 5 | app.factory('localStorage', function() { 6 | return window.localStorage || {}; 7 | }); 8 | 9 | app.controller("HomeController", ['$location', '$scope', 'localStorage', function($location, $scope, localStorage) { 10 | 11 | $scope.address = localStorage['sleth:address']; 12 | 13 | $scope.play = function() { 14 | if ($scope.address.substring(0, 2) !== "0x") { 15 | $scope.address = "0x" + $scope.address; 16 | } 17 | 18 | localStorage['sleth:address'] = $scope.address; 19 | $location.path('/sleth/' + $scope.address); 20 | }; 21 | 22 | }]); 23 | -------------------------------------------------------------------------------- /frontend/templates/stats-panel.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Stats

4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Contract{{contract | limitTo : 10}}
Contract Balance{{stats.slethBalance | number : 4}} ETH
Total Spins{{stats.total_spins}}
Total Coins Bet{{stats.total_coins_bet}}
Total Coins Won{{stats.total_coins_won}}
Payout Percentage{{stats.total_coins_won / stats.total_coins_bet * 100| number :2}} %
13 |
14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | *.swp 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | bin/ 14 | develop-eggs/ 15 | eggs/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | 24 | # Installer logs 25 | pip-log.txt 26 | pip-delete-this-directory.txt 27 | 28 | # Unit test / coverage reports 29 | htmlcov/ 30 | .tox/ 31 | .coverage 32 | .cache 33 | nosetests.xml 34 | coverage.xml 35 | 36 | # Translations 37 | *.mo 38 | 39 | # Mr Developer 40 | .mr.developer.cfg 41 | .project 42 | .pydevproject 43 | 44 | # Rope 45 | .ropeproject 46 | 47 | # Django stuff: 48 | *.log 49 | *.pot 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | -------------------------------------------------------------------------------- /frontend/templates/prev-rounds-panel.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Previous Rounds

4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
RoundPlayerBlockBetPrize
{{round.number}}{{round.player | limitTo : 10}}{{round.block}}{{round.bet}}{{round.result}}
21 |
22 | -------------------------------------------------------------------------------- /frontend/templates/player-bar.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Player {{player.address | limitTo : 10}} 4 |
5 |
6 | Balance {{player.balance | number : 4}} ETH 7 |
8 |
9 | Block {{web3.blockNumber}} 10 |
11 |
12 | Network 13 | 14 | online 15 | offline 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /frontend/sleth.abi.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name": "spin(int256)", 3 | "type": "function", 4 | "inputs": [{ 5 | "name": "bet", 6 | "type": "int256" 7 | }], 8 | "outputs": [] 9 | }, { 10 | "name": "claim(int256)", 11 | "type": "function", 12 | "inputs": [{ 13 | "name": "round", 14 | "type": "int256" 15 | }], 16 | "outputs": [] 17 | }, { 18 | "name": "get_round", 19 | "type": "function", 20 | "inputs": [{ 21 | "name": "round", 22 | "type": "int256" 23 | }], 24 | "outputs": [{ 25 | "name": "out", 26 | "type": "int256[]" 27 | }] 28 | }, { 29 | "name": "get_current_round()", 30 | "type": "function", 31 | "inputs": [], 32 | "outputs": [{ 33 | "name": "last_round", 34 | "type": "int256" 35 | }] 36 | }, { 37 | "name": "get_stats()", 38 | "type": "function", 39 | "inputs": [], 40 | "outputs": [{ 41 | "name": "out", 42 | "type": "int256[]" 43 | }] 44 | }] 45 | -------------------------------------------------------------------------------- /frontend/css/slots.css: -------------------------------------------------------------------------------- 1 | #game { 2 | margin: auto; 3 | 4 | background-image: url("../images/background.png"); 5 | background-position: 0px 0px; 6 | background-repeat: no-repeat; 7 | background-size: contain; 8 | 9 | image-rendering: -moz-crisp-edges; /* Firefox */ 10 | image-rendering: -o-crisp-edges; /* Opera */ 11 | image-rendering: -webkit-optimize-contrast;/* Webkit (non-standard naming) */ 12 | image-rendering: crisp-edges; 13 | -ms-interpolation-mode: nearest-neighbor; /* IE (non-standard property) */ 14 | } 15 | 16 | .top-buffer { 17 | margin-top:20px; 18 | } 19 | 20 | .player-bar { 21 | background-color: #ecf0f1; 22 | padding-top: 5px; 23 | padding-bottom: 5px; 24 | } 25 | 26 | /* 27 | * highlight value change using ngAnimate 28 | * 29 | */ 30 | .value-change { 31 | transition: background-color 0.26s linear 0s; 32 | } 33 | .value-change[class*="-add"] { 34 | background-color: yellow; 35 | } 36 | -------------------------------------------------------------------------------- /test/test_simple.py: -------------------------------------------------------------------------------- 1 | from ethereum import tester 2 | from eth_tools import address 3 | 4 | class TestSimpleContract(object): 5 | 6 | CONTRACT = 'contracts/simple.se' 7 | CONTRACT_GAS = 81815 8 | 9 | def setup_class(cls): 10 | cls.s = tester.state() 11 | cls.c = cls.s.abi_contract(cls.CONTRACT) 12 | cls.snapshot = cls.s.snapshot() 13 | 14 | def setup_method(self, method): 15 | self.s.revert(self.snapshot) 16 | 17 | def test_create_gas_used(self): 18 | print "create gas used:", self.s.block.gas_used 19 | assert self.s.block.gas_used <= self.CONTRACT_GAS 20 | 21 | def test_init(self): 22 | assert self.s.block.get_code(self.c.address) != '' 23 | assert address(self.s.block.get_storage_data(self.c.address, 0)) == tester.a0.encode('hex') 24 | assert self.s.block.get_storage_data(self.c.address, 1) == 1 25 | 26 | def test_incr(self): 27 | assert self.c.get_counter() == 1 28 | self.c.incr() 29 | assert self.c.get_counter() == 2 30 | -------------------------------------------------------------------------------- /test/test_stops.py: -------------------------------------------------------------------------------- 1 | from ethereum import tester 2 | 3 | class TestStops(object): 4 | 5 | CONTRACT = """ 6 | def shared(): 7 | REEL_COUNT = 3 8 | REEL_POSITIONS = 32 9 | 10 | def get_stops(rnd): 11 | stops = array(REEL_COUNT) 12 | i = 0 13 | while i < REEL_COUNT: 14 | stops[i] = rnd % REEL_POSITIONS 15 | rnd = rnd / REEL_POSITIONS 16 | i += 1 17 | return(stops, items=REEL_COUNT) 18 | 19 | def pass_(rnd): 20 | return(self.get_stops(rnd, outsz=REEL_COUNT), items=REEL_COUNT) 21 | """ 22 | 23 | def setup_class(cls): 24 | cls.s = tester.state() 25 | cls.c = cls.s.abi_contract(cls.CONTRACT) 26 | cls.snapshot = cls.s.snapshot() 27 | 28 | def setup_method(self, method): 29 | self.s.revert(self.snapshot) 30 | 31 | def test_get_stops(self): 32 | assert self.c.get_stops(23888) == [16, 10, 23] 33 | assert self.c.get_stops(1606) == [6, 18, 1] 34 | assert self.c.get_stops(30464) == [0, 24, 29] 35 | 36 | def test_pass(self): 37 | assert self.c.pass_(23888) == [16, 10, 23] 38 | -------------------------------------------------------------------------------- /frontend/templates/home.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome to Sleth!

3 | 4 |

Are you ready to play some slots? Please enter the Sleth contract address below so we can connect you to your slot machine!

5 | 6 |
7 |
8 | 9 | 10 |

please a contract address

11 |

please enter valid contract address

12 |
13 | 14 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /test/test_ecrecover.py: -------------------------------------------------------------------------------- 1 | import bitcoin as b 2 | from ethereum import tester, utils 3 | 4 | 5 | class TestECRecover(object): 6 | 7 | CONTRACT = """ 8 | 9 | def test_ecrecover(h, v, r, s): 10 | return(ecrecover(h, v, r, s)) 11 | """ 12 | 13 | def setup_class(cls): 14 | cls.s = tester.state() 15 | cls.c = cls.s.abi_contract(cls.CONTRACT) 16 | cls.snapshot = cls.s.snapshot() 17 | 18 | def setup_method(self, method): 19 | self.s.revert(self.snapshot) 20 | 21 | def test_ecrecover(self): 22 | priv = b.sha256('some big long brainwallet password') 23 | pub = b.privtopub(priv) 24 | 25 | msghash = b.sha256('the quick brown fox jumps over the lazy dog') 26 | V, R, S = b.ecdsa_raw_sign(msghash, priv) 27 | assert b.ecdsa_raw_verify(msghash, (V, R, S), pub) 28 | 29 | addr = utils.sha3(b.encode_pubkey(pub, 'bin')[1:])[12:] 30 | assert utils.privtoaddr(priv) == addr 31 | 32 | result = self.c.test_ecrecover(utils.big_endian_to_int(msghash.decode('hex')), V, R, S) 33 | assert result == utils.big_endian_to_int(addr) 34 | -------------------------------------------------------------------------------- /frontend/templates/round-panel.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Current Round #{{round.number}}

4 |
5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 |
Block{{round.block}}Status 10 | NEW 11 | SPINNING 12 | DONE 13 | EXPIRED 14 |
Bet{{round.bet}}Result
Entropy 23 | 24 | RND
28 |
29 | -------------------------------------------------------------------------------- /test/test_reels.py: -------------------------------------------------------------------------------- 1 | from sleth.reels import rnd_positions, outcome, position_symbols, sum_payline, find_paylines, REELS 2 | 3 | 4 | def test_rnd_positions(): 5 | assert rnd_positions(REELS, 3792463) == (8, 7, 20, 3, 25) 6 | assert rnd_positions(REELS, 1900918) == (4, 4, 4, 0, 6) 7 | 8 | def test_position_symbols(): 9 | assert position_symbols(REELS, (8, 7, 20, 3, 25)) == ('g!L', 'g!v', 'pHv', 'yvL', 'pL!') 10 | assert position_symbols(REELS, (4, 4, 4, 0, 6)) == ('pJL', 'gvS', 'LHp', 'vpl', 'pgv') 11 | 12 | def test_find_paylines(): 13 | assert find_paylines(('gyp', 'gHl', 'JgH', 'ygH', 'LSg')) == [(6, 'g', 3, 0.12), (19, 'g', 3, 0.12)] 14 | 15 | def test_round_321416(): 16 | rnd = 1149376 17 | exp_outcome = 0.24 18 | 19 | pos = rnd_positions(REELS, rnd) 20 | assert pos == (2, 13, 10, 6, 20) 21 | 22 | symbols = position_symbols(REELS, pos) 23 | assert symbols == ('gyp', 'gHl', 'JgH', 'ygH', 'LSg') 24 | 25 | paylines = find_paylines(symbols) 26 | assert paylines == [(6, 'g', 3, 0.12), (19, 'g', 3, 0.12)] 27 | assert sum_payline(paylines) == exp_outcome 28 | 29 | def test_round_329904(): 30 | assert outcome(10105281) == 1.2 31 | 32 | def test_round_329897(): 33 | assert outcome(3983410) == 0.52 34 | 35 | def test_round_329777(): 36 | assert outcome(10999484) == 0 37 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sleth - Ethereum Slots 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/js/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Copyright (c) 2015 Joris Bontje 4 | Copyright (c) 2012 Clint Bellanger 5 | 6 | MIT License: 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | 14 | 15 | Sounds by Brandon Morris (CC-BY 3.0) 16 | Art by Clint Bellanger (CC-BY 3.0) 17 | 18 | */ 19 | 20 | "use strict"; 21 | 22 | var app = angular.module('slots.config', []); 23 | 24 | app.constant('config', { 25 | FPS: 30, 26 | autoplay_delay: 3000, 27 | 28 | reel_count: 3, 29 | row_count: 3, 30 | reel_positions: 32, 31 | symbol_size: 32, 32 | 33 | reel_scale: 2, 34 | reel_pixel_length: 1024, // reel_positions * symbol_size 35 | stopping_distance: 528, 36 | max_reel_speed: 32, 37 | spinup_acceleration: 2, 38 | spindown_acceleration: 1, 39 | starting_credits: 100 40 | }); 41 | -------------------------------------------------------------------------------- /frontend/js/sounds.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Copyright (c) 2015 Joris Bontje 4 | Copyright (c) 2012 Clint Bellanger 5 | 6 | MIT License: 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | 14 | 15 | Sounds by Brandon Morris (CC-BY 3.0) 16 | Art by Clint Bellanger (CC-BY 3.0) 17 | 18 | */ 19 | 20 | "use strict"; 21 | 22 | var app = angular.module('slots.sounds', []); 23 | 24 | app.factory('sounds', function() { 25 | var snd_win = new Audio("sounds/win.wav"); 26 | var snd_reel_stop = []; 27 | snd_reel_stop[0] = new Audio("sounds/reel_stop.wav"); 28 | snd_reel_stop[1] = new Audio("sounds/reel_stop.wav"); 29 | snd_reel_stop[2] = new Audio("sounds/reel_stop.wav"); 30 | 31 | var sounds = {}; 32 | 33 | sounds.playWin = function() { 34 | try { 35 | snd_win.currentTime = 0; 36 | snd_win.load(); // workaround for chrome currentTime bug 37 | snd_win.play(); 38 | } catch(err) { 39 | console.error(err); 40 | } 41 | }; 42 | 43 | sounds.playReelStop = function(i) { 44 | try { 45 | snd_reel_stop[i].currentTime = 0; 46 | snd_reel_stop[i].load(); // workaround for chrome currentTime bug 47 | snd_reel_stop[i].play(); 48 | } catch(err) { 49 | console.error(err); 50 | } 51 | }; 52 | 53 | return sounds; 54 | }); 55 | -------------------------------------------------------------------------------- /frontend/ng-blockwatch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Your DApp 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Your DApp

13 | 14 |
15 |
16 | User: 17 | {{user.address}} 18 |
19 |
20 | Balance: 21 | {{user.balance}} 22 |
23 |
24 | 25 |
26 | Block #{{blockNumber}}: 27 |
{{block | json}}
28 |
29 |
30 | 31 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /frontend/slots.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Karma Slots! 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |

Karma ({{credits}})

21 |

22 | 23 | You won {{reward.payout}}!
24 |
25 | 26 | Line {{line}} pays {{partial_payout}}
27 |
28 |

29 |
30 |
31 |
32 | 33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 |
43 |

Pay Table

44 |

by Clint Bellanger and Brandon Morris

45 |
46 |
47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /frontend/js/directives.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Copyright (c) 2015 Joris Bontje 4 | Copyright (c) 2012 Clint Bellanger 5 | 6 | MIT License: 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | 14 | 15 | Sounds by Brandon Morris (CC-BY 3.0) 16 | Art by Clint Bellanger (CC-BY 3.0) 17 | 18 | */ 19 | 20 | "use strict"; 21 | 22 | var app = angular.module('slots.directives', []); 23 | 24 | app.directive('keypressEvents', function ($document, $rootScope) { 25 | return { 26 | restrict: 'A', 27 | link: function () { 28 | $document.bind('keypress', function (e) { 29 | $rootScope.$broadcast('keypress', e); 30 | }); 31 | } 32 | }; 33 | }); 34 | 35 | app.directive('playerBar', function() { 36 | return { 37 | restrict: 'E', 38 | scope: { 39 | player: '=player', 40 | web3: '=web3' 41 | }, 42 | templateUrl: 'templates/player-bar.html' 43 | }; 44 | }); 45 | 46 | app.directive('progressBar', function() { 47 | return { 48 | restrict: 'E', 49 | scope: { 50 | value: '=value', 51 | success: '=success' 52 | }, 53 | templateUrl: 'templates/progress-bar.html' 54 | }; 55 | }); 56 | 57 | app.directive('revealValue', function() { 58 | return { 59 | restrict: 'E', 60 | scope: { 61 | on: '=on', 62 | value: '=value' 63 | }, 64 | templateUrl: 'templates/reveal-value.html' 65 | }; 66 | }); 67 | 68 | app.directive('roundPanel', function() { 69 | return { 70 | restrict: 'E', 71 | scope: { 72 | round: '=round', 73 | state: '=state' 74 | }, 75 | templateUrl: 'templates/round-panel.html' 76 | }; 77 | }); 78 | 79 | app.directive('prevRoundsPanel', function() { 80 | return { 81 | restrict: 'E', 82 | scope: { 83 | rounds: '=rounds', 84 | currentRound: '=currentRound' 85 | }, 86 | templateUrl: 'templates/prev-rounds-panel.html' 87 | }; 88 | }); 89 | 90 | app.directive('statsPanel', function() { 91 | return { 92 | restrict: 'E', 93 | scope: { 94 | contract: '=contract', 95 | stats: '=stats' 96 | }, 97 | templateUrl: 'templates/stats-panel.html' 98 | }; 99 | }); 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/jorisbontje/sleth.svg?branch=master)](https://travis-ci.org/jorisbontje/sleth) 2 | [![Stories in Ready](https://badge.waffle.io/jorisbontje/sleth.png?label=ready&title=Ready)](https://waffle.io/jorisbontje/sleth) 3 | 4 | 5 | 6 | ## First game of sleth 7 | 8 | Lets create the `sleth` Ethereum Slot contract: 9 | ``` 10 | $ ./cli.py create 11 | Contract is available at 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 12 | Waiting for next block to be mined.. 13 | INFO:pyepm.api Ready! Mining took 10s 14 | ``` 15 | 16 | The contract is created under the account `0xc6c97de34c2b52b929baa21e662196b9e9e03fe0`. Now lets deposit `1000` Ether into the slot machine: 17 | ``` 18 | $ ./cli.py deposit 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 1000 19 | ``` 20 | 21 | Getting information about the current player: 22 | ``` 23 | $ ./cli.py get_current_player 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 24 | Current round: 0 25 | Balance: 1000 ether 26 | ``` 27 | 28 | Now that we have some coins to play with, lets give it a spin; betting `5` (letting us play on 5 lines): 29 | ``` 30 | $ ./cli.py spin 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 5 31 | ``` 32 | 33 | Our bet is deducted from the balance, and we are playing in game round `1`: 34 | ``` 35 | $ ./cli.py get_current_player 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 36 | Current round: 1 37 | Balance: 995 ether 38 | ``` 39 | 40 | Knowing that we are in round `1`, we can check out its status: 41 | ``` 42 | $ ./cli.py get_round 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 1 43 | Player: 0x9ecb3cc2e0d7eef7485a5234767b313c2f24b582L 44 | Block: 4823 45 | Timestamp: 1421155818 46 | Bet: 5 47 | Result: 0 48 | Entropy: 0 49 | Status: 1 50 | ``` 51 | 52 | The round `1` is still pending (status=1), lets claim it! (for testing purposes we can provide the random seed `12345`): 53 | ``` 54 | $ ./cli.py claim 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 1 12345 55 | ``` 56 | 57 | Now we'll inspect the results of round `1` again: 58 | ``` 59 | $ ./cli.py get_round 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 1 60 | Player: 0x9ecb3cc2e0d7eef7485a5234767b313c2f24b582L 61 | Block: 4823 62 | Timestamp: 1421155818 63 | Bet: 5 64 | Result: 0 65 | Entropy: 12345 66 | Status: 2 67 | ``` 68 | 69 | Too bad, we didn't win anything... 70 | 71 | Lets play again! 72 | ``` 73 | $ ./cli.py spin 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 5 74 | 75 | $ ./cli.py get_current_player 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 76 | Current round: 2 77 | Balance: 990 ether 78 | 79 | $ ./cli.py claim 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 2 23888 80 | 81 | $ ./cli.py get_round 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 2 82 | Player: 0x9ecb3cc2e0d7eef7485a5234767b313c2f24b582L 83 | Block: 4826 84 | Timestamp: 1421155937 85 | Bet: 5 86 | Result: 32 87 | Entropy: 23888 88 | Status: 2 89 | 90 | $ ./cli.py get_current_player 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 91 | Current round: 2 92 | Balance: 1022 ether 93 | ``` 94 | 95 | Yeehaa! We won 32 ether :) Time to withdraw: 96 | 97 | ``` 98 | $ ./cli.py withdraw 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 1022 99 | 100 | $ ./cli.py get_current_player 0xc6c97de34c2b52b929baa21e662196b9e9e03fe0 101 | Current round: 2 102 | Balance: 0 ether 103 | ``` 104 | -------------------------------------------------------------------------------- /frontend/js/slots.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Copyright (c) 2015 Joris Bontje 4 | Copyright (c) 2012 Clint Bellanger 5 | 6 | MIT License: 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | 14 | 15 | Sounds by Brandon Morris (CC-BY 3.0) 16 | Art by Clint Bellanger (CC-BY 3.0) 17 | 18 | */ 19 | 20 | "use strict"; 21 | 22 | var app = angular.module('slots', ['slots.config', 'slots.game', 'slots.reels']); 23 | 24 | app.controller("SlotsController", ['$scope', '$interval', 'config', 'game', function($scope, $interval, config, game) { 25 | 26 | $scope.canvasSize = 160 * config.reel_scale; 27 | $scope.infoTop = 16 * config.reel_scale; 28 | 29 | // sync with blockchain 30 | $scope.credits = config.starting_credits; 31 | 32 | $interval(function() { 33 | game.logic(); 34 | }, 1000 / config.FPS); 35 | 36 | $scope.generateEntropy = function() { 37 | $scope.entropy = Math.floor(Math.random() * Math.pow(32, 3)); 38 | }; 39 | 40 | //---- Input Functions --------------------------------------------- 41 | 42 | $scope.handleKey = function(evt) { 43 | if (evt.keyCode === 32) { // spacebar 44 | if (game.state === game.STATE_SPINMAX) { 45 | $scope.stop(); 46 | return; 47 | } 48 | if (game.state !== game.STATE_NEW && game.state !== game.STATE_REST) return; 49 | 50 | if ($scope.credits >= 5) $scope.spin(5); 51 | else if ($scope.credits >= 3) $scope.spin(3); 52 | else if ($scope.credits >= 1) $scope.spin(1); 53 | 54 | } 55 | }; 56 | 57 | $scope.spin = function(line_choice) { 58 | if (game.state !== game.STATE_NEW && game.state !== game.STATE_REST) return; 59 | if ($scope.credits < line_choice) return; 60 | 61 | $scope.credits -= line_choice; 62 | game.spin(line_choice); 63 | $scope.spinning = true; 64 | $scope.generateEntropy(); 65 | }; 66 | 67 | $scope.stop = function() { 68 | if (game.state !== game.STATE_SPINMAX) return; 69 | 70 | // calculate the final results now, so that spindown is ready 71 | game.set_stops($scope.entropy); 72 | }; 73 | 74 | $scope.calc_payout_percentage = function() { 75 | var totalPayout = 0; 76 | var bet = 5; 77 | var total_positions = 32768; 78 | for (var entropy = 0; entropy < total_positions; entropy++) { 79 | game.set_stops(entropy); 80 | var result = game.get_result(); 81 | var reward = game.calc_reward(bet, result); 82 | totalPayout += reward.payout; 83 | } 84 | console.log("payout percentage = ", totalPayout / (bet * total_positions) * 100); 85 | }; 86 | 87 | $scope.$on('slots:reward', function(evt, reward) { 88 | /*if (reward.payout > 0) { 89 | sounds.playWin(); 90 | }*/ 91 | $scope.credits += reward.payout; 92 | $scope.reward = reward; 93 | $scope.spinning = false; 94 | }); 95 | 96 | }]); 97 | -------------------------------------------------------------------------------- /sleth/reels.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from functools import reduce 3 | import operator 4 | import random 5 | 6 | # 'g' won 7 | # 'y' yen 8 | # 'v' dollar 9 | # 'p' euro 10 | # 'l' pound 11 | # 'H' miner 12 | # 'L' asic 13 | # 'S' ghost 14 | # 'J' liberty 15 | # '*' wild 16 | # '!' bitcoin 17 | 18 | REELS = [ 19 | 'lSgypJLyg!LvHly*vpHyvgplJg', 20 | 'vplygvSg!vy*pgHlpHgLylLSyJ', 21 | 'vgySLHpJ*SJgHLpylgv!pHvygl', 22 | 'vplyvLygHlpyJvSglHgp!Sv*JL', 23 | 'L!yvLlpgvpHSg*vlgyHJLSglyp' 24 | ] 25 | 26 | MAX_RANDOM = reduce(operator.mul, (len(reel) for reel in REELS), 1) 27 | 28 | PAYOUT = { 29 | 'g': [0.12, 0.4, 2], # won 30 | 'y': [0.16, 0.6, 2.4], # yen 31 | 'v': [0.2, 0.8, 3], # dollar 32 | 'p': [0.2, 1, 3.2], # euro 33 | 'l': [0.2, 1.2, 4], # pound 34 | 'H': [0.4, 3, 16], # miner 35 | 'L': [1, 4, 18], # asic 36 | 'S': [1.2, 8, 26], # ghost 37 | 'J': [2, 10, 40] # liberty 38 | } 39 | 40 | PAYLINES = [ 41 | [1, 1, 1, 1, 1], 42 | [0, 0, 0, 0, 0], 43 | [2, 2, 2, 2, 2], 44 | [0, 1, 2, 1, 0], 45 | [2, 1, 0, 1, 2], 46 | 47 | [0, 0, 1, 0, 0], 48 | [2, 2, 1, 2, 2], 49 | [1, 2, 2, 2, 1], 50 | [1, 0, 0, 0, 1], 51 | [1, 0, 1, 0, 1], 52 | 53 | [1, 2, 1, 2, 1], 54 | [0, 1, 0, 1, 0], 55 | [2, 1, 2, 1, 2], 56 | [1, 1, 0, 1, 1], 57 | [1, 1, 2, 1, 1], 58 | 59 | [0, 1, 1, 1, 0], 60 | [2, 1, 1, 1, 2], 61 | [2, 2, 1, 0, 0], 62 | [0, 0, 1, 2, 2], 63 | [0, 2, 0, 2, 0], 64 | 65 | [2, 0, 2, 0, 2], 66 | [0, 2, 2, 2, 0], 67 | [2, 0, 0, 0, 2], 68 | [0, 0, 2, 0, 0], 69 | [2, 2, 0, 2, 2] 70 | ] 71 | 72 | def wheel_frequency(wheel): 73 | freq = Counter() 74 | for char in wheel: 75 | freq[char] += 1 76 | return freq 77 | 78 | def rnd_positions(reels, rnd): 79 | reel1_div = len(reels[1]) * len(reels[2]) * len(reels[3]) * len(reels[4]) 80 | reel2_div = len(reels[2]) * len(reels[3]) * len(reels[4]) 81 | reel3_div = len(reels[3]) * len(reels[4]) 82 | reel4_div = len(reels[4]) 83 | 84 | reel1_pos = rnd / reel1_div 85 | reel1_rem = rnd % reel1_div 86 | reel2_pos = reel1_rem / reel2_div 87 | reel2_rem = reel1_rem % reel2_div 88 | reel3_pos = reel2_rem / reel3_div 89 | reel3_rem = reel2_rem % reel3_div 90 | reel4_pos = reel3_rem / reel4_div 91 | reel4_rem = reel3_rem % reel4_div 92 | reel5_pos = reel4_rem 93 | 94 | return reel1_pos, reel2_pos, reel3_pos, reel4_pos, reel5_pos 95 | 96 | def position_symbols(reels, pos): 97 | result = [] 98 | for i in range(0, len(reels)): 99 | double_reel = reels[i] * 2 100 | symbols = double_reel[pos[i]:pos[i] + 3] 101 | result.append(symbols) 102 | return tuple(result) 103 | 104 | 105 | def find_paylines(symbols): 106 | result = [] 107 | for payline_nr, payline in enumerate(PAYLINES): 108 | # print "PAYLINE", payline_nr, payline 109 | 110 | line = '' 111 | for pos, offset in enumerate(payline): 112 | line += symbols[pos][offset] 113 | 114 | symbol = line[0] 115 | cnt = 0 116 | if line.startswith(symbol * 5): 117 | cnt = 5 118 | elif line.startswith(symbol * 4): 119 | cnt = 4 120 | elif line.startswith(symbol * 3): 121 | cnt = 3 122 | 123 | if cnt: 124 | if symbol in PAYOUT: 125 | payout = PAYOUT[symbol][cnt - 3] 126 | result.append((payline_nr + 1, symbol, cnt, payout)) 127 | else: 128 | print "Symbol", symbol, "not found in paytable" 129 | return result 130 | 131 | def sum_payline(paylines): 132 | return sum(payout for line, symbol, cnt, payout in paylines) 133 | 134 | def outcome(rnd): 135 | return sum_payline(find_paylines(position_symbols(REELS, rnd_positions(REELS, rnd)))) 136 | 137 | def main(): 138 | total = 0 139 | for i in range(0, 10000): 140 | rnd = random.SystemRandom().randint(0, MAX_RANDOM) 141 | out = outcome(rnd) 142 | print i, rnd, out 143 | total += out 144 | 145 | print "TOTAL", total 146 | 147 | if __name__ == '__main__': 148 | main() 149 | -------------------------------------------------------------------------------- /frontend/templates/sleth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | 11 |
12 |
13 |
14 | 15 |
16 |
17 |
18 |

19 | 23 | 24 | 25 | (- {{bet}}) 26 | 27 |

28 | 29 |
30 |
31 |

Place your bet!

32 |
33 | 34 |
35 |
  • 36 | Line {{line}} pays {{partial_payout}} 37 |
  • 38 |
  • 39 | You won {{reward.payout}}! 40 | verified 41 |
  • 42 |
  • 43 | You won nothing :( 44 | verified 45 |
  • 46 |
    47 | 48 |
    49 |
  • 50 |

    Spinning...

    51 | 52 |
  • 53 |
    54 | 55 |
    56 |
  • 57 |

    Spinning...

    58 | 59 |
  • 60 |
    61 | 62 |
    63 |
  • 64 |

    Claiming results...

    65 | 66 |
  • 67 |
    68 |
    69 |
    70 |
    71 | 72 |
    73 |
    74 | 75 | 76 | 77 |
    78 | 81 |
    82 |
    83 |
    84 |

    85 | 86 | Pay Table 87 | 88 |

    89 |
    90 |
    91 | 92 |
    93 |
    94 | 95 | 96 |
    97 | 98 |
    99 | 100 |
    101 |
    102 | -------------------------------------------------------------------------------- /frontend/paytable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Slots - Pay Table 4 | 42 | 43 | 44 | 45 |

    Slots Pay Table

    46 | 47 |

    Lines

    48 | 49 |
    50 | one line 51 |
    52 | one line plays the middle row 53 |
    54 | 55 |
    56 | three lines 57 |
    58 | three lines plays all three rows 59 |
    60 | 61 |
    62 | five lines 63 |
    64 | five lines plays rows and diagonals 65 |
    66 | 67 | 68 |

    Payouts

    69 | 70 |
    71 |
    72 |
    73 |
    74 | +2 any three downvotes 75 |
    76 |
    77 |
    78 |
    79 |
    80 | +4 three triple downs 81 |
    82 |
    83 |
    84 |
    85 |
    86 | +6 three double downs 87 |
    88 |
    89 |
    90 |
    91 |
    92 | +8 three single downs 93 |
    94 |
    95 |
    96 |
    97 |
    98 | +6 any three upvotes 99 |
    100 |
    101 |
    102 |
    103 |
    104 | +10 three single ups 105 |
    106 |
    107 |
    108 |
    109 |
    110 | +15 three double ups 111 |
    112 |
    113 |
    114 |
    115 |
    116 | +20 three triple ups 117 |
    118 |
    119 |
    120 |
    121 |
    122 | +25 three orangereds 123 |
    124 |
    125 |
    126 |
    127 |
    128 | +50 three aliens 129 |
    130 |
    131 |
    132 |
    133 |
    134 | +75 three bacons 135 |
    136 |
    137 |
    138 |
    139 |
    140 | +100 three narwhals 141 |
    142 |
    143 |
    144 |
    145 |
    146 | +250 three cakedays 147 |
    148 |
    149 |
    150 |
    151 |
    152 | bacon goes with everything 153 |
    154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /test/test_log.py: -------------------------------------------------------------------------------- 1 | from ethereum import tester 2 | 3 | class TestLog(object): 4 | 5 | CONTRACT = """ 6 | def test_log_topics(): 7 | log(1) 8 | log(1, 2) 9 | log(1, 2, 3) 10 | log(1, 2, 3, 4) 11 | 12 | def test_log_data(): 13 | log(data=[1,2,3]) 14 | 15 | def test_log_topics_and_data(): 16 | log(1, data=[1,2,3]) 17 | log(1, 2, data=[1,2,3]) 18 | log(1, 2, 3, data=[1,2,3]) 19 | log(1, 2, 3, 4, data=[1,2,3]) 20 | """ 21 | 22 | EXPECTED_DATA = '0x0000000000000000000000000000000000000000000000000000000000000001' +\ 23 | '0000000000000000000000000000000000000000000000000000000000000002' +\ 24 | '0000000000000000000000000000000000000000000000000000000000000003' 25 | 26 | def _last_logs(self, tx=0): 27 | return [log.to_dict() for log in self.s.block.get_receipt(tx).logs] 28 | 29 | def setup_class(cls): 30 | cls.s = tester.state() 31 | cls.c = cls.s.abi_contract(cls.CONTRACT) 32 | cls.snapshot = cls.s.snapshot() 33 | 34 | def setup_method(self, method): 35 | self.s.revert(self.snapshot) 36 | 37 | def test_log_topics_without_data(self): 38 | self.s.mine(1) 39 | assert self.c.test_log_topics() is None 40 | logs = self._last_logs() 41 | assert len(logs) == 4 42 | 43 | assert logs[0]['address'] == self.c.address.encode('hex') 44 | assert logs[0]['topics'] == ['0000000000000000000000000000000000000000000000000000000000000001'] 45 | assert logs[0]['data'] == '0x' 46 | 47 | assert logs[1]['address'] == self.c.address.encode('hex') 48 | assert logs[1]['topics'] == ['0000000000000000000000000000000000000000000000000000000000000001', 49 | '0000000000000000000000000000000000000000000000000000000000000002'] 50 | assert logs[1]['data'] == '0x' 51 | 52 | assert logs[2]['address'] == self.c.address.encode('hex') 53 | assert logs[2]['topics'] == ['0000000000000000000000000000000000000000000000000000000000000001', 54 | '0000000000000000000000000000000000000000000000000000000000000002', 55 | '0000000000000000000000000000000000000000000000000000000000000003'] 56 | assert logs[2]['data'] == '0x' 57 | 58 | assert logs[3]['address'] == self.c.address.encode('hex') 59 | assert logs[3]['topics'] == ['0000000000000000000000000000000000000000000000000000000000000001', 60 | '0000000000000000000000000000000000000000000000000000000000000002', 61 | '0000000000000000000000000000000000000000000000000000000000000003', 62 | '0000000000000000000000000000000000000000000000000000000000000004'] 63 | assert logs[3]['data'] == '0x' 64 | 65 | def test_log_data(self): 66 | self.s.mine(1) 67 | assert self.c.test_log_data() is None 68 | logs = self._last_logs() 69 | assert len(logs) == 1 70 | 71 | assert logs[0]['address'] == self.c.address.encode('hex') 72 | assert logs[0]['topics'] == [] 73 | assert logs[0]['data'] == self.EXPECTED_DATA 74 | 75 | def test_log_topics_and_data(self): 76 | self.s.mine(1) 77 | assert self.c.test_log_topics_and_data() is None 78 | logs = self._last_logs() 79 | assert len(logs) == 4 80 | 81 | assert logs[0]['address'] == self.c.address.encode('hex') 82 | assert logs[0]['topics'] == ['0000000000000000000000000000000000000000000000000000000000000001'] 83 | assert logs[0]['data'] == self.EXPECTED_DATA 84 | 85 | assert logs[1]['address'] == self.c.address.encode('hex') 86 | assert logs[1]['topics'] == ['0000000000000000000000000000000000000000000000000000000000000001', 87 | '0000000000000000000000000000000000000000000000000000000000000002'] 88 | assert logs[1]['data'] == self.EXPECTED_DATA 89 | 90 | assert logs[2]['address'] == self.c.address.encode('hex') 91 | assert logs[2]['topics'] == ['0000000000000000000000000000000000000000000000000000000000000001', 92 | '0000000000000000000000000000000000000000000000000000000000000002', 93 | '0000000000000000000000000000000000000000000000000000000000000003'] 94 | assert logs[2]['data'] == self.EXPECTED_DATA 95 | 96 | assert logs[3]['address'] == self.c.address.encode('hex') 97 | assert logs[3]['topics'] == ['0000000000000000000000000000000000000000000000000000000000000001', 98 | '0000000000000000000000000000000000000000000000000000000000000002', 99 | '0000000000000000000000000000000000000000000000000000000000000003', 100 | '0000000000000000000000000000000000000000000000000000000000000004'] 101 | assert logs[3]['data'] == self.EXPECTED_DATA 102 | -------------------------------------------------------------------------------- /frontend/js/reels.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Copyright (c) 2015 Joris Bontje 4 | Copyright (c) 2012 Clint Bellanger 5 | 6 | MIT License: 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | 14 | 15 | Sounds by Brandon Morris (CC-BY 3.0) 16 | Art by Clint Bellanger (CC-BY 3.0) 17 | 18 | */ 19 | 20 | "use strict"; 21 | 22 | var app = angular.module('slots.reels', []); 23 | 24 | app.directive('slotsReels', ['$q', 'config', 'game', function($q, config, game) { 25 | return { 26 | restrict: 'A', 27 | link: function(scope, element, attrs) { 28 | var reel_area_left = config.reel_scale*32; 29 | var reel_area_top = config.reel_scale*32; 30 | var reel_area_width = 96; 31 | var reel_area_height = 96; 32 | 33 | var ctx = element[0].getContext("2d"); 34 | 35 | var symbols_loaded = false; 36 | var reels_bg_loaded = false; 37 | 38 | // art 39 | var symbols = new Image(); 40 | var reels_bg = new Image(); 41 | 42 | symbols.src = "images/reddit_icons_small.png"; 43 | reels_bg.src = "images/reels_bg.png"; 44 | 45 | function draw_symbol(symbol_index, x, y) { 46 | var symbol_pixel = symbol_index * config.symbol_size; 47 | ctx.drawImage(symbols, 0,symbol_pixel,config.symbol_size,config.symbol_size,config.reel_scale*x+reel_area_left,config.reel_scale*y+reel_area_top,config.reel_scale*config.symbol_size,config.reel_scale*config.symbol_size); 48 | } 49 | 50 | function render_reel() { 51 | // clear reel 52 | ctx.drawImage(reels_bg, reel_area_left, reel_area_top, config.reel_scale*reel_area_width, config.reel_scale*reel_area_height); 53 | 54 | // set clipping area 55 | ctx.beginPath(); 56 | ctx.rect(reel_area_left, reel_area_top, config.reel_scale*reel_area_width, config.reel_scale*reel_area_height); 57 | ctx.clip(); 58 | 59 | var reel_index; 60 | var symbol_offset; 61 | var symbol_index; 62 | var x; 63 | var y; 64 | 65 | for (var i=0; i= config.reel_positions) reel_index -= config.reel_positions; 73 | 74 | // symbol lookup 75 | symbol_index = game.reels[i][reel_index]; 76 | 77 | x = i * config.symbol_size; 78 | y = j * config.symbol_size - symbol_offset; 79 | 80 | draw_symbol(symbol_index, x, y); 81 | 82 | } 83 | } 84 | } 85 | 86 | // render all art needed in the current frame 87 | function render() { 88 | if (game.state === game.STATE_SPINUP || game.state === game.STATE_SPINMAX || game.state === game.STATE_SPINDOWN) { 89 | render_reel(); 90 | } 91 | requestAnimFrame(render); 92 | } 93 | 94 | function highlight_line(line_num) { 95 | ctx.strokeStyle = "orange"; 96 | ctx.lineWidth = config.reel_scale; 97 | var ss = config.reel_scale*config.symbol_size; 98 | 99 | // top row 100 | if (line_num === 2 || line_num === 4) { 101 | ctx.strokeRect(reel_area_left+1, reel_area_top+1, ss-1, ss-1); // top left 102 | } 103 | if (line_num === 2) { 104 | ctx.strokeRect(reel_area_left + ss, reel_area_top+1, ss-1, ss-1); // top middle 105 | } 106 | if (line_num === 2 || line_num === 5) { 107 | ctx.strokeRect(reel_area_left + ss + ss, reel_area_top+1, ss-1, ss-1); // top right 108 | } 109 | 110 | // middle row 111 | if (line_num === 1) { 112 | ctx.strokeRect(reel_area_left+1, reel_area_top + ss, ss-1, ss-1); // top left 113 | } 114 | if (line_num === 1 || line_num === 4 || line_num === 5) { 115 | ctx.strokeRect(reel_area_left + ss, reel_area_top + ss, ss-1, ss-1); // top middle 116 | } 117 | if (line_num === 1) { 118 | ctx.strokeRect(reel_area_left + ss + ss, reel_area_top + ss, ss-1, ss-1); // top right 119 | } 120 | 121 | // bottom row 122 | if (line_num === 3 || line_num === 5) { 123 | ctx.strokeRect(reel_area_left+1, reel_area_top + ss + ss, ss-1, ss-1); // top left 124 | } 125 | if (line_num === 3) { 126 | ctx.strokeRect(reel_area_left + ss, reel_area_top + ss + ss, ss-1, ss-1); // top middle 127 | } 128 | if (line_num === 3 || line_num === 4) { 129 | ctx.strokeRect(reel_area_left + ss + ss, reel_area_top + ss + ss, ss-1, ss-1); // top right 130 | } 131 | } 132 | 133 | scope.$on('slots:reward', function(evt, reward) { 134 | reward.highlights.forEach(highlight_line); 135 | }); 136 | 137 | var symbolsDefer = $q.defer(); 138 | var reelsBgDefer = $q.defer(); 139 | 140 | symbols.onload = function() { 141 | symbolsDefer.resolve(symbols); 142 | }; 143 | 144 | reels_bg.onload = function() { 145 | reelsBgDefer.resolve(reels_bg); 146 | }; 147 | 148 | $q.all([symbolsDefer, reelsBgDefer.promise]).then(render_reel); 149 | 150 | var requestAnimFrame = (function() { 151 | return window.requestAnimationFrame || 152 | window.webkitRequestAnimationFrame || 153 | window.mozRequestAnimationFrame || 154 | window.oRequestAnimationFrame || 155 | window.msRequestAnimationFrame || 156 | function(/* function FrameRequestCallback */ callback, /* DOMElement Element */ element) { 157 | window.setTimeout(callback, 1000/60); 158 | }; 159 | })(); 160 | 161 | requestAnimFrame(render); 162 | } 163 | }; 164 | }]); 165 | -------------------------------------------------------------------------------- /contracts/sleth.se: -------------------------------------------------------------------------------- 1 | data owner 2 | data total_spins 3 | data total_coins_bet 4 | data total_coins_won 5 | 6 | data rounds[2^160](player, block, bet, result, entropy, status) 7 | data players[2^160](last_round) 8 | 9 | # status: 10 | # 0 = new 11 | # 1 = spinning 12 | # 2 = done 13 | 14 | # constants 15 | 16 | macro ETHER(): 17 | 10 ** 18 18 | 19 | macro REEL_COUNT(): 20 | 3 21 | 22 | macro REEL_POSITIONS(): 23 | 32 24 | 25 | macro ROW_COUNT(): 26 | 3 27 | 28 | def init(): 29 | self.owner = msg.sender 30 | 31 | def spin(bet): 32 | if bet != 1 and bet != 3 and bet != 5: 33 | return(0) # invalid number of lines 34 | 35 | if msg.value / ETHER() != bet: 36 | return(0) # value doesn't match bet 37 | 38 | current_round = self.total_spins + 1 39 | self.rounds[current_round].player = msg.sender 40 | self.rounds[current_round].block = block.number 41 | self.rounds[current_round].bet = bet 42 | self.rounds[current_round].status = 1 43 | self.players[msg.sender].last_round = current_round 44 | self.total_spins += 1 45 | self.total_coins_bet += bet 46 | 47 | return(1) 48 | 49 | def claim(round): 50 | if self.rounds[round].status != 1: 51 | return(90) # invalid status 52 | 53 | if self.rounds[round].player != msg.sender: 54 | return(91) # you can only claim your own rounds 55 | 56 | target_block = self.rounds[round].block 57 | 58 | if block.number <= target_block: 59 | return(92) # not yet ready 60 | 61 | if block.number > target_block + 255: 62 | return(93) # out of range 63 | 64 | # calculate entropy. hex("sleth") == 0x736c657468 65 | entropy = sha3([blockhash(target_block), round, 0x736c657468], items=3) 66 | 67 | if entropy == 0: 68 | return(94) # invalid entropy 69 | 70 | # set entropy and rnd 71 | self.rounds[round].entropy = entropy 72 | rnd = mod(entropy, REEL_POSITIONS() * REEL_POSITIONS() * REEL_POSITIONS()) 73 | 74 | playing_lines = self.rounds[round].bet 75 | 76 | result = self.calc_reward(rnd, playing_lines) 77 | self.rounds[round].result = result 78 | self.rounds[round].status = 2 79 | 80 | if result > 0: 81 | self.total_coins_won += result 82 | send(msg.sender, result * ETHER()) 83 | 84 | log(msg.sender, data=[round]) 85 | 86 | return(1) 87 | 88 | def get_round(round): 89 | return([self.rounds[round].player, self.rounds[round].block, self.rounds[round].bet, self.rounds[round].result, self.rounds[round].entropy, self.rounds[round].status]:arr) 90 | 91 | def get_current_round(): 92 | return(self.players[msg.sender].last_round) 93 | 94 | def get_stats(): 95 | return([self.total_spins, self.total_coins_bet, self.total_coins_won]:arr) 96 | 97 | macro MATCH_PAYOUT($s): 98 | byte($s + 21, match_payout) 99 | 100 | macro PAYOUT_UPS(): 101 | 6 # Any 3 Ups 102 | 103 | macro PAYOUT_DOWNS(): 104 | 2 # Any 3 Downs 105 | 106 | # checks if symbol is 1Up, 2Up or 3Up 107 | macro IS_UP($s): 108 | ($s == 1 or $s == 2 or $s == 3) 109 | 110 | # checks if symbol is 1Down, 2Down or 3Down 111 | macro IS_DOWN($s): 112 | ($s == 5 or $s == 6 or $s == 7) 113 | 114 | def calc_line(s1, s2, s3): 115 | # match_payout[0] = 50 == 0x32 # AlienHead 116 | # match_payout[1] = 10 == 0x0a # 1Up 117 | # match_payout[2] = 15 == 0x0f # 2Up 118 | # match_payout[3] = 20 == 0x14 # 3Up 119 | # match_payout[4] = 25 == 0x19 # OrangeRed 120 | # match_payout[5] = 8 == 0x08 # 1Down 121 | # match_payout[6] = 6 == 0x06 # 2Down 122 | # match_payout[7] = 4 == 0x04 # 3Down 123 | # match_payout[8] = 250 == 0xfa # CakeDay 124 | # match_payout[9] = 75 == 0x4b # Bacon 125 | # match_payout[10] = 100 == 0x64 # Narwhal 126 | match_payout = 0x320a0f1419080604fa4b64 127 | 128 | # perfect match 129 | if s1 == s2 and s2 == s3: 130 | return(MATCH_PAYOUT(s1)) 131 | 132 | # special case #1: triple ups 133 | if IS_UP(s1) and IS_UP(s2) and IS_UP(s3): 134 | return(PAYOUT_UPS()) 135 | 136 | # special case #2: triple down 137 | if IS_DOWN(s1) and IS_DOWN(s2) and IS_DOWN(s3): 138 | return(PAYOUT_DOWNS()) 139 | 140 | # special case #3: bacon goes with everything 141 | if s1 == 9: 142 | if s2 == s3: 143 | return(MATCH_PAYOUT(s2)) 144 | 145 | # wildcard trip ups 146 | if IS_UP(s2) and IS_UP(s3): 147 | return(PAYOUT_UPS()) 148 | 149 | # wildcard trip downs 150 | if IS_DOWN(s2) and IS_DOWN(s3): 151 | return(PAYOUT_DOWNS()) 152 | 153 | if s2 == 9: 154 | if s1 == s3: 155 | return(MATCH_PAYOUT(s1)) 156 | 157 | # wildcard trip ups 158 | if IS_UP(s1) and IS_UP(s3): 159 | return(PAYOUT_UPS()) 160 | 161 | # wildcard trip downs 162 | if IS_DOWN(s1) and IS_DOWN(s3): 163 | return(PAYOUT_DOWNS()) 164 | 165 | if s3 == 9: 166 | if s1 == s2: 167 | return(MATCH_PAYOUT(s1)) 168 | 169 | # wildcard trip ups 170 | if IS_UP(s1) and IS_UP(s2): 171 | return(PAYOUT_UPS()) 172 | 173 | # wildcard trip downs 174 | if IS_DOWN(s1) and IS_DOWN(s2): 175 | return(PAYOUT_DOWNS()) 176 | 177 | # check double-bacon 178 | if s2 == 9 and s3 == 9: 179 | return(MATCH_PAYOUT(s1)) 180 | if s1 == 9 and s3 == 9: 181 | return(MATCH_PAYOUT(s2)) 182 | if s1 == 9 and s2 == 9: 183 | return(MATCH_PAYOUT(s3)) 184 | 185 | # no reward 186 | return(0) 187 | 188 | def get_stops(rnd): 189 | stops = array(REEL_COUNT()) 190 | i = 0 191 | while i < REEL_COUNT(): 192 | stops[i] = mod(rnd, REEL_POSITIONS()) 193 | rnd = div(rnd, REEL_POSITIONS()) 194 | i += 1 195 | return(stops, items=REEL_COUNT()) 196 | 197 | macro REEL_0($pos): 198 | # 2,1,7,1,2,7,6,7,3,10,1,6,1,7,3,4,3,2,4,5,0,6,10,5,6,5,8,3,0,9,5,4 199 | byte($pos, 0x0201070102070607030a0106010703040302040500060a050605080300090504) 200 | 201 | macro REEL_1($pos): 202 | # 6,0,10,3,6,7,9,2,5,2,3,1,5,2,1,10,4,5,8,4,7,6,0,1,7,6,3,1,5,9,7,4 203 | byte($pos, 0x06000a0306070902050203010502010a04050804070600010706030105090704) 204 | 205 | macro REEL_2($pos): 206 | # 1,4,2,7,5,6,4,10,7,5,2,0,6,4,10,1,7,6,3,0,5,7,2,3,9,3,5,6,1,8,1,3 207 | byte($pos, 0x010402070506040a0705020006040a0107060300050702030903050601080103) 208 | 209 | macro LINES($line, $pos): 210 | byte(3 * $line + $pos + 18, lines) 211 | 212 | def calc_reward(rnd, playing_lines): 213 | result_0 = array(ROW_COUNT()) 214 | result_1 = array(ROW_COUNT()) 215 | result_2 = array(ROW_COUNT()) 216 | 217 | stops = self.get_stops(rnd, outsz=REEL_COUNT()) 218 | i = 0 219 | while i < ROW_COUNT(): 220 | result_0[i] = REEL_0(mod(stops[0] + i, REEL_POSITIONS())) 221 | result_1[i] = REEL_1(mod(stops[1] + i, REEL_POSITIONS())) 222 | result_2[i] = REEL_2(mod(stops[2] + i, REEL_POSITIONS())) 223 | i += 1 224 | 225 | payout = 0 226 | line = 0 227 | 228 | # lines[0] = 0x01 01 01 229 | # lines[1] = 0x00 00 00 230 | # lines[2] = 0x02 02 02 231 | # lines[3] = 0x00 01 02 232 | # lines[4] = 0x02 01 00 233 | lines = 0x0101010000000202020001020201 234 | while line < playing_lines: 235 | payout += self.calc_line(result_0[LINES(line, 0)], result_1[LINES(line, 1)], result_2[LINES(line, 2)]) 236 | line += 1 237 | 238 | return(payout) 239 | 240 | def suicide(): 241 | if msg.sender == self.owner: 242 | suicide(msg.sender) 243 | -------------------------------------------------------------------------------- /contracts/commit_reveal_entropy.se: -------------------------------------------------------------------------------- 1 | # 2 | # Commit / Reveal Entropy Contract 3 | # 4 | # this contract allows for a stream of random numbers to be made available to 5 | # third party contracts (ie games). entropy provides can commit a random 6 | # value, which they have to reveal in a number of blocks (otherwise they lose 7 | # their deposit) 8 | # 9 | 10 | # TODO 11 | # - review algorithms for random seed and hash chain 12 | 13 | data ticket_count 14 | data tickets[2^50](owner, status, target, random) 15 | # requested entropy is tied to tickets. there can be multiple tickets per 16 | # block. random value for each ticket are generated from a hashchain based on 17 | # the random seed of the block 18 | 19 | data blocks[2^50](seed, commit_count, reveal_count, ticket_count, commits[2^20](sender, hash, value), committers[2^160], tickets[2^20]) 20 | # commits are made per block. reveal required to get deposit back. 21 | 22 | def shared(): 23 | REVEAL_WINDOW = 2 # time in which reveals are accepted 24 | QUIET_WINDOW = 1 # time between last commit and first reveal 25 | ENTROPY_COST = 10 ** 15 # cost of one piece of entropy 26 | DEPOSIT_COST = 10 ** 18 # how much ETH it will cost to deposit 27 | 28 | # Request a future piece of entropy. 29 | # There is a cost associated with this, which will be distributed among the 30 | # committers. 31 | # 32 | # Returns: 33 | # ticket_id - the ID of your entropy ticket, used for future reference 34 | # block_eta - the block number after which your entropy will be available 35 | # or 0 if the required cost wasn't met. 36 | def request_entropy(): 37 | if msg.value < ENTROPY_COST: 38 | return([0]:arr) # insufficient fee 39 | 40 | target = block.number + 1 41 | 42 | # initialize ticket 43 | ticket_id = self.ticket_count 44 | self.tickets[ticket_id].owner = msg.sender 45 | self.tickets[ticket_id].target = target 46 | self.ticket_count = ticket_id + 1 47 | 48 | # add ticket to target block 49 | ticket_idx = self.blocks[target].ticket_count 50 | self.blocks[target].tickets[ticket_idx] = ticket_id 51 | self.blocks[target].ticket_count = ticket_idx + 1 52 | 53 | eta = target + QUIET_WINDOW + REVEAL_WINDOW 54 | 55 | # return ticket_id and eta 56 | return([ticket_id, eta]:arr) 57 | 58 | # Get your entropy 59 | # 60 | # Parameter: 61 | # ticket_id - the ID of your entropy ticket 62 | # 63 | # Returns: 64 | # status - 0 = pending | 1 = ready | 2 = expired | 3 = not yours / not found 65 | # entropy - your random number 66 | def get_entropy(ticket_id): 67 | if msg.sender != self.tickets[ticket_id].owner: 68 | return([3, 0]:arr) # not yours / not found 69 | 70 | status = self.tickets[ticket_id].status 71 | if status == 1: 72 | return([1, self.tickets[ticket_id].random]:arr) # ready 73 | else: 74 | if block.number > self.tickets[ticket_id].target + REVEAL_WINDOW: 75 | return([2, 0]:arr) # expired 76 | else: 77 | return([0, 0]:arr) # pending 78 | 79 | # Returns entropy ticket as-is 80 | # 81 | # Parameter: 82 | # ticket_id - the ID of your entropy ticket 83 | def get_entropy_ticket(ticket_id): 84 | return([self.tickets[ticket_id].owner, self.tickets[ticket_id].status, self.tickets[ticket_id].target, self.tickets[ticket_id].random]:arr) 85 | 86 | # Commit your random contribution to a future block 87 | # There is a deposit cost associated with this, which will be returned upon 88 | # successful revealing. 89 | # 90 | # Parameters: 91 | # target - the block number that you want to contribute entropy towards 92 | # hash - the SHA3 hash of your entropy 93 | # 94 | # Returns: 95 | # 0 on error 96 | # 1 on success 97 | # 98 | # XXX how many blocks in the future could you be allowed to commit to? 99 | def commit(target, hash): 100 | if msg.value < DEPOSIT_COST: 101 | return(0) # insufficient deposit 102 | 103 | if target <= block.number: 104 | return(0) # block no longer available 105 | 106 | if hash == 0: 107 | return(0) # invalid hash 108 | 109 | if self.blocks[target].committers[msg.sender] != 0: 110 | return(0) # you have already committed 111 | 112 | # store hash on list 113 | commit_idx = self.blocks[target].commit_count 114 | self.blocks[target].commits[commit_idx].sender = msg.sender 115 | self.blocks[target].commits[commit_idx].hash = hash 116 | self.blocks[target].commit_count = commit_idx + 1 117 | 118 | # store committer mapping 119 | self.blocks[target].committers[msg.sender] = commit_idx + 1 # add 1 to differentiate from non existing committer 120 | 121 | return(1) 122 | 123 | # Reveal your entropy 124 | # 125 | # Parameters: 126 | # target - the block number that you previously commited entropy towards 127 | # value - the value belonging to the previously commited hash 128 | # 129 | # Returns: 130 | # 1 on success 131 | # 9x on error 132 | # 133 | # XXX cost of last reveal is substantially higher 134 | def reveal(target, value): 135 | 136 | if target + QUIET_WINDOW > block.number: 137 | return(90) # not yet allowed to reveal 138 | 139 | if block.number > target + QUIET_WINDOW + REVEAL_WINDOW: 140 | return(91) # reveal window expired 141 | 142 | commit_idx = self.blocks[target].committers[msg.sender] 143 | if commit_idx == 0: 144 | return(92) # you don't have committed anything 145 | commit_idx -= 1 # compensate for 0 is non existing committer 146 | 147 | if self.blocks[target].commits[commit_idx].value != 0: 148 | return(93) # already revealed 149 | 150 | hash = self.blocks[target].commits[commit_idx].hash 151 | if sha3(value) != hash: 152 | return(94) # hash mismatch 153 | 154 | # all is good 155 | self.blocks[target].commits[commit_idx].value = value 156 | self.blocks[target].reveal_count += 1 157 | send(msg.sender, DEPOSIT_COST) # return deposit 158 | 159 | commit_count = self.blocks[target].commit_count 160 | if commit_count == self.blocks[target].reveal_count: 161 | # all reveals are in! 162 | # 163 | if self.blocks[target].seed != 0: 164 | return(97) # already have a seed 165 | 166 | # each committer gets their share of the total collected entropy cost for this block 167 | committer_payout = self.blocks[target].ticket_count * ENTROPY_COST / commit_count 168 | 169 | # calculate the random seed and do payouts to the committers 170 | # H(c_0 || r_0 || c_1 || r_1 || .. || c_n || r_n) 171 | # TODO review and improve hash chain function! 172 | values = array(commit_count * 2) 173 | 174 | c = 0 175 | while c < commit_count: 176 | values[2 * c] = self.blocks[target].commits[c].hash 177 | values[2 * c + 1] = self.blocks[target].commits[c].value 178 | send(self.blocks[target].commits[c].sender, committer_payout) 179 | c += 1 180 | 181 | seed = sha3(values, items=commit_count * 2) 182 | self.blocks[target].seed = seed 183 | 184 | # calculate randomness for individual tickets 185 | # R_0 = seed; R_n = H(R_n-1) 186 | # TODO review and improve hash chain function! 187 | ticket_count = self.blocks[target].ticket_count 188 | random = seed 189 | 190 | c = 0 191 | while c < ticket_count: 192 | random = sha3(random) 193 | ticket_id = self.blocks[target].tickets[c] 194 | self.tickets[ticket_id].status = 1 195 | self.tickets[ticket_id].random = random 196 | c += 1 197 | 198 | return(1) 199 | 200 | # Returns block data as-is 201 | # 202 | # Parameter: 203 | # target - the target block number 204 | def get_block(target): 205 | return([self.blocks[target].seed, self.blocks[target].commit_count, self.blocks[target].reveal_count, self.blocks[target].ticket_count]:arr) 206 | 207 | # Calculate sha3 hash on the value, for testing purposes 208 | def hash(value): 209 | return(sha3(value)) 210 | -------------------------------------------------------------------------------- /sleth/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | from pprint import pprint 5 | 6 | from pyepm import api, config 7 | import serpent 8 | 9 | from constants import CONTRACT_FILE, CONTRACT_GAS, SPIN_GAS, CLAIM_GAS, ETHER 10 | 11 | 12 | def cmd_spin(instance, args): 13 | print "Spinning the slots with bet", args.bet 14 | assert instance.is_contract_at(args.contract), "Contract not found" 15 | instance.transact(args.contract, fun_name='spin', sig='i', data=[int(args.bet)], value=int(args.bet) * ETHER, gas=SPIN_GAS) 16 | 17 | def cmd_claim(instance, args): 18 | print "Claiming round ", args.round 19 | assert instance.is_contract_at(args.contract), "Contract not found" 20 | instance.transact(args.contract, fun_name='claim', sig='i', data=[int(args.round)], gas=CLAIM_GAS) 21 | 22 | def cmd_get_round(instance, args): 23 | print "Getting information about round", args.round 24 | assert instance.is_contract_at(args.contract), "Contract not found" 25 | result = instance.call(args.contract, fun_name='get_round', sig='i', data=[int(args.round)]) 26 | assert result, "No result returned to call" 27 | array_len, player, block, bet, result, entropy, status = result 28 | print "Player:", hex(player) 29 | print "Block:", block 30 | print "Bet:", bet 31 | print "Result:", result 32 | print "Entropy:", hex(entropy) 33 | print "RND:", entropy % 32768 34 | print "Status:", status 35 | 36 | def cmd_get_current_round(instance, args): 37 | print "Getting information about the current player round" 38 | assert instance.is_contract_at(args.contract), "Contract not found" 39 | result = instance.call(args.contract, fun_name='get_current_round', sig='', data=[]) 40 | assert result, "No result returned to call" 41 | print "Current round:", result[0] 42 | 43 | def cmd_get_stats(instance, args): 44 | print "Getting statistics" 45 | assert instance.is_contract_at(args.contract), "Contract not found" 46 | result = instance.call(args.contract, fun_name='get_stats', sig='', data=[]) 47 | assert result, "No result returned to call" 48 | array_len, total_spins, total_coins_bet, total_coins_won = result 49 | print "Total spins:", total_spins 50 | print "Total coins bet:", total_coins_bet 51 | print "Total coins won:", total_coins_won 52 | if total_coins_bet > 0: 53 | print "Payout percentage: %.2f" % (float(total_coins_won) / total_coins_bet * 100) 54 | 55 | def cmd_suicide(instance, args): 56 | print "Killing the contract" 57 | assert instance.is_contract_at(args.contract), "Contract not found" 58 | instance.transact(args.contract, fun_name='suicide', sig='', data=[]) 59 | 60 | def cmd_create(instance, args): 61 | creator_balance = instance.balance_at(args.from_address) 62 | balance_required = CONTRACT_GAS * 1e+13 + args.endowment * ETHER 63 | if creator_balance < balance_required: 64 | print "Insufficient balance to cover gas for contract creation." 65 | print "You need at least %d wei in account %s (current balance is %d wei)." % \ 66 | (balance_required, args.from_address, creator_balance) 67 | return 68 | 69 | contract = serpent.compile(open(CONTRACT_FILE).read()).encode('hex') 70 | 71 | contract_address = instance.create(contract, gas=CONTRACT_GAS, endowment=args.endowment * ETHER) 72 | 73 | print "Contract will be available at %s" % contract_address 74 | instance.wait_for_contract(contract_address, verbose=True) 75 | print "Is contract?", instance.is_contract_at(contract_address) 76 | 77 | def cmd_inspect(instance, args): 78 | defaultBlock = 'latest' 79 | if args.pending: 80 | defaultBlock = 'pending' 81 | 82 | result = instance.is_contract_at(args.contract, defaultBlock) 83 | print "Is contract?", result 84 | 85 | result = instance.balance_at(args.contract, defaultBlock) 86 | print "Balance", result 87 | 88 | print "Owner:" 89 | result = instance.storage_at(args.contract, 0, defaultBlock) 90 | pprint(result) 91 | 92 | print "Logs:" 93 | logs = instance.logs({'address': args.contract, 'fromBlock': hex(0), 'toBlock': 'latest'}) 94 | pprint(logs) 95 | 96 | def cmd_status(instance, args): 97 | print "Coinbase: %s" % instance.coinbase() 98 | print "Listening? %s" % instance.is_listening() 99 | print "Mining? %s" % instance.is_mining() 100 | print "Peer count: %d" % instance.peer_count() 101 | block_number = instance.number() 102 | print "Number: %d" % block_number 103 | 104 | last_block = instance.block(block_number) 105 | print "Last Block:" 106 | pprint(last_block) 107 | 108 | accounts = instance.accounts() 109 | print "Accounts:" 110 | for address in accounts: 111 | balance = instance.balance_at(address) 112 | print "- %s %.4e" % (address, balance) 113 | 114 | def cmd_transact(instance, args): 115 | tx_count = instance.transaction_count() 116 | instance.transact(args.dest, value=args.value * ETHER) 117 | instance.wait_for_transaction(tx_count, verbose=True) 118 | 119 | def main(): 120 | api_config = config.read_config() 121 | instance = api.Api(api_config) 122 | 123 | parser = argparse.ArgumentParser() 124 | from_address = instance.accounts()[0] 125 | parser.add_argument('--from_address', default=from_address, help='address to send transactions from') 126 | 127 | subparsers = parser.add_subparsers(help='sub-command help') 128 | parser_create = subparsers.add_parser('create', help='create the contract') 129 | parser_create.set_defaults(func=cmd_create) 130 | parser_create.add_argument('--endowment', type=int, default=500, help='value to endow in ether') 131 | 132 | parser_inspect = subparsers.add_parser('inspect', help='inspect the contract') 133 | parser_inspect.set_defaults(func=cmd_inspect) 134 | parser_inspect.add_argument('contract', help='sleth contract address') 135 | parser_inspect.add_argument('--pending', action='store_true', help='look in pending transactions instead of mined') 136 | 137 | parser_status = subparsers.add_parser('status', help='display the eth node status') 138 | parser_status.set_defaults(func=cmd_status) 139 | 140 | parser_transact = subparsers.add_parser('transact', help='transact ether to destination (default: 1 ETH)') 141 | parser_transact.set_defaults(func=cmd_transact) 142 | parser_transact.add_argument('dest', help='destination') 143 | parser_transact.add_argument('--value', type=int, default=1, help='value to transfer in ether') 144 | 145 | parser_spin = subparsers.add_parser('spin', help='make a spin') 146 | parser_spin.set_defaults(func=cmd_spin) 147 | parser_spin.add_argument('contract', help='sleth contract address') 148 | parser_spin.add_argument('bet', help='bet amount') 149 | 150 | parser_claim = subparsers.add_parser('claim', help='clain a round') 151 | parser_claim.set_defaults(func=cmd_claim) 152 | parser_claim.add_argument('contract', help='sleth contract address') 153 | parser_claim.add_argument('round', help='round number to claim') 154 | 155 | parser_get_round = subparsers.add_parser('get_round', help='get round information') 156 | parser_get_round.set_defaults(func=cmd_get_round) 157 | parser_get_round.add_argument('contract', help='sleth contract address') 158 | parser_get_round.add_argument('round', help='round number') 159 | 160 | parser_get_current_round = subparsers.add_parser('get_current_round', help='get current round') 161 | parser_get_current_round.set_defaults(func=cmd_get_current_round) 162 | parser_get_current_round.add_argument('contract', help='sleth contract address') 163 | 164 | parser_get_stats = subparsers.add_parser('get_stats', help='get contract statistics') 165 | parser_get_stats.set_defaults(func=cmd_get_stats) 166 | parser_get_stats.add_argument('contract', help='sleth contract address') 167 | 168 | parser_suicide = subparsers.add_parser('suicide', help='kills the contract') 169 | parser_suicide.set_defaults(func=cmd_suicide) 170 | parser_suicide.add_argument('contract', help='sleth contract address') 171 | 172 | args = parser.parse_args() 173 | 174 | print "Using from_address = %s" % (args.from_address) 175 | instance.address = args.from_address 176 | args.func(instance, args) 177 | 178 | if __name__ == '__main__': 179 | main() 180 | -------------------------------------------------------------------------------- /test/test_commit_reveal_entropy_contract.py: -------------------------------------------------------------------------------- 1 | from ethereum import tester 2 | from ethereum import utils 3 | 4 | def hash_value(value): 5 | return utils.big_endian_to_int(utils.sha3(utils.zpad(value, 32))) 6 | 7 | 8 | class TestCommitRevealEntropyContract(object): 9 | 10 | CONTRACT = 'contracts/commit_reveal_entropy.se' 11 | CONTRACT_GAS = 1014114 12 | COW_HASH = hash_value('cow') 13 | COW_INT = utils.big_endian_to_int('cow') 14 | MONKEY_INT = utils.big_endian_to_int('monkey') 15 | 16 | ENTROPY_COST = 10 ** 15 17 | DEPOSIT_COST = 10 ** 18 18 | 19 | def setup_class(cls): 20 | cls.s = tester.state() 21 | cls.c = cls.s.abi_contract(cls.CONTRACT, gas=cls.CONTRACT_GAS) 22 | cls.snapshot = cls.s.snapshot() 23 | 24 | def setup_method(self, method): 25 | self.s.revert(self.snapshot) 26 | 27 | def test_create_gas_used(self): 28 | print "create gas used:", self.s.block.gas_used 29 | assert self.s.block.gas_used <= self.CONTRACT_GAS 30 | 31 | def test_request_entropy(self): 32 | assert self.c.request_entropy(value=self.ENTROPY_COST) == [0, 4] 33 | assert self.c.get_entropy_ticket(0) == [utils.big_endian_to_int(tester.a0), 0, self.s.block.number + 1, 0] 34 | assert self.c.get_entropy(0) == [0, 0] # pending 35 | assert self.c.get_block(self.s.block.number + 1) == [0, 0, 0, 1] 36 | 37 | assert self.s.block.get_balance(tester.a0) == 10 ** 24 - self.ENTROPY_COST 38 | 39 | def test_request_entropy_insufficient_fee(self): 40 | assert self.c.request_entropy(value=0) == [0] 41 | assert self.c.get_entropy_ticket(0) == [0, 0, 0, 0] 42 | assert self.c.get_entropy(0) == [3, 0] # not found 43 | 44 | def test_request_multiple_entropy_tickets(self): 45 | assert self.c.request_entropy(value=self.ENTROPY_COST) == [0, 4] 46 | assert self.c.request_entropy(value=self.ENTROPY_COST) == [1, 4] 47 | assert self.c.request_entropy(value=self.ENTROPY_COST) == [2, 4] 48 | 49 | assert self.c.get_entropy_ticket(0) == [utils.big_endian_to_int(tester.a0), 0, self.s.block.number + 1, 0] 50 | assert self.c.get_entropy_ticket(1) == [utils.big_endian_to_int(tester.a0), 0, self.s.block.number + 1, 0] 51 | assert self.c.get_entropy_ticket(2) == [utils.big_endian_to_int(tester.a0), 0, self.s.block.number + 1, 0] 52 | 53 | assert self.c.get_block(self.s.block.number + 1) == [0, 0, 0, 3] 54 | 55 | def test_request_multiple_entropy_tickets_different_senders(self): 56 | assert self.c.request_entropy(value=self.ENTROPY_COST) == [0, 4] 57 | assert self.c.request_entropy(sender=tester.k1, value=self.ENTROPY_COST) == [1, 4] 58 | 59 | assert self.c.get_entropy_ticket(0) == [utils.big_endian_to_int(tester.a0), 0, self.s.block.number + 1, 0] 60 | assert self.c.get_entropy_ticket(1) == [utils.big_endian_to_int(tester.a1), 0, self.s.block.number + 1, 0] 61 | 62 | assert self.c.get_block(self.s.block.number + 1) == [0, 0, 0, 2] 63 | 64 | def test_request_entropy_target_depends_on_block_number(self): 65 | self.s.mine() 66 | assert self.c.request_entropy(value=self.ENTROPY_COST) == [0, 5] 67 | assert self.c.get_entropy_ticket(0) == [utils.big_endian_to_int(tester.a0), 0, self.s.block.number + 1, 0] 68 | 69 | self.s.mine(10) 70 | assert self.c.request_entropy(value=self.ENTROPY_COST) == [1, 15] 71 | assert self.c.get_entropy_ticket(1) == [utils.big_endian_to_int(tester.a0), 0, self.s.block.number + 1, 0] 72 | 73 | def test_request_entropy_get_expired(self): 74 | assert self.c.request_entropy(value=self.ENTROPY_COST) == [0, 4] 75 | 76 | # XXX off by one? 77 | self.s.mine(4) 78 | assert self.s.block.number == 4 79 | assert self.c.get_entropy(0) == [2, 0] # expired 80 | 81 | def test_hash_sha3(self): 82 | value = 'cow' 83 | assert self.c.hash(self.COW_INT) == hash_value(value) 84 | 85 | def test_commit(self): 86 | assert self.c.commit(1, self.COW_HASH, value=self.DEPOSIT_COST) == 1 87 | assert self.c.get_block(1) == [0, 1, 0, 0] 88 | 89 | assert self.s.block.get_balance(tester.a0) == 10 ** 24 - self.DEPOSIT_COST 90 | 91 | def test_commit_insufficient_deposit(self): 92 | assert self.c.commit(4, self.COW_HASH, value=0) == 0 93 | assert self.c.get_block(4) == [0, 0, 0, 0] 94 | 95 | def test_commit_invalid_target(self): 96 | assert self.c.commit(0, self.COW_HASH, value=self.DEPOSIT_COST) == 0 97 | assert self.c.get_block(0) == [0, 0, 0, 0] 98 | 99 | self.s.mine(4) 100 | assert self.c.commit(4, self.COW_HASH, value=self.DEPOSIT_COST) == 0 101 | assert self.c.get_block(4) == [0, 0, 0, 0] 102 | 103 | def test_commit_twice(self): 104 | assert self.c.commit(4, self.COW_HASH, value=self.DEPOSIT_COST) == 1 105 | assert self.c.get_block(4) == [0, 1, 0, 0] 106 | 107 | assert self.c.commit(4, self.COW_HASH, value=self.DEPOSIT_COST) == 0 108 | assert self.c.get_block(4) == [0, 1, 0, 0] 109 | 110 | def test_commit_twice_different_senders(self): 111 | assert self.c.commit(4, self.COW_HASH, value=self.DEPOSIT_COST) == 1 112 | assert self.c.commit(4, self.COW_HASH, sender=tester.k1, value=self.DEPOSIT_COST) == 1 113 | assert self.c.get_block(4) == [0, 2, 0, 0] 114 | 115 | def test_commit_invalid_hash(self): 116 | assert self.c.commit(1, 0, value=self.DEPOSIT_COST) == 0 117 | assert self.c.get_block(0) == [0, 0, 0, 0] 118 | 119 | def test_reveal(self): 120 | self.test_commit() 121 | self.s.mine(2) 122 | 123 | balance = self.s.block.get_balance(tester.a0) 124 | assert self.c.reveal(1, self.COW_INT) == 1 125 | assert self.c.get_block(1) == [0x6d8d9b450dd77c907e2bc2b6612699789c3464ea8757c2c154621057582287a3, 1, 1, 0] 126 | assert self.s.block.get_balance(tester.a0) - balance == self.DEPOSIT_COST # deposit return 127 | 128 | def test_reveal_not_yet_allowed(self): 129 | self.test_commit() 130 | 131 | assert self.c.reveal(1, self.COW_INT) == 90 132 | 133 | def test_reveal_window_expired(self): 134 | self.test_commit() 135 | self.s.mine(5) 136 | 137 | assert self.c.reveal(1, self.COW_INT) == 91 138 | 139 | def test_reveal_not_committed(self): 140 | self.s.mine(2) 141 | 142 | assert self.c.reveal(1, self.COW_INT) == 92 143 | 144 | def test_reveal_already_revealed(self): 145 | self.test_commit() 146 | self.s.mine(2) 147 | 148 | assert self.c.reveal(1, self.COW_INT) == 1 149 | assert self.c.reveal(1, self.COW_INT) == 93 150 | 151 | def test_reveal_hash_mismatch(self): 152 | self.test_commit() 153 | self.s.mine(2) 154 | 155 | assert self.c.reveal(1, self.MONKEY_INT) == 94 156 | 157 | def test_reveal_calculates_seed_when_all_reveals_are_in(self): 158 | assert self.c.commit(1, self.COW_HASH, value=self.DEPOSIT_COST) == 1 159 | assert self.c.commit(1, self.COW_HASH, sender=tester.k1, value=self.DEPOSIT_COST) == 1 160 | self.s.mine(2) 161 | 162 | assert self.c.reveal(1, self.COW_INT) == 1 163 | assert self.c.get_block(1) == [0, 2, 1, 0] 164 | assert self.c.reveal(1, self.COW_INT, sender=tester.k1) == 1 165 | assert self.c.get_block(1) == [0x2c996eb68c74cec2b2acd81abe0f75fe67b2b941702b8dc25e96a106800eb922, 2, 2, 0] 166 | 167 | def test_reveal_returns_entropy(self): 168 | assert self.c.commit(1, self.COW_HASH, value=self.DEPOSIT_COST) == 1 169 | assert self.c.request_entropy(sender=tester.k1, value=self.ENTROPY_COST) == [0, 4] 170 | assert self.c.get_block(self.s.block.number + 1) == [0, 1, 0, 1] 171 | 172 | self.s.mine(2) 173 | 174 | COW_SEED = utils.big_endian_to_int(utils.sha3(utils.sha3(utils.zpad('cow', 32)) + utils.zpad('cow', 32))) 175 | COW_HASH_1 = utils.big_endian_to_int(utils.sha3(utils.int_to_big_endian(COW_SEED))) 176 | 177 | balance = self.s.block.get_balance(tester.a0) 178 | assert self.c.reveal(1, self.COW_INT) == 1 179 | assert self.s.block.get_balance(tester.a0) - balance == self.DEPOSIT_COST + self.ENTROPY_COST # deposit return + payout of committer share 180 | 181 | assert self.c.get_block(1) == [COW_SEED, 1, 1, 1] 182 | 183 | # signed vs unsigned as introduced by tester.send 184 | assert self.c.get_entropy_ticket(0) == [utils.big_endian_to_int(tester.a1), 1, 1, COW_HASH_1 - 2 ** 256] 185 | assert self.c.get_entropy(0, sender=tester.k1) == [1, COW_HASH_1 - 2 ** 256] # ready 186 | -------------------------------------------------------------------------------- /test/test_sleth_contract.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from ethereum import tester 3 | from eth_tools import address 4 | 5 | import random 6 | import pytest 7 | 8 | from sleth.constants import CONTRACT_FILE, CONTRACT_GAS, SPIN_GAS, CLAIM_GAS, ETHER 9 | 10 | @contextmanager 11 | def assert_max_gas_cost(block, max_gas): 12 | start_gas = block.gas_used 13 | yield 14 | gas_cost = block.gas_used - start_gas 15 | assert gas_cost <= max_gas, "Maximum gas cost exceeded. gas_cost = %d, max_gas = %d" % (gas_cost, max_gas) 16 | 17 | class TestSlethContract(object): 18 | 19 | def setup_class(cls): 20 | cls.s = tester.state() 21 | cls.c = cls.s.abi_contract(CONTRACT_FILE, endowment=2000 * ETHER, gas=CONTRACT_GAS) 22 | cls.snapshot = cls.s.snapshot() 23 | 24 | def setup_method(self, method): 25 | self.s.revert(self.snapshot) 26 | tester.seed = 3 ** 160 27 | 28 | def profile_calc_reward(self, rnd, lines): 29 | return self.s.profile(tester.k0, self.c.address, 0, funid=8, abi=[rnd, lines]) 30 | 31 | def test_init(self): 32 | assert self.c.get_stats() == [0, 0, 0] 33 | assert self.s.block.get_code(self.c.address) != '' 34 | 35 | def test_suicide(self): 36 | assert self.c.suicide() is None 37 | assert self.s.block.get_code(self.c.address) == '' 38 | 39 | def test_create_gas_used(self): 40 | print "create gas used:", self.s.block.gas_used 41 | assert self.s.block.gas_used <= CONTRACT_GAS 42 | 43 | def test_spin_bet_out_of_range(self): 44 | with assert_max_gas_cost(self.s.block, SPIN_GAS): 45 | assert self.c.spin(0) == 0 46 | 47 | with assert_max_gas_cost(self.s.block, SPIN_GAS): 48 | assert self.c.spin(6, value=6 * ETHER) == 0 49 | 50 | def test_spin_invalid_funds(self): 51 | with assert_max_gas_cost(self.s.block, SPIN_GAS): 52 | assert self.c.spin(5) == 0 53 | 54 | with assert_max_gas_cost(self.s.block, SPIN_GAS): 55 | assert self.c.spin(5, value=3 * ETHER) == 0 56 | 57 | def test_spin_valid_bet(self): 58 | with assert_max_gas_cost(self.s.block, SPIN_GAS): 59 | assert self.c.spin(5, value=5 * ETHER) == 1 60 | 61 | current_round = self.c.get_current_round() 62 | assert current_round == 1 63 | 64 | player, block, bet, result, entropy, status = self.c.get_round(current_round) 65 | assert address(player) == tester.a0.encode('hex') 66 | assert block == 0 67 | assert bet == 5 68 | assert result == 0 69 | assert entropy == 0 70 | assert status == 1 # spinning 71 | 72 | assert self.c.get_stats() == [1, 5, 0] 73 | 74 | def test_calc_line_perfect_match(self): 75 | assert self.c.calc_line(0, 0, 0) == 50 76 | assert self.c.calc_line(1, 1, 1) == 10 77 | assert self.c.calc_line(2, 2, 2) == 15 78 | assert self.c.calc_line(3, 3, 3) == 20 79 | assert self.c.calc_line(4, 4, 4) == 25 80 | assert self.c.calc_line(5, 5, 5) == 8 81 | assert self.c.calc_line(6, 6, 6) == 6 82 | assert self.c.calc_line(7, 7, 7) == 4 83 | assert self.c.calc_line(8, 8, 8) == 250 84 | assert self.c.calc_line(9, 9, 9) == 75 85 | assert self.c.calc_line(10, 10, 10) == 100 86 | 87 | def test_calc_line_triple_ups(self): 88 | assert self.c.calc_line(1, 2, 3) == 6 89 | assert self.c.calc_line(3, 2, 3) == 6 90 | assert self.c.calc_line(2, 3, 2) == 6 91 | 92 | def test_calc_line_triple_down(self): 93 | assert self.c.calc_line(5, 6, 7) == 2 94 | assert self.c.calc_line(5, 6, 6) == 2 95 | 96 | def test_calc_line_bacon(self): 97 | assert self.c.calc_line(9, 0, 0) == 50 98 | assert self.c.calc_line(0, 9, 0) == 50 99 | assert self.c.calc_line(0, 0, 9) == 50 100 | 101 | def test_calc_line_bacon_trip_ups(self): 102 | assert self.c.calc_line(9, 2, 3) == 6 103 | assert self.c.calc_line(1, 9, 3) == 6 104 | assert self.c.calc_line(1, 2, 9) == 6 105 | 106 | def test_calc_line_bacon_trip_down(self): 107 | assert self.c.calc_line(9, 6, 7) == 2 108 | assert self.c.calc_line(5, 9, 7) == 2 109 | assert self.c.calc_line(5, 6, 9) == 2 110 | 111 | def test_calc_line_double_bacon(self): 112 | assert self.c.calc_line(9, 9, 0) == 50 113 | assert self.c.calc_line(0, 9, 9) == 50 114 | assert self.c.calc_line(9, 0, 9) == 50 115 | 116 | def test_calc_line_nothing(self): 117 | assert self.c.calc_line(0, 1, 2) == 0 118 | 119 | def test_get_stops(self): 120 | assert self.c.get_stops(23888) == [16, 10, 23] 121 | assert self.c.get_stops(1606) == [6, 18, 1] 122 | assert self.c.get_stops(30464) == [0, 24, 29] 123 | assert self.c.get_stops(0) == [0, 0, 0] 124 | 125 | def test_calc_reward(self): 126 | assert self.c.calc_reward(23888, 1) == 6 127 | assert self.c.calc_reward(23888, 3) == 26 128 | assert self.c.calc_reward(23888, 5) == 32 129 | 130 | def test_calc_reward_2878(self): 131 | # wheel stops should wrap around 132 | assert self.c.get_stops(2878) == [30, 25, 2] 133 | assert self.c.calc_reward(2878, 5) == 6 134 | 135 | def _spin_mine_claim(self, amount, premine, expected_result): 136 | self.s.mine(premine) 137 | 138 | with assert_max_gas_cost(self.s.block, SPIN_GAS): 139 | assert self.c.spin(amount, value=amount * ETHER) == 1 140 | 141 | self.s.mine(1) 142 | balance_before = self.s.block.get_balance(tester.a0) 143 | 144 | with assert_max_gas_cost(self.s.block, CLAIM_GAS): 145 | assert self.c.claim(1) == 1 146 | 147 | player, block, bet, result, entropy, status = self.c.get_round(1) 148 | assert address(player) == tester.a0.encode('hex') 149 | assert block == premine 150 | assert bet == amount 151 | assert entropy != 0 152 | assert status == 2 # done 153 | assert result == expected_result 154 | 155 | balance_after = self.s.block.get_balance(tester.a0) 156 | assert balance_after - balance_before == expected_result * ETHER 157 | 158 | current_round = self.c.get_current_round() 159 | assert current_round == 1 160 | 161 | assert self.c.get_stats() == [1, amount, expected_result] 162 | 163 | def test_claim_winning(self): 164 | self._spin_mine_claim(amount=5, premine=1, expected_result=4) 165 | 166 | def test_claim_losing(self): 167 | self._spin_mine_claim(amount=5, premine=0, expected_result=0) 168 | 169 | def test_claim_invalid_status(self): 170 | with assert_max_gas_cost(self.s.block, CLAIM_GAS): 171 | assert self.c.claim(1) == 90 172 | 173 | def test_claim_invalid_round(self): 174 | with assert_max_gas_cost(self.s.block, SPIN_GAS): 175 | assert self.c.spin(5, value=5 * ETHER) == 1 176 | 177 | with assert_max_gas_cost(self.s.block, CLAIM_GAS): 178 | assert self.c.claim(1, sender=tester.k1) == 91 179 | 180 | def test_claim_not_yet_ready(self): 181 | with assert_max_gas_cost(self.s.block, SPIN_GAS): 182 | assert self.c.spin(5, value=5 * ETHER) == 1 183 | 184 | with assert_max_gas_cost(self.s.block, CLAIM_GAS): 185 | assert self.c.claim(1) == 92 186 | 187 | def test_claim_block_number_out_of_range(self): 188 | with assert_max_gas_cost(self.s.block, SPIN_GAS): 189 | assert self.c.spin(5, value=5 * ETHER) == 1 190 | 191 | self.s.mine(256) 192 | with assert_max_gas_cost(self.s.block, CLAIM_GAS): 193 | assert self.c.claim(1) == 93 194 | 195 | def test_claim_has_unique_entropy(self): 196 | self.s.mine(1) 197 | 198 | with assert_max_gas_cost(self.s.block, SPIN_GAS): 199 | assert self.c.spin(5, value=5 * ETHER) == 1 200 | 201 | with assert_max_gas_cost(self.s.block, SPIN_GAS): 202 | assert self.c.spin(5, sender=tester.k1, value=5 * ETHER) == 1 203 | 204 | current_round = self.c.get_current_round() 205 | assert current_round == 1 206 | 207 | current_round = self.c.get_current_round(sender=tester.k1) 208 | assert current_round == 2 209 | 210 | self.s.mine(1) 211 | 212 | with assert_max_gas_cost(self.s.block, CLAIM_GAS): 213 | assert self.c.claim(1) == 1 214 | 215 | with assert_max_gas_cost(self.s.block, CLAIM_GAS): 216 | assert self.c.claim(2, sender=tester.k1) == 1 217 | 218 | player1, block1, bet1, result1, entropy1, status1 = self.c.get_round(1) 219 | player2, block2, bet2, result2, entropy2, status2 = self.c.get_round(2) 220 | assert address(player1) == tester.a0.encode('hex') 221 | assert address(player2) == tester.a1.encode('hex') 222 | assert entropy1 != entropy2 223 | 224 | @pytest.mark.slow 225 | def test_calc_reward_loop(self): 226 | random.seed(0) 227 | bet = 5 228 | times = 1000 229 | 230 | total_cost = 0 231 | total_payout = 0 232 | total_gas = 0 233 | total_time = 0 234 | 235 | for _ in range(0, times): 236 | total_cost += bet 237 | rnd = random.randint(0, 32 ** 3) 238 | 239 | result = self.profile_calc_reward(rnd, bet) 240 | total_gas += result['gas'] 241 | total_time += result['time'] 242 | total_payout += result['output'][0] 243 | 244 | print total_payout, total_cost, float(total_payout) / total_cost, total_gas / times, total_time / times 245 | assert total_payout < total_cost 246 | -------------------------------------------------------------------------------- /frontend/js/game.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Copyright (c) 2015 Joris Bontje 4 | Copyright (c) 2012 Clint Bellanger 5 | 6 | MIT License: 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | 14 | 15 | Sounds by Brandon Morris (CC-BY 3.0) 16 | Art by Clint Bellanger (CC-BY 3.0) 17 | 18 | */ 19 | 20 | "use strict"; 21 | 22 | var app = angular.module('slots.game', []); 23 | 24 | app.factory('game', ['$rootScope', 'config', function($rootScope, config) { 25 | var symbol_count = 11; 26 | var match_payout = []; 27 | match_payout[7] = 4; // 3Down 28 | match_payout[6] = 6; // 2Down 29 | match_payout[5] = 8; // 1Down 30 | match_payout[1] = 10; // 1Up 31 | match_payout[2] = 15; // 2Up 32 | match_payout[3] = 20; // 3Up 33 | match_payout[4] = 25; // OrangeRed 34 | match_payout[0] = 50; // AlienHead 35 | match_payout[9] = 75; // Bacon 36 | match_payout[10] = 100; // Narwhal 37 | match_payout[8] = 250; // CakeDay 38 | 39 | var payout_ups = 6; // Any 3 Ups 40 | var payout_downs = 2; // Any 3 Downs 41 | 42 | var game = {}; 43 | 44 | 45 | game.STATE_NEW = 0; 46 | game.STATE_REST = 1; 47 | game.STATE_SPINUP = 2; 48 | game.STATE_SPINMAX = 3; 49 | game.STATE_SPINDOWN = 4; 50 | 51 | game.state = game.STATE_NEW; 52 | 53 | game.reward = { 54 | payout: 0, 55 | partial_payouts: {}, 56 | highlights: [] 57 | }; 58 | 59 | // set up reels 60 | game.reels = []; 61 | game.reels[0] = [2,1,7,1,2,7,6,7,3,10,1,6,1,7,3,4,3,2,4,5,0,6,10,5,6,5,8,3,0,9,5,4]; 62 | game.reels[1] = [6,0,10,3,6,7,9,2,5,2,3,1,5,2,1,10,4,5,8,4,7,6,0,1,7,6,3,1,5,9,7,4]; 63 | game.reels[2] = [1,4,2,7,5,6,4,10,7,5,2,0,6,4,10,1,7,6,3,0,5,7,2,3,9,3,5,6,1,8,1,3]; 64 | 65 | // config 66 | game.reel_position = []; 67 | 68 | var stopping_position = []; 69 | var start_slowing = []; 70 | var reel_speed = []; // reel spin speed in pixels per frame 71 | var result = []; 72 | 73 | for (var i=0; i 0) { 149 | reward.partial_payouts[line] = partial_payout; 150 | reward.payout += partial_payout; 151 | reward.highlights.push(line); 152 | } 153 | }; 154 | 155 | // calculate the reward 156 | game.calc_reward = function(playing_lines, result) { 157 | var reward = { 158 | payout: 0, 159 | partial_payouts: {}, 160 | highlights: [] 161 | }; 162 | 163 | // Line 1 164 | game.calc_reward_line(reward, 1, result[0][1], result[1][1], result[2][1]); 165 | 166 | if (playing_lines > 1) { 167 | // Line 2 168 | game.calc_reward_line(reward, 2, result[0][0], result[1][0], result[2][0]); 169 | // Line 3 170 | game.calc_reward_line(reward, 3, result[0][2], result[1][2], result[2][2]); 171 | } 172 | 173 | if (playing_lines > 3) { 174 | // Line 4 175 | game.calc_reward_line(reward, 4, result[0][0], result[1][1], result[2][2]); 176 | // Line 5 177 | game.calc_reward_line(reward, 5, result[0][2], result[1][1], result[2][0]); 178 | } 179 | 180 | return reward; 181 | }; 182 | 183 | game.spin = function(line_choice) { 184 | game.playing_lines = line_choice; 185 | game.reward.payout = 0; 186 | game.reward.partial_payouts = {}; 187 | game.state = game.STATE_SPINUP; 188 | $rootScope.$broadcast('slots:state', game.state); 189 | }; 190 | 191 | game.set_stops = function(entropy) { 192 | var rnd = entropy; 193 | 194 | for (var i=0; i= config.reel_pixel_length) stopping_position[i] -= config.reel_pixel_length; 205 | 206 | // convenient here to remember the winning positions 207 | for (var j=0; j= config.reel_positions) result[i][j] -= config.reel_positions; 210 | 211 | // translate reel positions into symbol 212 | result[i][j] = game.reels[i][result[i][j]]; 213 | } 214 | } 215 | 216 | game.state = game.STATE_SPINDOWN; 217 | $rootScope.$broadcast('slots:state', game.state); 218 | }; 219 | 220 | game.get_result = function() { 221 | return result; 222 | }; 223 | 224 | game.reinit = function(line_choice) { 225 | for (var i=0; i= config.max_reel_speed) { 259 | game.state = game.STATE_SPINMAX; 260 | $rootScope.$broadcast('slots:state', game.state); 261 | } 262 | } 263 | 264 | function logic_spinmax() { 265 | for (var i=0; i 0) { 307 | reel_speed[i] -= config.spindown_acceleration; 308 | 309 | // XXX sounds 310 | /* 311 | if (reel_speed[i] == 0) { 312 | sounds.playReelStop(i); 313 | }*/ 314 | } 315 | } 316 | } 317 | } 318 | 319 | // update all logic in the current frame 320 | game.logic = function() { 321 | 322 | // SPINMAX TO SPINDOWN happens on an input event 323 | // NEW or REST to SPINUP happens on an input event 324 | 325 | if (game.state === game.STATE_SPINUP) { 326 | logic_spinup(); 327 | } 328 | else if (game.state === game.STATE_SPINMAX) { 329 | logic_spinmax(); 330 | } 331 | else if (game.state === game.STATE_SPINDOWN) { 332 | logic_spindown(); 333 | } 334 | }; 335 | 336 | return game; 337 | }]); 338 | -------------------------------------------------------------------------------- /frontend/js/sleth.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Copyright (c) 2015 Joris Bontje 4 | Copyright (c) 2012 Clint Bellanger 5 | 6 | MIT License: 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | 14 | 15 | Sounds by Brandon Morris (CC-BY 3.0) 16 | Art by Clint Bellanger (CC-BY 3.0) 17 | 18 | */ 19 | 20 | "use strict"; 21 | 22 | var app = angular.module('slethController', ['slots.config', 'slots.game', 'slots.reels', 'slots.directives', 'ngAnimate', 'angular-lodash', 'angularMoment']); 23 | 24 | app.factory('moment', function() { 25 | var moment = window.moment; 26 | window.moment = undefined; 27 | return moment; 28 | }); 29 | 30 | app.factory('web3', function() { 31 | var web3 = require('web3'); 32 | web3.setProvider(new web3.providers.HttpProvider("http://localhost:8545/")); 33 | return web3; 34 | }); 35 | 36 | app.controller("SlethController", ['$http', '$interval', '$log', '$q', '$routeParams', '$scope', '$timeout', 'config', 'game', 'moment', 'web3', function($http, $interval, $log, $q, $routeParams, $scope, $timeout, config, game, moment, web3) { 37 | 38 | var ROUND_NEW = 0; 39 | var ROUND_SPINNING = 1; 40 | var ROUND_DONE = 2; 41 | var ROUND_EXPIRED = 2; 42 | 43 | var MAX_BLOCK_AGE = 255; 44 | 45 | /* global BigNumber:false */ 46 | var two_256 = new BigNumber(2).toPower(256); 47 | 48 | $scope.canvasSize = 160 * config.reel_scale; 49 | 50 | $scope.slethAddress = $routeParams.contractAddress; 51 | $scope.defaultGas = web3.fromDecimal(200000); 52 | $scope.contract = $q.defer(); 53 | 54 | $scope.bet = 0; 55 | $scope.lastClaimed = 0; 56 | $scope.player = {}; 57 | $scope.stats = {}; 58 | $scope.round = {}; 59 | $scope.messages = []; 60 | $scope.web3 = {}; 61 | $scope.state = game.STATE_NEW; 62 | 63 | $scope.maxPayout = 250; 64 | 65 | $scope.rounds = {}; 66 | 67 | $interval(function() { 68 | game.logic(); 69 | }, 1000 / config.FPS); 70 | 71 | $http.get('sleth.abi.json').then(function(res) { 72 | var Contract = web3.eth.contract(res.data); 73 | $scope.contract.resolve(new Contract($scope.slethAddress)); 74 | }); 75 | 76 | $scope.updateChain = function() { 77 | var accounts = web3.eth.accounts; 78 | $scope.player.address = accounts[0]; 79 | 80 | var playerBalance = web3.eth.getBalance(accounts[0]); 81 | $scope.player.balance = playerBalance.toNumber() / Math.pow(10, 18) || 0; 82 | $scope.player.coins = Math.floor($scope.player.balance); 83 | 84 | var slethBalance = web3.eth.getBalance($scope.slethAddress); 85 | $scope.stats.slethBalance = slethBalance.toNumber() / Math.pow(10, 18) || 0; 86 | $scope.stats.slethAddress = $scope.slethAddress; 87 | 88 | $scope.web3.blockNumber = web3.eth.blockNumber; 89 | }; 90 | 91 | $scope.updateStats = function() { 92 | $scope.contract.promise.then(function(contract) { 93 | var res = contract.call({from: $scope.player.address}).get_stats(); 94 | if (res.length) { 95 | $scope.stats.total_spins = res[0].toNumber(); 96 | $scope.stats.total_coins_bet = res[1].toNumber(); 97 | $scope.stats.total_coins_won = res[2].toNumber(); 98 | } else { 99 | $log.warn("get_stats: Empty response"); 100 | } 101 | }); 102 | }; 103 | 104 | $scope.getCurrentRound = function(contract) { 105 | var res = contract.call({from: $scope.player.address}).get_current_round(); 106 | if (res) { 107 | return res.toNumber(); 108 | } 109 | }; 110 | 111 | $scope.getRound = function(contract, roundNumber) { 112 | var res = contract.call({from: $scope.player.address}).get_round(roundNumber); 113 | if (res.length) { 114 | var player = res[0].isNeg() ? res[0].plus(two_256) : res[0]; 115 | var entropy = res[4].isNeg() ? res[4].plus(two_256) : res[4]; 116 | var round = { 117 | number: roundNumber, 118 | player: '0x' + player.toString(16), 119 | block: res[1].toNumber(), 120 | bet: res[2].toNumber(), 121 | result: res[3].toNumber(), 122 | entropy: '0x' + entropy.toString(16), 123 | rnd: entropy.modulo(32768).toNumber(), 124 | status: res[5].toNumber() 125 | }; 126 | return round; 127 | } 128 | }; 129 | 130 | $scope.updateRound = function() { 131 | $scope.contract.promise.then(function(contract) { 132 | var roundNumber = $scope.getCurrentRound(contract); 133 | if (!roundNumber) { 134 | $log.warn("get_current_round: Empty response"); 135 | return; 136 | } 137 | $scope.player.round = roundNumber; 138 | 139 | var round = $scope.getRound(contract, roundNumber); 140 | if (!round) { 141 | $log.warn("get_round: Empty response"); 142 | return; 143 | } 144 | 145 | if (round.status === ROUND_SPINNING && ($scope.web3.blockNumber > round.block + MAX_BLOCK_AGE)) { 146 | round.status = ROUND_EXPIRED; 147 | } 148 | 149 | var changed = !angular.equals(round, $scope.round); 150 | $scope.round = round; 151 | 152 | if (changed) { 153 | if (round.status === ROUND_SPINNING && (game.state === game.STATE_NEW)) { 154 | $scope.bet = round.bet; 155 | game.spin(round.bet); 156 | } else if (round.status === ROUND_DONE && (game.state !== game.STATE_NEW)) { 157 | $scope.bet = 0; 158 | game.set_stops(round.rnd); 159 | var message = "Results for round #" + roundNumber + ": you won "; 160 | if (round.result) { 161 | message += round.result + " coins :)"; 162 | } else { 163 | message += "nothing :("; 164 | } 165 | $scope.logMessage(message); 166 | $scope.rounds[roundNumber] = round; 167 | } 168 | } 169 | }); 170 | }; 171 | 172 | $scope.spin = function(bet) { 173 | if (bet) { 174 | if (game.state !== game.STATE_NEW && game.state !== game.STATE_REST) return; 175 | if ($scope.player.coins < bet) return; 176 | 177 | $scope.clearMessages(); 178 | 179 | var value = web3.fromDecimal(bet * Math.pow(10, 18)); 180 | $scope.contract.promise.then(function(contract) { 181 | contract.sendTransaction({from: $scope.player.address, gas: $scope.defaultGas, value: value}).spin(bet); 182 | 183 | $scope.bet = bet; 184 | 185 | game.spin(bet); 186 | $scope.logMessage("Spinning... " + bet); 187 | $scope.round = {}; 188 | }); 189 | } 190 | }; 191 | 192 | $scope.canClaim = function(round) { 193 | return round.status === ROUND_SPINNING && ($scope.web3.blockNumber >= round.block) && ($scope.web3.blockNumber <= round.block + MAX_BLOCK_AGE) && (round.number > $scope.lastClaimed); 194 | }; 195 | 196 | $scope.$watchGroup(['round', 'web3'], function(newValues, oldValues, scope) { 197 | if ($scope.canClaim($scope.round)) { 198 | $scope.claim($scope.round); 199 | } 200 | }); 201 | 202 | $scope.claim = function(round) { 203 | if (round.number) { 204 | $scope.contract.promise.then(function(contract) { 205 | contract.sendTransaction({from: $scope.player.address, gas: $scope.defaultGas}).claim(round.number); 206 | 207 | $scope.logMessage("Claiming round #" + round.number + "..."); 208 | $scope.lastClaimed = round.number; 209 | }); 210 | } 211 | }; 212 | 213 | $scope.spinBest = function() { 214 | if (game.state !== game.STATE_NEW && game.state !== game.STATE_REST) return; 215 | 216 | if ($scope.player.coins >= 5) { 217 | $scope.spin(5); 218 | } else if ($scope.player.coins >= 3) { 219 | $scope.spin(3); 220 | } else if ($scope.player.coins >= 1) { 221 | $scope.spin(1); 222 | } else if ($scope.autoplay) { 223 | $scope.logMessage("Out of funds, disabling autoplay"); 224 | $scope.autoplay = false; 225 | } 226 | }; 227 | 228 | $scope.autoplaySpin = function() { 229 | if ($scope.autoplay) { 230 | $scope.spinBest(); 231 | } 232 | }; 233 | 234 | $scope.$on('slots:reward', function(evt, reward) { 235 | $scope.reward = reward; 236 | // check if the locally calculated reward matches with the contract results 237 | $scope.reward.verified = (reward.payout === $scope.round.result); 238 | if ($scope.reward.verified) { 239 | $scope.logMessage("Reward verified"); 240 | } else { 241 | $scope.logMessage("Reward NOT verified"); 242 | } 243 | 244 | if ($scope.autoplay) { 245 | $scope.logMessage("Autoplay enabled, playing next round in " + config.autoplay_delay / 1000 + " seconds..."); 246 | $timeout($scope.autoplaySpin, config.autoplay_delay); 247 | } 248 | }); 249 | 250 | $scope.$on('keypress', function (evt, obj) { 251 | if (obj.which === 32) { // spacebar 252 | $scope.spinBest(); 253 | } 254 | }); 255 | 256 | $scope.clearMessages = function() { 257 | $scope.messages = []; 258 | }; 259 | 260 | $scope.logMessage = function(message) { 261 | $log.info(message); 262 | $scope.messages.push(message); 263 | }; 264 | 265 | // test if web3 is available 266 | try { 267 | $scope.web3.available = (web3.eth.coinbase !== ""); 268 | $scope.contractExists = (web3.eth.getCode($scope.slethAddress) !== "0x0000000000000000000000000000000000000000000000000000000000000000"); 269 | } catch(e) { 270 | $log.error(e); 271 | $scope.web3.error = e; 272 | } 273 | 274 | $scope.$on('slots:state', function(evt, state) { 275 | $scope.state = state; 276 | }); 277 | 278 | function init() { 279 | if (!$scope.contractExists) { 280 | $scope.web3.error = {'name': "Contract Not Found", 'message': "The specified contract couldn't be found on the blockchain"}; 281 | return; 282 | } 283 | 284 | $scope.$watch('player.round', $scope.updateRound); 285 | 286 | web3.eth.filter('latest').watch(function(res) { 287 | $log.debug('filter:latest'); 288 | $scope.updateChain(); 289 | $scope.updateRound(); 290 | $scope.updateStats(); 291 | }); 292 | 293 | // force initial load 294 | $scope.updateChain(); 295 | $scope.updateRound(); 296 | $scope.updateStats(); 297 | 298 | $scope.contract.promise.then(function(contract) { 299 | web3.eth.filter({'address': $scope.slethAddress, 'limit': 10}).watch(function(res) { 300 | if (!res || res.address !== $scope.slethAddress) { 301 | $log.warn("watch: invalid result", res); 302 | return; 303 | } 304 | var roundNumber = web3.toDecimal(res.data); 305 | $log.debug("filter.watch", roundNumber); 306 | if (roundNumber > 0 && !(roundNumber in $scope.rounds)) { 307 | var round = $scope.getRound(contract, roundNumber); 308 | if (round) { 309 | $scope.rounds[roundNumber] = round; 310 | } else { 311 | $log.warn("watch: empty response received for round", roundNumber); 312 | } 313 | } 314 | }); 315 | }); 316 | } 317 | 318 | if ($scope.web3.available) { 319 | init(); 320 | } 321 | }]); 322 | -------------------------------------------------------------------------------- /frontend/js/lib/web3.min.js: -------------------------------------------------------------------------------- 1 | require=function t(e,n,r){function i(s,a){if(!n[s]){if(!e[s]){var u="function"==typeof require&&require;if(!a&&u)return u(s,!0);if(o)return o(s,!0);var c=new Error("Cannot find module '"+s+"'");throw c.code="MODULE_NOT_FOUND",c}var l=n[s]={exports:{}};e[s][0].call(l.exports,function(t){var n=e[s][1][t];return i(n?n:t)},l,l.exports,t,e,n,r)}return n[s].exports}for(var o="function"==typeof require&&require,s=0;sv;v++)g.push(h(e.slice(0,s))),e=e.slice(s);n.push(g)}else i.prefixedType("bytes")(t[c].type)?(l=l.slice(s),n.push(h(e.slice(0,s))),e=e.slice(s)):(n.push(h(e.slice(0,s))),e=e.slice(s))}),n},d=function(t){var e={};return t.forEach(function(t){var r=n.extractDisplayName(t.name),i=n.extractTypeName(t.name),o=function(){var e=Array.prototype.slice.call(arguments);return f(t.inputs,e)};void 0===e[r]&&(e[r]=o),e[r][i]=o}),e},g=function(t){var e={};return t.forEach(function(t){var r=n.extractDisplayName(t.name),i=n.extractTypeName(t.name),o=function(e){return h(t.outputs,e)};void 0===e[r]&&(e[r]=o),e[r][i]=o}),e},v=function(t,e){var n=s.getConstructor(t,e.length);return n?f(n.inputs,e):(e.length>0&&console.warn("didn't found matching constructor, using default one"),"")};e.exports={inputParser:d,outputParser:g,formatInput:f,formatOutput:h,formatConstructorParams:v}},{"../utils/config":7,"../utils/utils":8,"./formatters":2,"./types":3,"./utils":4}],2:[function(t,e){var n=t("bignumber.js"),r=t("../utils/utils"),i=t("../utils/config"),o=function(t){var e=2*i.ETH_PADDING;return n.config(i.ETH_BIGNUMBER_ROUNDING_MODE),r.padLeft(r.toTwosComplement(t).round().toString(16),e)},s=function(t){return r.fromAscii(t,i.ETH_PADDING).substr(2)},a=function(t){return"000000000000000000000000000000000000000000000000000000000000000"+(t?"1":"0")},u=function(t){return o(new n(t).times(new n(2).pow(128)))},c=function(t){return"1"===new n(t.substr(0,1),16).toString(2).substr(0,1)},l=function(t){return t=t||"0",c(t)?new n(t,16).minus(new n("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",16)).minus(1):new n(t,16)},f=function(t){return t=t||"0",new n(t,16)},p=function(t){return l(t).dividedBy(new n(2).pow(128))},m=function(t){return f(t).dividedBy(new n(2).pow(128))},h=function(t){return"0x"+t},d=function(t){return"0000000000000000000000000000000000000000000000000000000000000001"===t?!0:!1},g=function(t){return r.toAscii(t)},v=function(t){return"0x"+t.slice(t.length-40,t.length)};e.exports={formatInputInt:o,formatInputString:s,formatInputBool:a,formatInputReal:u,formatOutputInt:l,formatOutputUInt:f,formatOutputReal:p,formatOutputUReal:m,formatOutputHash:h,formatOutputBool:d,formatOutputString:g,formatOutputAddress:v}},{"../utils/config":7,"../utils/utils":8,"bignumber.js":"bignumber.js"}],3:[function(t,e){var n=t("./formatters"),r=function(t){return function(e){return 0===e.indexOf(t)}},i=function(t){return function(e){return t===e}},o=function(){return[{type:r("uint"),format:n.formatInputInt},{type:r("int"),format:n.formatInputInt},{type:r("bytes"),format:n.formatInputString},{type:r("real"),format:n.formatInputReal},{type:r("ureal"),format:n.formatInputReal},{type:i("address"),format:n.formatInputInt},{type:i("bool"),format:n.formatInputBool}]},s=function(){return[{type:r("uint"),format:n.formatOutputUInt},{type:r("int"),format:n.formatOutputInt},{type:r("bytes"),format:n.formatOutputString},{type:r("real"),format:n.formatOutputReal},{type:r("ureal"),format:n.formatOutputUReal},{type:i("address"),format:n.formatOutputAddress},{type:i("bool"),format:n.formatOutputBool}]};e.exports={prefixedType:r,namedType:i,inputTypes:o,outputTypes:s}},{"./formatters":2}],4:[function(t,e){var n=function(t,e){return t.filter(function(t){return"constructor"===t.type&&t.inputs.length===e})[0]},r=function(t){return t.filter(function(t){return"function"===t.type})},i=function(t){return t.filter(function(t){return"event"===t.type})};e.exports={getConstructor:n,filterFunctions:r,filterEvents:i}},{}],5:[function(t,e){"use strict";e.exports=BigNumber},{}],6:[function(t,e,n){"use strict";n.XMLHttpRequest="undefined"==typeof XMLHttpRequest?{}:XMLHttpRequest},{}],7:[function(t,e){var n=t("bignumber.js"),r=["wei","Kwei","Mwei","Gwei","szabo","finney","ether","grand","Mether","Gether","Tether","Pether","Eether","Zether","Yether","Nether","Dether","Vether","Uether"];e.exports={ETH_PADDING:32,ETH_SIGNATURE_LENGTH:4,ETH_UNITS:r,ETH_BIGNUMBER_ROUNDING_MODE:{ROUNDING_MODE:n.ROUND_DOWN},ETH_POLLING_TIMEOUT:1e3,ETH_DEFAULTBLOCK:"latest"}},{"bignumber.js":"bignumber.js"}],8:[function(t,e){var n=t("bignumber.js"),r={wei:"1",kwei:"1000",ada:"1000",mwei:"1000000",babbage:"1000000",gwei:"1000000000",shannon:"1000000000",szabo:"1000000000000",finney:"1000000000000000",ether:"1000000000000000000",kether:"1000000000000000000000",grand:"1000000000000000000000",einstein:"1000000000000000000000",mether:"1000000000000000000000000",gether:"1000000000000000000000000000",tether:"1000000000000000000000000000000"},i=function(t,e,n){return new Array(e-t.length+1).join(n?n:"0")+t},o=function(t,e){for(var n=!1,r=0;rn;n+=2){var i=parseInt(t.substr(n,2),16);if(0===i)break;e+=String.fromCharCode(i)}return e},a=function(t){for(var e="",n=0;n1?(t[n[0]]=t[n[0]]||{},t[n[0]][n[1]]=e):t[n[0]]=e},o.prototype.toPayload=function(t){var e=this.getCall(t),n=this.extractCallback(t),r=this.formatInput(t);return this.validateArgs(r),{method:e,params:r,callback:n}},o.prototype.send=function(){var t=this.toPayload(Array.prototype.slice.call(arguments));if(t.callback){var e=this;return n.getInstance().sendAsync(t,function(n,r){t.callback(null,e.formatOutput(r))})}return this.formatOutput(n.getInstance().send(t))},e.exports=o},{"../utils/utils":8,"./errors":13,"./requestmanager":24}],21:[function(t,e){var n=t("../utils/utils"),r=t("./property"),i=[],o=[new r({name:"listening",getter:"net_listening"}),new r({name:"peerCount",getter:"net_peerCount",outputFormatter:n.toDecimal})];e.exports={methods:i,properties:o}},{"../utils/utils":8,"./property":22}],22:[function(t,e){var n=t("./requestmanager"),r=function(t){this.name=t.name,this.getter=t.getter,this.setter=t.setter,this.outputFormatter=t.outputFormatter,this.inputFormatter=t.inputFormatter};r.prototype.formatInput=function(t){return this.inputFormatter?this.inputFormatter(t):t},r.prototype.formatOutput=function(t){return this.outputFormatter&&null!==t?this.outputFormatter(t):t},r.prototype.attachToObject=function(t){var e={get:this.get.bind(this),set:this.set.bind(this)},n=this.name.split(".");n.length>1?(t[n[0]]=t[n[0]]||{},Object.defineProperty(t[n[0]],n[1],e)):Object.defineProperty(t,n[0],e)},r.prototype.get=function(){return this.formatOutput(n.getInstance().send({method:this.getter}))},r.prototype.set=function(t){return n.getInstance().send({method:this.setter,params:[this.formatInput(t)]})},e.exports=r},{"./requestmanager":24}],23:[function(t,e){var n=function(){};n.prototype.send=function(t){var e=navigator.qt.callMethod(JSON.stringify(t));return JSON.parse(e)},e.exports=n},{}],24:[function(t,e){var n=t("./jsonrpc"),r=t("../utils/utils"),i=t("../utils/config"),o=t("./errors"),s=function(t){return arguments.callee._singletonInstance?arguments.callee._singletonInstance:(arguments.callee._singletonInstance=this,this.provider=t,this.polls=[],this.timeout=null,void this.poll())};s.getInstance=function(){var t=new s;return t},s.prototype.send=function(t){if(!this.provider)return console.error(o.InvalidProvider()),null;var e=n.getInstance().toPayload(t.method,t.params),r=this.provider.send(e);if(!n.getInstance().isValidResponse(r))throw o.InvalidResponse(r);return r.result},s.prototype.sendAsync=function(t,e){if(!this.provider)return e(o.InvalidProvider());var r=n.getInstance().toPayload(t.method,t.params);this.provider.sendAsync(r,function(t,r){return t?e(t):n.getInstance().isValidResponse(r)?void e(null,r.result):e(o.InvalidResponse(r))})},s.prototype.setProvider=function(t){this.provider=t},s.prototype.startPolling=function(t,e,n,r){this.polls.push({data:t,id:e,callback:n,uninstall:r})},s.prototype.stopPolling=function(t){for(var e=this.polls.length;e--;){var n=this.polls[e];n.id===t&&this.polls.splice(e,1)}},s.prototype.reset=function(){this.polls.forEach(function(t){t.uninstall(t.id)}),this.polls=[],this.timeout&&(clearTimeout(this.timeout),this.timeout=null),this.poll()},s.prototype.poll=function(){if(this.timeout=setTimeout(this.poll.bind(this),i.ETH_POLLING_TIMEOUT),this.polls.length){if(!this.provider)return void console.error(o.InvalidProvider());var t=n.getInstance().toBatchPayload(this.polls.map(function(t){return t.data})),e=this;this.provider.sendAsync(t,function(t,i){if(!t){if(!r.isArray(i))throw o.InvalidResponse(i);i.map(function(t,n){return t.callback=e.polls[n].callback,t}).filter(function(t){var e=n.getInstance().isValidResponse(t);return e||t.callback(o.InvalidResponse(t)),e}).filter(function(t){return r.isArray(t.result)&&t.result.length>0}).forEach(function(t){t.callback(null,t.result)})}})}},e.exports=s},{"../utils/config":7,"../utils/utils":8,"./errors":13,"./jsonrpc":19}],25:[function(t,e){var n=t("./method"),r=t("./formatters"),i=new n({name:"post",call:"shh_post",params:1,inputFormatter:[r.inputPostFormatter]}),o=new n({name:"newIdentity",call:"shh_newIdentity",params:0}),s=new n({name:"hasIdentity",call:"shh_hasIdentity",params:1}),a=new n({name:"newGroup",call:"shh_newGroup",params:0}),u=new n({name:"addToGroup",call:"shh_addToGroup",params:0}),c=[i,o,s,a,u];e.exports={methods:c}},{"./formatters":17,"./method":20}],26:[function(t,e){var n=t("../web3"),r=t("../utils/config"),i=function(t){return n.sha3(n.fromAscii(t)).slice(0,2+2*r.ETH_SIGNATURE_LENGTH)},o=function(t){return n.sha3(n.fromAscii(t))};e.exports={functionSignatureFromAscii:i,eventSignatureFromAscii:o}},{"../utils/config":7,"../web3":10}],27:[function(t,e){var n=t("./method"),r=function(){var t=function(t){return"string"==typeof t[0]?"eth_newBlockFilter":"eth_newFilter"},e=new n({name:"newFilter",call:t,params:1}),r=new n({name:"uninstallFilter",call:"eth_uninstallFilter",params:1}),i=new n({name:"getLogs",call:"eth_getFilterLogs",params:1}),o=new n({name:"poll",call:"eth_getFilterChanges",params:1});return[e,r,i,o]},i=function(){var t=new n({name:"newFilter",call:"shh_newFilter",params:1}),e=new n({name:"uninstallFilter",call:"shh_uninstallFilter",params:1}),r=new n({name:"getLogs",call:"shh_getMessages",params:1}),i=new n({name:"poll",call:"shh_getFilterChanges",params:1});return[t,e,r,i]};e.exports={eth:r,shh:i}},{"./method":20}],"bignumber.js":[function(t,e){!function(t){"use strict";function n(t,e){var r,i,o,a,f,p,d=this;if(!(d instanceof n))return new n(t,e);if(t instanceof n){if(null==e)return F=0,d.s=t.s,d.e=t.e,void(d.c=(t=t.c)?t.slice():t);t+=""}else if(a="number"==(f=typeof t)){if(null==e&&t===~~t){for(d.s=0>1/t?(t=-t,-1):1,i=F=0,o=t;o>=10;o/=10,i++);return d.e=i,void(d.c=[t])}t=0===t&&0>1/t?"-0":t+""}else"string"!=f&&(t+="");if(f=t,null==e&&O.test(f))d.s=45===f.charCodeAt(0)?(f=f.slice(1),-1):1;else{if(10==e)return d=new n(f),c(d,m+d.e+1,h);if(f=T.call(f).replace(/^\+(?!-)/,""),d.s=45===f.charCodeAt(0)?(f=f.replace(/^-(?!-)/,""),-1):1,null!=e?e!=~~e&&y||(l=!(e>=2&&65>e))?(u(e,2),p=O.test(f)):(r="["+N.slice(0,e=0|e)+"]+",f=f.replace(/\.$/,"").replace(/^\./,"0."),(p=new RegExp("^"+r+"(?:\\."+r+")?$",37>e?"i":"").test(f))?(a&&(f.replace(/^0\.0*|\./,"").length>15&&u(t,0),a=!a),f=s(f,10,e,d.s)):"Infinity"!=f&&"NaN"!=f&&(u(t,1,e),t="NaN")):p=O.test(f),!p)return d.c=d.e=null,"Infinity"!=f&&("NaN"!=f&&u(t,3),d.s=null),void(F=0)}for((i=f.indexOf("."))>-1&&(f=f.replace(".","")),(o=f.search(/e/i))>0?(0>i&&(i=o),i+=+f.slice(o+1),f=f.substring(0,o)):0>i&&(i=f.length),o=0;48===f.charCodeAt(o);o++);for(e=f.length;48===f.charCodeAt(--e););if(f=f.slice(o,e+1))if(e=f.length,a&&e>15&&u(t,0),i=i-o-1,i>b)d.c=d.e=null;else if(v>i)d.c=[d.e=0];else{if(d.e=i,d.c=[],o=(i+1)%A,0>i&&(o+=A),e>o){for(o&&d.c.push(+f.slice(0,o)),e-=A;e>o;d.c.push(+f.slice(o,o+=A)));f=f.slice(o),o=A-f.length}else o-=e;for(;o--;f+="0");d.c.push(+f)}else d.c=[d.e=0];F=0}function r(t,e,n){for(var r=1,i=e.length;!e[--i];e.pop());for(i=e[0];i>=10;i/=10,r++);return(n=r+n*A-1)>b?t.c=t.e=null:v>n?t.c=[t.e=0]:(t.e=n,t.c=e),t}function i(t){for(var e,n,r=1,i=t.length,o=t[0]+"";i>r;){for(e=t[r++]+"",n=A-e.length;n--;e="0"+e);o+=e}for(i=o.length;48===o.charCodeAt(--i););return o.slice(0,i+1||1)}function o(t,e,n){for(var r,i,o=[0],s=0,a=t.length;a>s;){for(i=o.length;i--;o[i]*=e);for(o[r=0]+=N.indexOf(t.charAt(s++));rn-1&&(null==o[r+1]&&(o[r+1]=0),o[r+1]+=o[r]/n|0,o[r]%=n)}return o.reverse()}function s(t,e,r,i){var s,a,u,c,l,f,p,d=t.indexOf("."),g=h;for(37>r&&(t=t.toLowerCase()),d>=0&&(t=t.replace(".",""),p=new n(r),l=p.pow(t.length-d),p.c=o(l.toFixed(),10,e),p.e=p.c.length),f=o(t,r,e),a=u=f.length;0==f[--u];f.pop());if(!f[0])return"0";if(0>d?--a:(l.c=f,l.e=a,l.s=i,l=P(l,p,m,g,e),f=l.c,c=l.r,a=l.e),s=a+m+1,d=f[s],u=e/2,c=c||0>s||null!=f[s+1],c=4>g?(null!=d||c)&&(0==g||g==(l.s<0?3:2)):d>u||d==u&&(4==g||c||6==g&&1&f[s-1]||g==(l.s<0?8:7)),1>s||!f[0])f.length=1,u=0,c?(f[0]=1,a=-m):a=f[0]=0;else{if(f.length=s,c)for(--e;++f[--s]>e;)f[s]=0,s||(++a,f.unshift(1));for(u=f.length;!f[--u];);}for(d=0,t="";u>=d;t+=N.charAt(f[d++]));if(0>a){for(;++a;t="0"+t);t="0."+t}else if(d=t.length,++a>d)for(a-=d;a--;t+="0");else d>a&&(t=t.slice(0,a)+"."+t.slice(a));return t}function a(t,e,r){var o,s,a,u=(t=new n(t)).e;if(null==e?o=0:(c(t,++e,h),o=r?e:e+t.e-u,u=t.e),s=i(t.c),1==r||2==r&&(u>=e||d>=u)){for(;s.length1&&(s=s.charAt(0)+"."+s.slice(1)),s+=(0>u?"e":"e+")+u 2 | }else{if(r=s.length,0>u){for(a=o-r;++u;s="0"+s);s="0."+s}else if(++u>r){for(a=o-u,u-=r;u--;s+="0");a>0&&(s+=".")}else a=o-r,r>u?s=s.slice(0,u)+"."+s.slice(u):a>0&&(s+=".");if(a>0)for(;a--;s+="0");}return t.s<0&&t.c[0]?"-"+s:s}function u(t,e,n,r,i,o){if(y){var s,a=["new BigNumber","cmp","div","eq","gt","gte","lt","lte","minus","mod","plus","times","toFraction","divToInt"][F?0>F?-F:F:0>1/F?1:0]+"()",u=l?" out of range":" not a"+(i?" non-zero":"n")+" integer";throw u=([a+" number type has more than 15 significant digits",a+" not a base "+n+" number",a+" base"+u,a+" not a number"][e]||n+"() "+e+(o?" not a boolean or binary digit":u+(r?" or not ["+(l?" negative, positive":" integer, integer")+" ]":"")))+": "+t,l=F=0,s=new Error(u),s.name="BigNumber Error",s}}function c(t,e,n,r){var i,o,s,a,u,c,l,f,p=D;if(f=t.c){t:{for(i=1,a=f[0];a>=10;a/=10,i++);if(o=e-i,0>o)o+=A,s=e,u=f[c=0],l=u/p[i-s-1]%10|0;else if(c=Math.ceil((o+1)/A),c>=f.length){if(!r)break t;for(;f.length<=c;f.push(0));u=l=0,i=1,o%=A,s=o-A+1}else{for(u=a=f[c],i=1;a>=10;a/=10,i++);o%=A,s=o-A+i,l=0>s?0:u/p[i-s-1]%10|0}if(r=r||0>e||null!=f[c+1]||(0>s?u:u%p[i-s-1]),r=4>n?(l||r)&&(0==n||n==(t.s<0?3:2)):l>5||5==l&&(4==n||r||6==n&&(o>0?s>0?u/p[i-s]:0:f[c-1])%10&1||n==(t.s<0?8:7)),1>e||!f[0])return f.length=0,r?(e-=t.e+1,f[0]=p[e%A],t.e=-e||0):f[0]=t.e=0,t;if(0==o?(f.length=c,a=1,c--):(f.length=c+1,a=p[A-o],f[c]=s>0?_(u/p[i-s]%p[s])*a:0),r)for(;;){if(0==c){for(o=1,s=f[0];s>=10;s/=10,o++);for(s=f[0]+=a,a=1;s>=10;s/=10,a++);o!=a&&(t.e++,f[0]==E&&(f[0]=1));break}if(f[c]+=a,f[c]!=E)break;f[c--]=0,a=1}for(o=f.length;0===f[--o];f.pop());}t.e>b?t.c=t.e=null:t.et||t>n)||w(t)!=t&&0!==t)},c=o&&"object"==typeof o?function(){return o.hasOwnProperty(e)?null!=(t=o[e]):void 0}:function(){return i.length>n?null!=(t=i[n++]):void 0};if(c(e="DECIMAL_PLACES")&&(a(t,0,f)?m=0|t:u(t,e,s)),r[e]=m,c(e="ROUNDING_MODE")&&(a(t,0,8)?h=0|t:u(t,e,s)),r[e]=h,c(e="EXPONENTIAL_AT")&&(a(t,-f,f)?d=-(g=~~(0>t?-t:+t)):!l&&t&&a(t[0],-f,0)&&a(t[1],0,f)?(d=~~t[0],g=~~t[1]):u(t,e,s,1)),r[e]=[d,g],c(e="RANGE")&&(a(t,-f,f)&&~~t?v=-(b=~~(0>t?-t:+t)):!l&&t&&a(t[0],-f,-1)&&a(t[1],1,f)?(v=~~t[0],b=~~t[1]):u(t,e,s,1,1)),r[e]=[v,b],c(e="ERRORS")&&(t===!!t||1===t||0===t?(l=F=0,w=(y=!!t)?parseInt:parseFloat):u(t,e,s,0,0,1)),r[e]=y,c(e="FORMAT"))if("object"==typeof t)x=t;else if(y)throw r=new Error(s+"() "+e+" not an object: "+t),r.name="BigNumber Error",r;return r[e]=x,r};var P=function(){function t(t,e,n){var r,i,o,s,a=0,u=t.length,c=e%B,l=e/B|0;for(t=t.slice();u--;)o=t[u]%B,s=t[u]/B|0,r=l*o+s*c,i=c*o+r%B*B+a,a=(i/n|0)+(r/B|0)+l*s,t[u]=i%n;return a&&t.unshift(a),t}function e(t,e,n,r){var i,o;if(n!=r)o=n>r?1:-1;else for(i=o=0;n>i;i++)if(t[i]!=e[i]){o=t[i]>e[i]?1:-1;break}return o}function r(t,e,n,r){for(var i=0;n--;)t[n]-=i,i=t[n]1;t.shift());}return function(i,o,s,a,u){var l,f,p,m,h,d,g,v,b,y,w,x,I,N,F,O,T,B=i.s==o.s?1:-1,D=i.c,S=o.c;if(!(D&&D[0]&&S&&S[0]))return new n(i.s&&o.s&&(D?!S||D[0]!=S[0]:S)?D&&0==D[0]||!S?0*B:B/0:0/0);for(v=new n(B),b=v.c=[],f=i.e-o.e,B=s+f+1,u||(u=E,N=i.e/A,p=0|N,f=(N>0||N===p?p:p-1)-(O=o.e/A,p=0|O,O>0||O===p?p:p-1),B=B/A|0),p=0;S[p]==(D[p]||0);p++);if(S[p]>(D[p]||0)&&f--,0>B)b.push(1),m=!0;else{for(N=D.length,O=S.length,p=0,B+=2,h=_(u/(S[0]+1)),h>1&&(S=t(S,h,u),D=t(D,h,u),O=S.length,N=D.length),I=O,y=D.slice(0,O),w=y.length;O>w;y[w++]=0);T=S.slice(),T.unshift(0),F=S[0],S[1]>=u/2&&F++;do h=0,l=e(S,y,O,w),0>l?(x=y[0],O!=w&&(x=x*u+(y[1]||0)),h=_(x/F),h>1?(h>=u&&(h=u-1),d=t(S,h,u),g=d.length,w=y.length,l=e(d,y,g,w),1==l&&(h--,r(d,g>O?T:S,g,u))):(0==h&&(l=h=1),d=S.slice()),g=d.length,w>g&&d.unshift(0),r(y,d,w,u),-1==l&&(w=y.length,l=e(S,y,O,w),1>l&&(h++,r(y,w>O?T:S,w,u))),w=y.length):0===l&&(h++,y=[0]),b[p++]=h,l&&y[0]?y[w++]=D[I]||0:(y=[D[I]],w=1);while((I++=10;B/=10,p++);c(v,s+(v.e=p+f*A-1)+1,a,m)}else v.e=f,v.r=+m;return v}}();I.absoluteValue=I.abs=function(){var t=new n(this);return t.s<0&&(t.s=1),t},I.ceil=function(){return c(new n(this),this.e+1,2)},I.comparedTo=I.cmp=function(t,e){var r,i=this,o=i.c,s=(F=-F,t=new n(t,e)).c,a=i.s,u=t.s,c=i.e,l=t.e;if(!a||!u)return null;if(r=o&&!o[0],e=s&&!s[0],r||e)return r?e?0:-u:a;if(a!=u)return a;if(r=0>a,e=c==l,!o||!s)return e?0:!o^r?1:-1;if(!e)return c>l^r?1:-1;for(a=-1,u=(c=o.length)<(l=s.length)?c:l;++as[a]^r?1:-1;return c==l?0:c>l^r?1:-1},I.decimalPlaces=I.dp=function(){var t,e,n=this.c;if(!n)return null;if(t=((e=n.length-1)-_(this.e/A))*A,e=n[e])for(;e%10==0;e/=10,t--);return 0>t&&(t=0),t},I.dividedBy=I.div=function(t,e){return F=2,P(this,new n(t,e),m,h)},I.dividedToIntegerBy=I.divToInt=function(t,e){return F=13,P(this,new n(t,e),0,1)},I.equals=I.eq=function(t,e){return F=3,0===this.cmp(t,e)},I.floor=function(){return c(new n(this),this.e+1,3)},I.greaterThan=I.gt=function(t,e){return F=4,this.cmp(t,e)>0},I.greaterThanOrEqualTo=I.gte=function(t,e){return F=5,1==(e=this.cmp(t,e))||0===e},I.isFinite=function(){return!!this.c},I.isInteger=I.isInt=function(){return!!this.c&&_(this.e/A)>this.c.length-2},I.isNaN=function(){return!this.s},I.isNegative=I.isNeg=function(){return this.s<0},I.isZero=function(){return!!this.c&&0==this.c[0]},I.lessThan=I.lt=function(t,e){return F=6,this.cmp(t,e)<0},I.lessThanOrEqualTo=I.lte=function(t,e){return F=7,-1==(e=this.cmp(t,e))||0===e},I.minus=function(t,e){var i,o,s,a,u=this,c=u.s;if(F=8,t=new n(t,e),e=t.s,!c||!e)return new n(0/0);if(c!=e)return t.s=-e,u.plus(t);var l=u.e/A,f=t.e/A,p=u.c,m=t.c;if(!l||!f){if(!p||!m)return p?(t.s=-e,t):new n(m?u:0/0);if(!p[0]||!m[0])return m[0]?(t.s=-e,t):new n(p[0]?u:3==h?-0:0)}if(i=0|l,l=l>0||l===i?i:i-1,i=0|f,f=f>0||f===i?i:i-1,p=p.slice(),c=l-f){for((a=0>c)?(c=-c,s=p):(f=l,s=m),s.reverse(),e=c;e--;s.push(0));s.reverse()}else for(o=(a=(c=p.length)<(e=m.length))?c:e,c=e=0;o>e;e++)if(p[e]!=m[e]){a=p[e]0)for(;e--;p[i++]=0);for(e=E-1;o>c;){if(p[--o]0||a===s?s:s-1,s=0|u,u=u>0||u===s?s:s-1,c=c.slice(),s=a-u){for(s>0?(u=a,i=l):(s=-s,i=c),i.reverse();s--;i.push(0));i.reverse()}for(s=c.length,e=l.length,0>s-e&&(i=l,l=c,c=i,e=s),s=0;e;)s=(c[--e]=c[e]+l[e]+s)/E|0,c[e]%=E;return s&&(c.unshift(s),++u),r(t,c,u)},I.round=function(t,e){return t=null==t||((l=0>t||t>f)||w(t)!=t)&&!u(t,"decimal places","round")?0:0|t,e=null==e||((l=0>e||e>8)||w(e)!=e&&0!==e)&&!u(e,"mode","round")?h:0|e,c(new n(this),t+this.e+1,e)},I.squareRoot=I.sqrt=function(){var t,e,r,o,s,a=this,u=a.c,l=a.s,f=a.e,p=m+4,d=new n("0.5");if(1!==l||!u||!u[0])return new n(!l||0>l&&(!u||u[0])?0/0:u?a:1/0);if(l=Math.sqrt(+a),0==l||l==1/0?(e=i(u),(e.length+f)%2==0&&(e+="0"),l=Math.sqrt(e),f=_((f+1)/2)-(0>f||f%2),l==1/0?e="1e"+f:(e=l.toExponential(),e=e.slice(0,e.indexOf("e")+1)+f),r=new n(e)):r=new n(l.toString()),r.c[0])for(f=r.e,l=f+p,3>l&&(l=0);;)if(s=r,r=d.times(s.plus(P(a,s,p,1))),i(s.c).slice(0,l)===(e=i(r.c)).slice(0,l)){if(r.e0||g===o?o:o-1)+(o=0|v,v>0||v===o?o:o-1),b=h.length,e=d.length,e>b&&(u=h,h=d,d=u,v=b,b=e,e=v),v=b+e,u=[];v--;u.push(0));for(g=e;--g>=0;){for(i=0,v=b+g,s=b,f=d[g]%B,p=d[g]/B|0;v>g;)c=h[--s]%B,l=h[s]/B|0,a=p*c+l*f,c=f*c+a%B*B+u[v]+i,i=(c/E|0)+(a/B|0)+p*l,u[v--]=c%E;u[v]=i}return i?++o:u.shift(),r(t,u,o)},I.toExponential=function(t){var e=this;return e.c?a(e,null==t||((l=0>t||t>f)||w(t)!=t&&0!==t)&&!u(t,"decimal places","toExponential")?null:0|t,1):e.toString()},I.toFixed=function(t){var e,n=this,r=d,i=g;return t=null==t||((l=0>t||t>f)||w(t)!=t&&0!==t)&&!u(t,"decimal places","toFixed")?null:n.e+(0|t),d=-(g=1/0),null!=t&&n.c?(e=a(n,t),n.s<0&&n.c&&(n.c[0]?e.indexOf("-")<0&&(e="-"+e):e=e.replace("-",""))):e=n.toString(),d=r,g=i,e},I.toFormat=function(t){var e=this;if(!e.c)return e.toString();var n,r=e.s<0,i=x.groupSeparator,o=+x.groupSize,s=+x.secondaryGroupSize,a=e.toFixed(t).split("."),u=a[0],c=a[1],l=r?u.slice(1):u,f=l.length;if(s&&(n=o,o=s,s=n,f-=n),o>0&&f>0){for(n=f%o||o,u=l.substr(0,n);f>n;n+=o)u+=i+l.substr(n,o);s>0&&(u+=i+l.slice(n)),r&&(u="-"+u)}return c?u+x.decimalSeparator+((s=+x.fractionGroupSize)?c.replace(new RegExp("\\d{"+s+"}\\B","g"),"$&"+x.fractionGroupSeparator):c):u},I.toFraction=function(t){var e,r,o,s,a,c,f,p,m,d=r=new n(S),g=f=new n(S),v=this,w=v.c,x=new n(S);if(!w)return v.toString();for(m=i(w),s=x.e=m.length-v.e-1,x.c[0]=D[(a=s%A)<0?A+a:a],(null==t||(!(F=12,c=new n(t)).s||(l=c.cmp(d)<0||!c.c)||y&&_(c.e/A)0)&&(t=s>0?x:d),a=b,b=1/0,c=new n(m),f.c[0]=0;p=P(c,x,0,1),o=r.plus(p.times(g)),1!=o.cmp(t);)r=g,g=o,d=f.plus(p.times(o=d)),f=o,x=c.minus(p.times(o=x)),c=o;return o=P(t.minus(r),g,0,1),f=f.plus(o.times(d)),r=r.plus(o.times(g)),f.s=d.s=v.s,s*=2,e=P(d,g,s,h).minus(v).abs().cmp(P(f,r,s,h).minus(v).abs())<1?[d.toString(),g.toString()]:[f.toString(),r.toString()],b=a,e},I.toNumber=function(){var t=this;return+t||(t.s?0*t.s:0/0)},I.toPower=I.pow=function(t){var e=0*t==0?~~t:t,r=new n(this),i=new n(S);if(((l=-p>t||t>p)&&(e=1*t/0)||w(t)!=t&&0!==t&&!(e=0/0))&&!u(t,"exponent","pow")||!e)return new n(Math.pow(+r,e));for(e=0>e?-e:e;1&e&&(i=i.times(r)),e>>=1,e;)r=r.times(r);return 0>t?S.div(i):i},I.toPrecision=function(t){var e=this;return null!=t&&(!(l=1>t||t>f)&&w(t)==t||u(t,"precision","toPrecision"))&&e.c?a(e,0|--t,2):e.toString()},I.toString=function(t){var e,n,r,o=this,c=o.e;if(null===c)n=o.s?"Infinity":"NaN";else{if(t==e&&(d>=c||c>=g))return a(o,e,1);if(n=i(o.c),0>c){for(;++c;n="0"+n);n="0."+n}else if(r=n.length,c>0)if(++c>r)for(c-=r;c--;n+="0");else r>c&&(n=n.slice(0,c)+"."+n.slice(c));else if(e=n.charAt(0),r>1)n=e+"."+n.slice(1);else if("0"==e)return e;if(null!=t)if((l=!(t>=2&&65>t))||t!=~~t&&y)u(t,"base","toS");else if(n=s(n,0|t,10,o.s),"0"==n)return n}return o.s<0?"-"+n:n},I.valueOf=I.toJSON=function(){return this.toString()},"undefined"!=typeof e&&e.exports?e.exports=n:"function"==typeof define&&define.amd?define(function(){return n}):t.BigNumber=n}(this)},{}],web3:[function(t,e){var n=t("./lib/web3");n.providers.HttpProvider=t("./lib/web3/httpprovider"),n.providers.QtSyncProvider=t("./lib/web3/qtsync"),n.eth.contract=t("./lib/web3/contract"),n.abi=t("./lib/solidity/abi"),"undefined"!=typeof window&&"undefined"==typeof window.web3&&(window.web3=n),e.exports=n},{"./lib/solidity/abi":1,"./lib/web3":10,"./lib/web3/contract":11,"./lib/web3/httpprovider":18,"./lib/web3/qtsync":23}]},{},["web3"]); --------------------------------------------------------------------------------