├── .eslintignore ├── .dockerignore ├── .babelrc ├── src ├── components │ ├── css │ │ ├── RollUnder.css │ │ └── Footers.css │ ├── __snapshots__ │ │ ├── Transaction.test.jsx.snap │ │ ├── Address.test.jsx.snap │ │ ├── MetaMaskLink.test.jsx.snap │ │ ├── Alert.test.jsx.snap │ │ ├── RollButton.test.jsx.snap │ │ ├── RollUnderRecap.test.jsx.snap │ │ ├── LanguageUpdate.test.jsx.snap │ │ ├── ValueSlider.test.jsx.snap │ │ ├── ContractInfo.test.jsx.snap │ │ ├── BetSize.test.jsx.snap │ │ ├── ChanceOfWinning.test.jsx.snap │ │ ├── Transactions.test.jsx.snap │ │ └── CoinFlip.test.jsx.snap │ ├── MetaMaskLink.jsx │ ├── MetaMaskLink.test.jsx │ ├── Address.test.jsx │ ├── RollButton.test.jsx │ ├── ValueSlider.test.jsx │ ├── Transaction.test.jsx │ ├── BetSize.test.jsx │ ├── RollUnderRecap.test.jsx │ ├── ChanceOfWinning.test.jsx │ ├── BaseGame.jsx │ ├── LanguageUpdate.test.jsx │ ├── Footers.jsx │ ├── Transaction.jsx │ ├── Address.jsx │ ├── Alert.test.jsx │ ├── Alert.jsx │ ├── ContractInfo.test.jsx │ ├── CoinFlipRecap.jsx │ ├── BetSize.jsx │ ├── CoinFlip.test.jsx │ ├── RollButton.jsx │ ├── ChanceOfWinning.jsx │ ├── Transactions.test.jsx │ ├── FlipButton.jsx │ ├── ValueSlider.jsx │ ├── LanguageUpdate.jsx │ ├── RollUnderRecap.jsx │ ├── CoinFlip.jsx │ ├── ContractInfo.jsx │ ├── RollUnder.jsx │ ├── Headers.jsx │ ├── CoinFlipTransactions.jsx │ ├── Transactions.jsx │ └── Container.jsx ├── utils │ ├── sentry.js │ ├── analytics.js │ ├── createComponentWithIntl.jsx │ ├── locales.js │ ├── get-web3.js │ ├── etheroll-contract.js │ ├── etheroll-contract.test.js │ └── etheroll-abi.js ├── App.test.jsx ├── contexts │ └── IntlContext.jsx ├── index.css ├── App.css ├── App.jsx ├── index.jsx ├── translations │ ├── cn.json │ ├── es.json │ ├── en.json │ ├── ru.json │ ├── fr.json │ └── vn.json ├── logo.svg └── serviceWorker.js ├── public ├── favicon.ico ├── manifest.json └── index.html ├── heroku.yml ├── .env ├── default.conf.template ├── jest.config.js ├── .eslintrc ├── .travis.yml ├── .gitignore ├── docker-compose.yml ├── Makefile ├── LICENSE ├── docs ├── Release.md └── Developers.md ├── README.md ├── Dockerfile ├── package.json └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | src/serviceWorker.js 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | .git/ 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-app" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/components/css/RollUnder.css: -------------------------------------------------------------------------------- 1 | .card.transactions { 2 | margin-top: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreMiras/etheroll/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: 4 | dockerfile: Dockerfile 5 | target: prod 6 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_SENTRY_DSN=https://597204e062cc4579b44d28fe32273301@sentry.io/1337196 2 | REACT_APP_ANALYTICS_ID=UA-130594569-1 3 | -------------------------------------------------------------------------------- /default.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen $PORT default_server; 3 | 4 | location / { 5 | root /usr/share/nginx/html; 6 | index index.html index.htm; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/q/39418555/185510 2 | module.exports = { 3 | "moduleNameMapper": { 4 | "\\.(css|less|sass|scss)$": "identity-obj-proxy" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/components/css/Footers.css: -------------------------------------------------------------------------------- 1 | .Footers { 2 | position: absolute; 3 | bottom: 0; 4 | width: 100%; 5 | height: 60px; 6 | line-height: 60px; 7 | background-color: #f5f5f5; 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | "extends": "airbnb" 2 | "env": { 3 | "browser": true, 4 | "jest": true, 5 | } 6 | "rules": { 7 | "allowShortCircuit": true, 8 | "allowTernary": true, 9 | "no-unused-expressions": "off", 10 | "no-mixed-operators": "off" 11 | } -------------------------------------------------------------------------------- /src/components/__snapshots__/Transaction.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly on empty transactions 1`] = ` 4 | 7 | 0x1234 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: generic 4 | 5 | services: 6 | - docker 7 | 8 | env: 9 | - DOCKER_COMMAND='make test CI=true' 10 | - DOCKER_COMMAND='make lint' 11 | 12 | install: 13 | - make docker/build 14 | 15 | script: 16 | - make docker/run 17 | -------------------------------------------------------------------------------- /src/components/__snapshots__/Address.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 8 | 0x0123 9 | 10 | `; 11 | -------------------------------------------------------------------------------- /src/components/__snapshots__/MetaMaskLink.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 9 | Meta Mask 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /src/components/MetaMaskLink.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const MetaMaskLink = () => ( 4 | 9 | Meta Mask 10 | 11 | ); 12 | 13 | export default MetaMaskLink; 14 | -------------------------------------------------------------------------------- /src/components/__snapshots__/Alert.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly message 1`] = ` 4 |
8 | message 9 |
10 | `; 11 | 12 | exports[`renders correctly no message 1`] = `null`; 13 | -------------------------------------------------------------------------------- /src/components/__snapshots__/RollButton.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 12 | `; 13 | -------------------------------------------------------------------------------- /src/components/MetaMaskLink.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import MetaMaskLink from './MetaMaskLink'; 4 | 5 | it('renders correctly', () => { 6 | const tree = renderer 7 | .create() 8 | .toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/Address.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Address from './Address'; 4 | 5 | it('renders correctly', () => { 6 | const tree = renderer 7 | .create(
) 8 | .toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/RollButton.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import RollButton from './RollButton'; 4 | 5 | it('renders correctly', () => { 6 | const tree = renderer 7 | .create( {}} text="text" />) 8 | .toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # custom 2 | *.swp 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /src/components/ValueSlider.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import ValueSlider from './ValueSlider'; 4 | 5 | it('renders correctly', () => { 6 | const tree = renderer 7 | .create( {}} />) 8 | .toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | hdfs: 4 | container_name: etheroll 5 | image: etheroll-js 6 | build: 7 | context: . 8 | command: | 9 | /bin/bash -c " 10 | set -e 11 | make start 12 | /bin/bash || exit 0 13 | " 14 | ports: 15 | - "3000:3000" 16 | - "80:3000" 17 | working_dir: /app 18 | -------------------------------------------------------------------------------- /src/components/Transaction.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Transaction from './Transaction'; 4 | 5 | 6 | it('renders correctly on empty transactions', () => { 7 | const tree = renderer 8 | .create() 9 | .toJSON(); 10 | expect(tree).toMatchSnapshot(); 11 | }); 12 | -------------------------------------------------------------------------------- /src/utils/sentry.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/browser'; 2 | import { version } from '../../package.json'; 3 | 4 | const setupSentry = () => { 5 | if (process.env.NODE_ENV !== 'production') { 6 | return; 7 | } 8 | Sentry.init({ 9 | dsn: process.env.REACT_APP_SENTRY_DSN, 10 | release: version, 11 | }); 12 | }; 13 | 14 | export default setupSentry; 15 | -------------------------------------------------------------------------------- /src/components/BetSize.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createComponentWithIntl from '../utils/createComponentWithIntl'; 3 | import BetSize from './BetSize'; 4 | 5 | it('renders correctly', () => { 6 | const tree = createComponentWithIntl( 7 | {}} />, 8 | ).toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/RollUnderRecap.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createComponentWithIntl from '../utils/createComponentWithIntl'; 3 | import RollUnderRecap from './RollUnderRecap'; 4 | 5 | it('renders correctly', () => { 6 | const tree = createComponentWithIntl( 7 | , 8 | ).toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/App.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import IntlContextProvider from './contexts/IntlContext'; 4 | import App from './App'; 5 | 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render(, div); 9 | ReactDOM.unmountComponentAtNode(div); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/ChanceOfWinning.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createComponentWithIntl from '../utils/createComponentWithIntl'; 3 | import ChanceOfWinning from './ChanceOfWinning'; 4 | 5 | it('renders correctly', () => { 6 | const tree = createComponentWithIntl( 7 | {}} />, 8 | ).toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/utils/analytics.js: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga'; 2 | 3 | 4 | const fireTracking = () => ( 5 | ReactGA.pageview(window.location.pathname + window.location.search) 6 | ); 7 | 8 | const setupGA = () => { 9 | if (process.env.NODE_ENV !== 'production') { 10 | return; 11 | } 12 | ReactGA.initialize(process.env.REACT_APP_ANALYTICS_ID); 13 | fireTracking(); 14 | }; 15 | 16 | export default setupGA; 17 | -------------------------------------------------------------------------------- /src/components/BaseGame.jsx: -------------------------------------------------------------------------------- 1 | import { toWei } from 'web3-utils'; 2 | 3 | const onRollClick = ({ 4 | accountAddress, rollUnder, contract, betSize, 5 | }) => { 6 | const value = toWei(betSize.toString(), 'ether'); 7 | contract.web3Contract.methods.playerRollDice(rollUnder).send({ 8 | from: accountAddress, 9 | value, 10 | }).then(result => console.log(JSON.stringify(result))); 11 | }; 12 | 13 | export default onRollClick; 14 | -------------------------------------------------------------------------------- /src/components/LanguageUpdate.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import IntlContextProvider from '../contexts/IntlContext'; 4 | import LanguageUpdate from './LanguageUpdate'; 5 | 6 | it('renders correctly', () => { 7 | const tree = renderer.create( 8 | 9 | 10 | , 11 | ).toJSON(); 12 | expect(tree).toMatchSnapshot(); 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/Footers.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './css/Footers.css'; 3 | import { version } from '../../package.json'; 4 | 5 | const Footers = () => ( 6 |
7 |
8 | 9 | Copyright (c) 2018 Andre Miras - Etheroll v 10 | {version} 11 | 12 |
13 |
14 | ); 15 | 16 | export default Footers; 17 | -------------------------------------------------------------------------------- /src/components/Transaction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { string, number } from 'prop-types'; 3 | import { etherscanUrls } from '../utils/etheroll-contract'; 4 | 5 | 6 | const Transaction = ({ hash, network }) => { 7 | const url = `${etherscanUrls[network]}/tx/${hash}`; 8 | return {hash}; 9 | }; 10 | Transaction.propTypes = { 11 | hash: string.isRequired, 12 | network: number.isRequired, 13 | }; 14 | 15 | export default Transaction; 16 | -------------------------------------------------------------------------------- /src/utils/createComponentWithIntl.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * https://github.com/formatjs/react-intl/blob/v3.10.0/docs/Testing-with-React-Intl.md#helper-function-2 3 | */ 4 | import React from 'react'; 5 | import renderer from 'react-test-renderer'; 6 | import { IntlProvider } from 'react-intl'; 7 | 8 | const createComponentWithIntl = (children, props = { locale: 'en' }) => ( 9 | renderer.create({children}) 10 | ); 11 | 12 | export default createComponentWithIntl; 13 | -------------------------------------------------------------------------------- /src/components/Address.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { string, number } from 'prop-types'; 3 | import { etherscanUrls } from '../utils/etheroll-contract'; 4 | 5 | 6 | const Address = ({ address, network }) => { 7 | const url = `${etherscanUrls[network]}/address/${address}`; 8 | return {address}; 9 | }; 10 | Address.propTypes = { 11 | address: string.isRequired, 12 | network: number.isRequired, 13 | }; 14 | 15 | export default Address; 16 | -------------------------------------------------------------------------------- /src/components/Alert.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Alert from './Alert'; 4 | 5 | it('renders correctly message', () => { 6 | const tree = renderer 7 | .create() 8 | .toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | 12 | it('renders correctly no message', () => { 13 | const tree = renderer 14 | .create() 15 | .toJSON(); 16 | expect(tree).toMatchSnapshot(); 17 | }); 18 | -------------------------------------------------------------------------------- /src/utils/locales.js: -------------------------------------------------------------------------------- 1 | import messagesCn from '../translations/cn.json'; 2 | import messagesEs from '../translations/es.json'; 3 | import messagesFr from '../translations/fr.json'; 4 | import messagesRu from '../translations/ru.json'; 5 | import messagesVn from '../translations/vn.json'; 6 | 7 | 8 | const messages = { 9 | en: null, 10 | cn: messagesCn, 11 | es: messagesEs, 12 | fr: messagesFr, 13 | ru: messagesRu, 14 | vn: messagesVn, 15 | }; 16 | const locales = Object.keys(messages); 17 | 18 | export { messages, locales }; 19 | -------------------------------------------------------------------------------- /src/components/Alert.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { string, node, oneOfType } from 'prop-types'; 3 | 4 | const Alert = ({ classType, message }) => ( 5 | message 6 | ? ( 7 |
8 | {message} 9 |
10 | ) 11 | : null 12 | ); 13 | Alert.propTypes = { 14 | classType: string, 15 | message: oneOfType([string, node]), 16 | }; 17 | Alert.defaultProps = { 18 | classType: 'primary', 19 | message: null, 20 | }; 21 | 22 | export default Alert; 23 | -------------------------------------------------------------------------------- /src/contexts/IntlContext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { element } from 'prop-types'; 3 | 4 | 5 | export const IntlContext = React.createContext(); 6 | 7 | const IntlContextProvider = ({ children }) => { 8 | const [locale, setLocale] = React.useState('en'); 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | IntlContextProvider.propTypes = { 16 | children: element.isRequired, 17 | }; 18 | 19 | export default IntlContextProvider; 20 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* https://getbootstrap.com/docs/4.1/examples/sticky-footer-navbar/ */ 2 | html { 3 | position: relative; 4 | min-height: 100%; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 11 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 12 | sans-serif; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | code { 18 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 19 | monospace; 20 | } 21 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | padding: 60px 0 0; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | } 9 | 10 | .App-header { 11 | background-color: #282c34; 12 | min-height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | font-size: calc(10px + 2vmin); 18 | color: white; 19 | } 20 | 21 | .App-link { 22 | color: #61dafb; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { 27 | transform: rotate(0deg); 28 | } 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/ContractInfo.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createComponentWithIntl from '../utils/createComponentWithIntl'; 3 | import ContractInfo from './ContractInfo'; 4 | 5 | it('renders correctly', () => { 6 | const tree = createComponentWithIntl( 7 | , 8 | ).toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | 12 | 13 | it('no account address', () => { 14 | const tree = createComponentWithIntl( 15 | , 16 | ).toJSON(); 17 | expect(tree).toMatchSnapshot(); 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/CoinFlipRecap.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { number } from 'prop-types'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import { getProfit } from '../utils/etheroll-contract'; 5 | 6 | 7 | const CoinFlipRecap = ({ betSize }) => { 8 | const chances = 50; 9 | const profit = getProfit(betSize, chances); 10 | return ( 11 |

12 | 17 |

18 | ); 19 | }; 20 | CoinFlipRecap.propTypes = { 21 | betSize: number.isRequired, 22 | }; 23 | 24 | export default CoinFlipRecap; 25 | -------------------------------------------------------------------------------- /src/utils/get-web3.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | 3 | const getWeb3 = new Promise((resolve, reject) => { 4 | window.addEventListener('load', async () => { 5 | let web3; 6 | if (window.ethereum) { // Modern dapp browsers... 7 | web3 = new Web3(window.ethereum); 8 | try { 9 | // Request account access if needed 10 | await window.ethereum.enable(); 11 | } catch (error) { 12 | console.error(error); 13 | } 14 | } else if (window.web3) { // Legacy dapp browsers... 15 | web3 = new Web3(window.web3.currentProvider); 16 | } else { // Non-dapp browsers... 17 | reject(new Error('Non-Ethereum browser detected. Is MetaMask running?')); 18 | } 19 | const results = { web3 }; 20 | resolve(results); 21 | }); 22 | }); 23 | 24 | 25 | export default getWeb3; 26 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IntlProvider } from 'react-intl'; 3 | import { HashRouter as Router } from 'react-router-dom'; 4 | import './App.css'; 5 | import { IntlContext } from './contexts/IntlContext'; 6 | import Headers from './components/Headers'; 7 | import Footers from './components/Footers'; 8 | import Container from './components/Container'; 9 | import { messages } from './utils/locales'; 10 | 11 | 12 | const App = () => { 13 | const [locale] = React.useContext(IntlContext); 14 | return ( 15 | 16 | 17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/components/BetSize.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { number, func } from 'prop-types'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import ValueSlider from './ValueSlider'; 5 | 6 | const BetSize = ({ 7 | betSize, min, max, updateBetSize, 8 | }) => ( 9 |
10 | 11 | 15 | 16 | 17 |
18 | ); 19 | BetSize.propTypes = { 20 | betSize: number.isRequired, 21 | min: number, 22 | max: number, 23 | updateBetSize: func.isRequired, 24 | }; 25 | BetSize.defaultProps = { 26 | min: 0, 27 | max: 10, 28 | }; 29 | 30 | export default BetSize; 31 | -------------------------------------------------------------------------------- /src/components/CoinFlip.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createComponentWithIntl from '../utils/createComponentWithIntl'; 3 | import CoinFlip from './CoinFlip'; 4 | 5 | it('renders correctly', () => { 6 | const accountAddress = '0x46044beaa1e985c67767e04de58181de5daaa00f'; 7 | const betSize = 0.1; 8 | const filteredTransactions = []; 9 | const maxBet = 10; 10 | const minBet = 0; 11 | const network = 1; 12 | const filterTransactions = () => {}; 13 | const updateState = () => () => {}; 14 | const gameProps = { 15 | accountAddress, 16 | betSize, 17 | filteredTransactions, 18 | maxBet, 19 | minBet, 20 | network, 21 | updateState, 22 | filterTransactions, 23 | }; 24 | const tree = createComponentWithIntl( 25 | , 26 | ).toJSON(); 27 | expect(tree).toMatchSnapshot(); 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/RollButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { bool, func, string } from 'prop-types'; 3 | 4 | const Button = ({ isDisabled, onClick, text }) => ( 5 | 13 | ); 14 | Button.propTypes = { 15 | isDisabled: bool, 16 | onClick: func.isRequired, 17 | text: string.isRequired, 18 | }; 19 | Button.defaultProps = { 20 | isDisabled: false, 21 | }; 22 | 23 | const RollButton = ({ isDisabled, onClick }) => ( 24 | 14 | ); 15 | Button.propTypes = { 16 | isDisabled: bool, 17 | onClick: func.isRequired, 18 | text: string.isRequired, 19 | }; 20 | Button.defaultProps = { 21 | isDisabled: false, 22 | }; 23 | 24 | const FlipButton = ({ isDisabled, onClick }) => { 25 | // https://github.com/leesx/react-intl-demo2018/blob/0cd88df/docs/react-intl-corner-cases.md 26 | const messages = defineMessages({ 27 | text: { 28 | id: 'flipbutton.text', 29 | defaultMessage: 'Flip Head', 30 | }, 31 | }); 32 | const intl = useIntl(); 33 | return ( 34 | 21 |
25 | 32 | 39 | 46 | 53 | 60 | 67 |
68 | 69 | `; 70 | -------------------------------------------------------------------------------- /src/components/LanguageUpdate.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { arrayOf, func, string } from 'prop-types'; 3 | import { IntlContext } from '../contexts/IntlContext'; 4 | import { locales } from '../utils/locales'; 5 | 6 | const DropdownItem = ({ text, onClick }) => ( 7 | 8 | ); 9 | DropdownItem.propTypes = { 10 | text: string.isRequired, 11 | onClick: func.isRequired, 12 | }; 13 | 14 | const DropdownMenu = ({ items, onClick }) => ( 15 |
16 | {items.map(item => )} 17 |
18 | ); 19 | DropdownMenu.propTypes = { 20 | items: arrayOf(string).isRequired, 21 | onClick: func.isRequired, 22 | }; 23 | 24 | const LanguageUpdate = () => { 25 | const [locale, setLocale] = React.useContext(IntlContext); 26 | 27 | return ( 28 |
  • 29 | 41 | setLocale(newLocale)} 44 | /> 45 |
  • 46 | ); 47 | }; 48 | 49 | export default LanguageUpdate; 50 | -------------------------------------------------------------------------------- /src/components/RollUnderRecap.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { number } from 'prop-types'; 4 | import { getProfit } from '../utils/etheroll-contract'; 5 | 6 | 7 | const RollUnderRecap = ({ betSize, value }) => { 8 | const chances = value - 1; 9 | const profit = getProfit(betSize, chances); 10 | return ( 11 |
    12 |
    13 |

    14 | 18 |

    19 |
    20 |
    21 |

    {value}

    22 |
    23 |
    24 | 28 |
    29 |
    30 |

    31 | {betSize.toFixed(2)} 32 |   33 | ETH 34 |

    35 |
    36 |
    37 | 41 |
    42 |
    43 |

    44 | {profit.toFixed(2)} 45 |   46 | ETH 47 |

    48 |
    49 |
    50 | ); 51 | }; 52 | RollUnderRecap.propTypes = { 53 | betSize: number.isRequired, 54 | value: number.isRequired, 55 | }; 56 | 57 | export default RollUnderRecap; 58 | -------------------------------------------------------------------------------- /src/components/__snapshots__/ValueSlider.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |
    7 |
    10 | 16 |
    17 |
    20 |
    29 |
    33 |
    43 |
    46 |
    65 |
    68 |
    69 |
    70 |
    71 | `; 72 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | Etheroll :: Blockchain casino :: Ether gambling 23 | 24 | 25 | 28 |
    29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/__snapshots__/ContractInfo.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`no account address 1`] = ` 4 |
    7 |
    10 | 13 |   14 | Account (0.00 ETH) 15 |
    16 |
    19 | 20 | Not connected, please login to MetaMask 21 | 22 |
    23 |
    26 | 29 |   30 | Contract (123.00 ETH) 31 |
    32 | 42 |
    43 | `; 44 | 45 | exports[`renders correctly 1`] = ` 46 |
    49 |
    52 | 55 |   56 | Account (1.12 ETH) 57 |
    58 | 68 |
    71 | 74 |   75 | Contract (123.00 ETH) 76 |
    77 | 87 |
    88 | `; 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etheroll", 3 | "version": "2020.05.28", 4 | "private": true, 5 | "homepage": "https://andremiras.github.io/etheroll/", 6 | "dependencies": { 7 | "@fortawesome/fontawesome-free": "^5.5.0", 8 | "@sentry/browser": "^4.4.1", 9 | "babel-preset-react": "^6.24.1", 10 | "bignumber.js": "^8.0.1", 11 | "bootstrap": "^4.3.1", 12 | "extract-react-intl-messages": "^2.3.5", 13 | "prop-types": "^15.6.2", 14 | "rc-slider": "^8.6.3", 15 | "react": "^16.6.1", 16 | "react-dom": "^16.6.1", 17 | "react-ga": "^2.5.6", 18 | "react-intl": "^3.9.3", 19 | "react-router-dom": "^4.3.1", 20 | "react-scripts": "2.1.1", 21 | "react-test-renderer": "^16.6.3", 22 | "web3": "^1.2.2", 23 | "web3-utils": "^1.2.4" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "build-staging": "PUBLIC_URL=/ react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject", 31 | "predeploy": "npm run build", 32 | "deploy": "gh-pages -d build", 33 | "lint": "eslint -c .eslintrc src/**/*.{js,jsx}", 34 | "intl": "NODE_ENV=development extract-messages -l=en,es,fr -o src/translations/ --flat true 'src/**/!(*.test).jsx'" 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "browserslist": [ 40 | ">0.2%", 41 | "not dead", 42 | "not ie <= 11", 43 | "not op_mini all" 44 | ], 45 | "devDependencies": { 46 | "eslint-config-airbnb": "^17.1.0", 47 | "eslint-plugin-import": "^2.14.0", 48 | "eslint-plugin-jsx-a11y": "^6.1.2", 49 | "eslint-plugin-react": "^7.11.1", 50 | "gh-pages": "^2.0.1", 51 | "identity-obj-proxy": "^3.0.0", 52 | "jquery": "^3.5.0", 53 | "popper.js": "^1.14.5" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/CoinFlip.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { 3 | arrayOf, func, number, shape, string, 4 | } from 'prop-types'; 5 | import onRollClick from './BaseGame'; 6 | import BetSize from './BetSize'; 7 | import FlipButton from './FlipButton'; 8 | import CoinFlipRecap from './CoinFlipRecap'; 9 | import CoinFlipTransactions from './CoinFlipTransactions'; 10 | 11 | 12 | const CoinFlip = (props) => { 13 | const { 14 | accountAddress, betSize, contract, 15 | filterTransactions, filteredTransactions, minBet, maxBet, network, 16 | updateState, 17 | } = props; 18 | const rollUnder = 51; 19 | const onRollClickProps = { 20 | accountAddress, rollUnder, contract, betSize, 21 | }; 22 | const rollDisabled = accountAddress === null; 23 | return ( 24 | 25 | 26 | 27 | onRollClick(onRollClickProps)} /> 28 | filterTransactions(transactionsFilter)} 31 | transactions={filteredTransactions} 32 | /> 33 | 34 | ); 35 | }; 36 | 37 | CoinFlip.propTypes = { 38 | accountAddress: string, 39 | betSize: number.isRequired, 40 | contract: shape({ 41 | // TODO: seems completely ignored 42 | // https://github.com/facebook/prop-types/issues/181 43 | todo: number, 44 | }), 45 | filterTransactions: func.isRequired, 46 | filteredTransactions: arrayOf(shape({ 47 | // TODO: seems completely ignored 48 | // https://github.com/facebook/prop-types/issues/181 49 | todo: number, 50 | })).isRequired, 51 | minBet: number.isRequired, 52 | maxBet: number.isRequired, 53 | network: number.isRequired, 54 | updateState: func.isRequired, 55 | }; 56 | CoinFlip.defaultProps = { 57 | accountAddress: null, 58 | contract: null, 59 | }; 60 | 61 | export default CoinFlip; 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [2020.05.28] 4 | 5 | - Migrate to new contract 6 | 7 | ## [2020.05.22] 8 | 9 | - Translate to Vietnamese (@hesman) 10 | - Migrate to new contract 11 | 12 | 13 | ## [2020.04.05] 14 | 15 | - Docker build & run Makefile targets, refs #59 16 | - Translate to Chinese, refs #64 17 | - Translate to Russian, refs #65 18 | - Deploy to Heroku, refs #67 19 | 20 | 21 | ## [2020.03.07] 22 | 23 | - Language switcher, refs #61, #62 and #63 24 | 25 | 26 | ## [2020.01.03] 27 | 28 | - Setup up basic translations in French and Spanish, refs #2 29 | 30 | 31 | ## [2019.12.16] 32 | 33 | - Post Web3 1.2 migration fixes, refs #56, #60 34 | 35 | 36 | ## [2019.11.11] 37 | 38 | - Migrates from to Web3 1.2, refs #56 39 | 40 | 41 | ## [v20191110] 42 | 43 | - Fix input size on smartphone 44 | - Dependencies bumps 45 | - Setup unit testing, refs #6 46 | - Provide Docker image, refs #13 47 | - CI testing with Travis, refs #7 48 | - Show error message on missing web3-wallet, refs #4 and #5 49 | - Show transaction history, refs #1 50 | - Pull min bet and min/max roll number from contract, refs #8 51 | - Clarify units, refs #17 52 | - Detect when not logged in to MetaMask, refs #20 53 | - Show account and contract balance, refs #12 54 | - Indicate potential profit, refs #19 55 | - Setup error reporting, refs #22 56 | - Wager double digit precision, refs #25 57 | - Setup web tracking, refs #3 58 | - Periodically refresh transaction history, refs #29 59 | - Introduce react router for multiple games (@lgarest), refs #30 60 | - Add coin flip game, refs #11, and #38 61 | - Add docker-compose.yml (@Npizza), refs #44 62 | - Refactor etheroll contract (@Simonboeuf1), refs #47 63 | - Clean proptype imports and private components (@lgarest), refs #49 64 | 65 | 66 | ## [v20181016] 67 | 68 | - Show transaction on roll 69 | - Network management (support for Mainnet & Ropsten) 70 | - Deploy to GitHub-Pages 71 | - Layout improvements & fixes 72 | 73 | 74 | ## [v20181015] 75 | 76 | - Connect to the Etheroll contract 77 | 78 | 79 | ## [v20181012] 80 | 81 | - Initial UI release, with no blockchain interactions 82 | -------------------------------------------------------------------------------- /src/components/__snapshots__/BetSize.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |
    7 | 8 | Bet size 9 | 10 |
    13 |
    16 | 22 |
    25 | 28 | ETH 29 | 30 |
    31 |
    32 |
    35 |
    44 |
    48 |
    58 |
    61 |
    80 |
    83 |
    84 |
    85 |
    86 |
    87 | `; 88 | -------------------------------------------------------------------------------- /src/components/__snapshots__/ChanceOfWinning.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |
    7 | 8 | Chance of winning 9 | 10 |
    13 |
    16 | 22 |
    25 | 28 |  %  29 | 30 |
    31 |
    32 |
    35 |
    44 |
    48 |
    58 |
    61 |
    80 |
    83 |
    84 |
    85 |
    86 |
    87 | `; 88 | -------------------------------------------------------------------------------- /src/components/ContractInfo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { number, string } from 'prop-types'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import Address from './Address'; 5 | 6 | const ContractInfo = ({ 7 | accountAddress, accountBalance, contractAddress, contractBalance, network, 8 | }) => { 9 | const contractAddr =
    ; 10 | 11 | const contractBalanceBlock = ( 12 |
    13 | 14 |   15 | 20 |
    21 | ); 22 | 23 | const contractAddressBlock = ( 24 |
    25 | {contractAddr} 26 |
    27 | ); 28 | const accountAddr = (accountAddress !== null) 29 | ?
    30 | : ( 31 | 32 | 36 | 37 | ); 38 | 39 | const accountBalanceBlock = ( 40 |
    41 | 42 |   43 | 48 |
    49 | ); 50 | 51 | const accountAddressBlock = ( 52 |
    53 | {accountAddr} 54 |
    55 | ); 56 | 57 | return ( 58 |
    59 | {accountBalanceBlock} 60 | {accountAddressBlock} 61 | {contractBalanceBlock} 62 | {contractAddressBlock} 63 |
    64 | ); 65 | }; 66 | ContractInfo.propTypes = { 67 | accountAddress: string, 68 | accountBalance: number.isRequired, 69 | contractAddress: string.isRequired, 70 | contractBalance: number.isRequired, 71 | network: number.isRequired, 72 | }; 73 | ContractInfo.defaultProps = { 74 | accountAddress: null, 75 | }; 76 | 77 | export default ContractInfo; 78 | -------------------------------------------------------------------------------- /docs/Developers.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
    10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
    13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
    18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
    23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
    26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | 41 | ### `npm run deploy` 42 | 43 | Deploy to GitHub-Pages. 44 | 45 | 46 | ## Learn More 47 | 48 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 49 | 50 | To learn React, check out the [React documentation](https://reactjs.org/). 51 | -------------------------------------------------------------------------------- /src/components/RollUnder.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | arrayOf, func, number, shape, string, 4 | } from 'prop-types'; 5 | import './css/RollUnder.css'; 6 | import onRollClick from './BaseGame'; 7 | import BetSize from './BetSize'; 8 | import ChanceOfWinning from './ChanceOfWinning'; 9 | import RollUnderRecap from './RollUnderRecap'; 10 | import RollButton from './RollButton'; 11 | import Transactions from './Transactions'; 12 | 13 | 14 | const RollUnder = (props) => { 15 | const { 16 | accountAddress, betSize, chances, contract, 17 | filterTransactions, filteredTransactions, minBet, maxBet, minChances, maxChances, network, 18 | updateState, 19 | } = props; 20 | const rollUnder = chances + 1; 21 | const onRollClickProps = { 22 | accountAddress, rollUnder, contract, betSize, 23 | }; 24 | const rollDisabled = accountAddress === null; 25 | return ( 26 |
    27 |
    28 | 29 | 30 | 31 | onRollClick(onRollClickProps)} /> 32 | 33 | filterTransactions(transactionsFilter)} 36 | transactions={filteredTransactions} 37 | /> 38 |
    39 | ); 40 | }; 41 | RollUnder.propTypes = { 42 | accountAddress: string, 43 | betSize: number.isRequired, 44 | chances: number.isRequired, 45 | contract: shape({ 46 | // TODO: seems completely ignored 47 | // https://github.com/facebook/prop-types/issues/181 48 | todo: number, 49 | }), 50 | filterTransactions: func.isRequired, 51 | filteredTransactions: arrayOf(shape({ 52 | // TODO: seems completely ignored 53 | // https://github.com/facebook/prop-types/issues/181 54 | todo: number, 55 | })).isRequired, 56 | minBet: number.isRequired, 57 | maxBet: number.isRequired, 58 | minChances: number.isRequired, 59 | maxChances: number.isRequired, 60 | network: number.isRequired, 61 | updateState: func.isRequired, 62 | }; 63 | RollUnder.defaultProps = { 64 | accountAddress: null, 65 | contract: null, 66 | }; 67 | 68 | export default RollUnder; 69 | -------------------------------------------------------------------------------- /src/components/Headers.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { NavLink } from 'react-router-dom'; 4 | import LanguageUpdate from './LanguageUpdate'; 5 | 6 | const Logo = () => ( 7 | 8 | 9 | {' Etheroll'} 10 | 11 | ); 12 | 13 | const HamburgerBtn = () => ( 14 | 25 | ); 26 | 27 | const NavSections = () => ( 28 | 69 | ); 70 | 71 | const Headers = () => ( 72 |
    73 | 78 |
    79 | ); 80 | 81 | export default Headers; 82 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/utils/etheroll-contract.js: -------------------------------------------------------------------------------- 1 | import etherollAbi from './etheroll-abi'; 2 | 3 | const HOUSE_EDGE = 1 / 100.0; 4 | 5 | const Networks = Object.freeze({ mainnet: 1, morden: 2, ropsten: 3 }); 6 | 7 | const contractAddresses = { 8 | [Networks.mainnet]: '0xf478c8Bc5448236d52067c96F8f4C8376E62Fa8f', 9 | [Networks.ropsten]: '0xe12c6dEb59f37011d2D9FdeC77A6f1A8f3B8B1e8', 10 | }; 11 | 12 | const etherscanUrls = { 13 | [Networks.mainnet]: 'https://etherscan.io', 14 | [Networks.ropsten]: 'https://ropsten.etherscan.io', 15 | }; 16 | 17 | 18 | const getPayout = (betSize, winningChances) => ( 19 | 100 / winningChances * betSize 20 | ); 21 | 22 | const cutHouseEdge = payout => ( 23 | payout * (1 - HOUSE_EDGE) 24 | ); 25 | 26 | const getProfit = (betSize, winningChances) => { 27 | if (winningChances === 0) { 28 | return 0; 29 | } 30 | const rawPayout = getPayout(betSize, winningChances); 31 | const netPayout = cutHouseEdge(rawPayout); 32 | 33 | return Math.max(netPayout - betSize, 0); 34 | }; 35 | 36 | 37 | // Merges bet logs (LogBet) with bet results logs (LogResult). 38 | const mergeLogs = (logBetEvents, logResultEvents) => { 39 | const findLogResultEventBylogBetEvent = logBetEvent => ( 40 | logResultEvents.find(logResultEvent => ( 41 | logResultEvent.returnValues.BetID === logBetEvent.returnValues.BetID 42 | )) 43 | ); 44 | 45 | return logBetEvents.map(logBetEvent => ({ 46 | logBetEvent, 47 | logResultEvent: findLogResultEventBylogBetEvent(logBetEvent), 48 | })); 49 | }; 50 | 51 | class EtherollContract { 52 | constructor(web3, address) { 53 | this.web3 = web3; 54 | this.address = address; 55 | this.abi = etherollAbi; 56 | this.web3Contract = new web3.eth.Contract(etherollAbi, address); 57 | } 58 | 59 | // callback(error, result) 60 | getTransactionLogs(callback) { 61 | this.web3.eth.getBlockNumber((error, blockNumber) => { 62 | if (error) { 63 | console.log(error); 64 | } else { 65 | const { address } = this; 66 | const toBlock = blockNumber; 67 | const fromBlock = toBlock - 100; 68 | const options = { 69 | address, 70 | fromBlock, 71 | toBlock, 72 | }; 73 | this.web3Contract.getPastEvents('allEvents', options, callback); 74 | } 75 | }); 76 | } 77 | 78 | // callback(error, result) 79 | getMergedTransactionLogs(callback) { 80 | this.getTransactionLogs((error, result) => { 81 | if (error) { 82 | console.log(error); 83 | } else { 84 | const logBetEvents = result.filter(evnt => evnt.event === 'LogBet'); 85 | const logResultEvents = result.filter(evnt => evnt.event === 'LogResult'); 86 | const mergedLogs = mergeLogs(logBetEvents, logResultEvents); 87 | callback(error, mergedLogs); 88 | } 89 | }); 90 | } 91 | } 92 | 93 | 94 | export { 95 | EtherollContract, etherscanUrls, getProfit, mergeLogs, Networks, contractAddresses, 96 | }; 97 | -------------------------------------------------------------------------------- /src/components/__snapshots__/Transactions.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly on empty transactions 1`] = ` 4 |
    7 |
    10 | 28 |
    29 |
    32 |
    35 |
    36 |
    37 | `; 38 | 39 | exports[`renders correctly on not empty transactions 1`] = ` 40 |
    43 |
    46 | 64 |
    65 |
    68 |
    71 |
    74 |
    77 |

    78 | ? 79 |

    80 |
    81 |
    84 |
    87 | ? 88 |   ETH 89 |
    90 |
    93 | ? 94 |   95 | ? 96 |   97 | 51 98 |
    99 |
    102 | Wallet: 103 |   104 | 108 | 0x0123 109 | 110 |
    111 |
    114 | Transaction: 115 |   116 | 119 | 0x0123 120 | 121 |
    122 |
    123 |
    124 |
    125 |
    126 |
    127 | `; 128 | -------------------------------------------------------------------------------- /src/components/__snapshots__/CoinFlip.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | Array [ 5 |
    8 | 9 | Bet size 10 | 11 |
    14 |
    17 | 23 |
    26 | 29 | ETH 30 | 31 |
    32 |
    33 |
    36 |
    45 |
    49 |
    59 |
    62 |
    81 |
    84 |
    85 |
    86 |
    87 |
    , 88 |

    89 | Flip Head with a wager of 0.10 for a profit of 0.10 90 |

    , 91 | , 99 |
    102 |
    105 | 123 |
    124 |
    127 |
    130 |
    131 |
    , 132 | ] 133 | `; 134 | -------------------------------------------------------------------------------- /src/utils/etheroll-contract.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | mergeLogs, getProfit, EtherollContract, 3 | } from './etheroll-contract'; 4 | 5 | test('mergeLogs', () => { 6 | const logBetEvents = [ 7 | { 8 | address: '0xa52e014b3f5cc48287c2d483a3e026c32cc76e6d', 9 | transactionHash: '0x669ae171b5951986edb13497c66efd1125d8121894f4ce359c851113e47a2d4e', 10 | event: 'LogBet', 11 | returnValues: { 12 | BetID: '0x30716d2ad03c2f355e1847f3b3e4e140d2f4ea8a70af087e6198b400033c02b7', 13 | PlayerAddress: '0xd2efbb03e67ed8a17fde6cc32d1f757cffdcf49e', 14 | }, 15 | }, 16 | { 17 | address: '0xa52e014b3f5cc48287c2d483a3e026c32cc76e6d', 18 | transactionHash: '0xc611ca779f2deffe3871e7eeea81dedecf568e58bd7783352b918c4e9c74756d', 19 | event: 'LogBet', 20 | returnValues: { 21 | BetID: '0xdcb9aa58ae316160c03eaf22289eb9dc2382de26d11ba5424b7d8dc852ddd176', 22 | PlayerAddress: '0xd2efbb03e67ed8a17fde6cc32d1f757cffdcf49e', 23 | }, 24 | }, 25 | { 26 | address: '0xa52e014b3f5cc48287c2d483a3e026c32cc76e6d', 27 | transactionHash: '0x3cff8259824e17edf927fda935ba629db51b002549ccaa06f6cdb7198e94b4fc', 28 | event: 'LogBet', 29 | returnValues: { 30 | BetID: '0x2c4544b6cadc99db972cd79bc8bbb07a5dff95bfcd3166233428ca36525b1c7d', 31 | PlayerAddress: '0xd2efbb03e67ed8a17fde6cc32d1f757cffdcf49e', 32 | }, 33 | }, 34 | ]; 35 | const logResultEvents = [ 36 | { 37 | address: '0xa52e014b3f5cc48287c2d483a3e026c32cc76e6d', 38 | transactionHash: '0xb1b68a7fda0a88b306abce866a81c7ba4f42b2b02b8bdd43145535814a9b9e90', 39 | event: 'LogResult', 40 | returnValues: { 41 | BetID: '0xdcb9aa58ae316160c03eaf22289eb9dc2382de26d11ba5424b7d8dc852ddd176', 42 | PlayerAddress: '0xd2efbb03e67ed8a17fde6cc32d1f757cffdcf49e', 43 | }, 44 | }, 45 | { 46 | address: '0xa52e014b3f5cc48287c2d483a3e026c32cc76e6d', 47 | transactionHash: '0x7240c28182f2b90be5622904b76877c2ff0da8b07851926c5a3c3f05ea1d1cad', 48 | event: 'LogResult', 49 | returnValues: { 50 | BetID: '0x2c4544b6cadc99db972cd79bc8bbb07a5dff95bfcd3166233428ca36525b1c7d', 51 | PlayerAddress: '0xd2efbb03e67ed8a17fde6cc32d1f757cffdcf49e', 52 | }, 53 | }, 54 | ]; 55 | const expectedMergedLog = [ 56 | { 57 | logBetEvent: logBetEvents[0], 58 | logResultEvent: undefined, 59 | }, 60 | { 61 | logBetEvent: logBetEvents[1], 62 | logResultEvent: logResultEvents[0], 63 | }, 64 | { 65 | logBetEvent: logBetEvents[2], 66 | logResultEvent: logResultEvents[1], 67 | }, 68 | ]; 69 | const mergedLogs = mergeLogs(logBetEvents, logResultEvents); 70 | expect(mergedLogs).toEqual(expectedMergedLog); 71 | }); 72 | 73 | describe('getProfit', () => { 74 | it('computes a net profit', () => { 75 | const betSize = 10; 76 | const winningChances = 10; 77 | 78 | const expectedProfit = 89; 79 | expect(getProfit(betSize, winningChances)).toEqual(expectedProfit); 80 | }); 81 | 82 | it('never returns a negative value', () => { 83 | const betSize = 10; 84 | const winningChances = Infinity; 85 | 86 | const expectedProfit = 0; 87 | expect(getProfit(betSize, winningChances)).toEqual(expectedProfit); 88 | }); 89 | 90 | it('returns 0 when the winning chances are 0', () => { 91 | const betSize = 10; 92 | const winningChances = 0; 93 | 94 | const expectedProfit = 0; 95 | expect(getProfit(betSize, winningChances)).toEqual(expectedProfit); 96 | }); 97 | }); 98 | 99 | 100 | class MockContract { 101 | constructor(abi, address) { 102 | this.abi = abi; 103 | this.address = address; 104 | } 105 | } 106 | 107 | const mockWeb3 = () => ( 108 | { 109 | eth: { 110 | Contract: MockContract, 111 | }, 112 | } 113 | ); 114 | 115 | describe('EtherollContract', () => { 116 | it('construct with two parameters', () => { 117 | const address = '0x1234'; 118 | const web3 = mockWeb3(); 119 | const etherollContract = new EtherollContract(web3, address); 120 | expect(etherollContract.address).toEqual(address); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/components/CoinFlipTransactions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | arrayOf, func, number, shape, 4 | } from 'prop-types'; 5 | import { FormattedMessage } from 'react-intl'; 6 | import Address from './Address'; 7 | import Transaction from './Transaction'; 8 | 9 | 10 | const MergedLog = ({ network, mergedLog }) => { 11 | const { logBetEvent, logResultEvent } = mergedLog; 12 | const playerNumber = Number(logBetEvent.returnValues.PlayerNumber); 13 | let valueEth = '?'; 14 | let coinResult = '?'; 15 | let alertClass = 'secondary'; 16 | // resolved bet case 17 | if (typeof logResultEvent !== 'undefined') { 18 | const diceResult = Number(logResultEvent.returnValues.DiceResult); 19 | coinResult = diceResult < 51 ? 'Head' : 'Tail'; 20 | const playerWon = diceResult < playerNumber; 21 | valueEth = (logResultEvent.returnValues.Value * (10 ** (-18))).toFixed(2); 22 | alertClass = playerWon ? 'success' : 'danger'; 23 | } 24 | return ( 25 |
    26 |
    27 |

    {coinResult}

    28 |
    29 |
    30 |
    31 | {valueEth} 32 |   33 | ETH 34 |
    35 |
    36 | 40 |   41 |
    42 |
    43 |
    44 | 48 |   49 | 50 |
    51 |
    52 |
    53 | ); 54 | }; 55 | MergedLog.propTypes = { 56 | network: number.isRequired, 57 | mergedLog: shape({ 58 | // TODO: seems completely ignored 59 | todo: number, 60 | }).isRequired, 61 | }; 62 | 63 | const TransactionsFilterButtons = ({ onClick }) => ( 64 | 86 | ); 87 | TransactionsFilterButtons.propTypes = { 88 | onClick: func.isRequired, 89 | }; 90 | 91 | const Transactions = ({ network, onClick, transactions }) => { 92 | const coinflipTransactions = transactions.filter(transaction => ( 93 | Number(transaction.logBetEvent.returnValues.PlayerNumber) === 51 94 | )); 95 | const reversedTransactions = coinflipTransactions.slice().reverse(); 96 | const transactionsElems = reversedTransactions.map(transaction => ( 97 | 102 | )); 103 | return ( 104 |
    105 |
    106 | 107 |
    108 |
    109 |
    {transactionsElems}
    110 |
    111 |
    112 | ); 113 | }; 114 | Transactions.propTypes = { 115 | network: number.isRequired, 116 | onClick: func.isRequired, 117 | transactions: arrayOf(shape({ 118 | // TODO: seems completely ignored 119 | // https://github.com/facebook/prop-types/issues/181 120 | todo: number, 121 | })).isRequired, 122 | }; 123 | 124 | 125 | export default Transactions; 126 | -------------------------------------------------------------------------------- /src/components/Transactions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | arrayOf, func, number, shape, 4 | } from 'prop-types'; 5 | import { FormattedMessage } from 'react-intl'; 6 | import Address from './Address'; 7 | import Transaction from './Transaction'; 8 | 9 | 10 | const MergedLog = ({ network, mergedLog }) => { 11 | const { logBetEvent, logResultEvent } = mergedLog; 12 | const playerNumber = Number(logBetEvent.returnValues.PlayerNumber); 13 | let valueEth = '?'; 14 | let diceResult = '?'; 15 | let sign = '?'; 16 | let alertClass = 'secondary'; 17 | // resolved bet case 18 | if (typeof logResultEvent !== 'undefined') { 19 | diceResult = Number(logResultEvent.returnValues.DiceResult); 20 | const playerWon = diceResult < playerNumber; 21 | valueEth = (logResultEvent.returnValues.Value * (10 ** (-18))).toFixed(2); 22 | sign = playerWon ? '<' : '>'; 23 | alertClass = playerWon ? 'success' : 'danger'; 24 | } 25 | return ( 26 |
    27 |
    28 |

    {diceResult}

    29 |
    30 |
    31 |
    32 | {valueEth} 33 |   34 | ETH 35 |
    36 |
    37 | {diceResult} 38 |   39 | {sign} 40 |   41 | {playerNumber} 42 |
    43 |
    44 | 48 |   49 |
    50 |
    51 |
    52 | 56 |   57 | 58 |
    59 |
    60 |
    61 | ); 62 | }; 63 | MergedLog.propTypes = { 64 | network: number.isRequired, 65 | mergedLog: shape({ 66 | // TODO: seems completely ignored 67 | todo: number, 68 | }).isRequired, 69 | }; 70 | 71 | const TransactionsFilterButtons = ({ onClick }) => ( 72 | 94 | ); 95 | TransactionsFilterButtons.propTypes = { 96 | onClick: func.isRequired, 97 | }; 98 | 99 | const Transactions = ({ network, onClick, transactions }) => { 100 | const reversedTransactions = transactions.slice().reverse(); 101 | const transactionsElems = reversedTransactions.map(transaction => ( 102 | 107 | )); 108 | return ( 109 |
    110 |
    111 | 112 |
    113 |
    114 |
    {transactionsElems}
    115 |
    116 |
    117 | ); 118 | }; 119 | Transactions.propTypes = { 120 | network: number.isRequired, 121 | onClick: func.isRequired, 122 | transactions: arrayOf(shape({ 123 | // TODO: seems completely ignored 124 | // https://github.com/facebook/prop-types/issues/181 125 | todo: number, 126 | })).isRequired, 127 | }; 128 | 129 | 130 | export default Transactions; 131 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/utils/etheroll-abi.js: -------------------------------------------------------------------------------- 1 | const etherollAbi = [{ 2 | constant: false, inputs: [{ name: 'newCallbackGasPrice', type: 'uint256' }], name: 'ownerSetCallbackGasPrice', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 3 | }, { 4 | constant: true, inputs: [], name: 'totalWeiWon', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 5 | }, { 6 | constant: true, inputs: [], name: 'maxProfitAsPercentOfHouse', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 7 | }, { 8 | constant: false, inputs: [{ name: 'newHouseEdge', type: 'uint256' }], name: 'ownerSetHouseEdge', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 9 | }, { 10 | constant: false, inputs: [{ name: 'myid', type: 'bytes32' }, { name: 'result', type: 'string' }], name: '__callback', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 11 | }, { 12 | constant: true, inputs: [], name: 'payoutsPaused', outputs: [{ name: '', type: 'bool' }], payable: false, stateMutability: 'view', type: 'function', 13 | }, { 14 | constant: false, inputs: [{ name: 'newTreasury', type: 'address' }], name: 'ownerSetTreasury', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 15 | }, { 16 | constant: false, inputs: [{ name: 'myid', type: 'bytes32' }, { name: 'result', type: 'string' }, { name: 'proof', type: 'bytes' }], name: '__callback', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 17 | }, { 18 | constant: true, inputs: [], name: 'maxNumber', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 19 | }, { 20 | constant: true, inputs: [{ name: 'addressToCheck', type: 'address' }], name: 'playerGetPendingTxByAddress', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 21 | }, { 22 | constant: false, inputs: [{ name: 'newContractBalanceInWei', type: 'uint256' }], name: 'ownerUpdateContractBalance', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 23 | }, { 24 | constant: true, inputs: [], name: 'maxProfitDivisor', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 25 | }, { 26 | constant: false, inputs: [{ name: 'newPayoutStatus', type: 'bool' }], name: 'ownerPausePayouts', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 27 | }, { 28 | constant: false, inputs: [{ name: 'newOwner', type: 'address' }], name: 'ownerChangeOwner', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 29 | }, { 30 | constant: true, inputs: [], name: 'minNumber', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 31 | }, { 32 | constant: false, inputs: [{ name: 'newMaxProfitAsPercent', type: 'uint256' }], name: 'ownerSetMaxProfitAsPercentOfHouse', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 33 | }, { 34 | constant: true, inputs: [], name: 'treasury', outputs: [{ name: '', type: 'address' }], payable: false, stateMutability: 'view', type: 'function', 35 | }, { 36 | constant: true, inputs: [], name: 'totalWeiWagered', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 37 | }, { 38 | constant: false, inputs: [{ name: 'newMinimumBet', type: 'uint256' }], name: 'ownerSetMinBet', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 39 | }, { 40 | constant: false, inputs: [{ name: 'newStatus', type: 'bool' }], name: 'ownerPauseGame', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 41 | }, { 42 | constant: true, inputs: [], name: 'gasForOraclize', outputs: [{ name: '', type: 'uint32' }], payable: false, stateMutability: 'view', type: 'function', 43 | }, { 44 | constant: false, inputs: [{ name: 'sendTo', type: 'address' }, { name: 'amount', type: 'uint256' }], name: 'ownerTransferEther', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 45 | }, { 46 | constant: true, inputs: [], name: 'contractBalance', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 47 | }, { 48 | constant: true, inputs: [], name: 'owner', outputs: [{ name: '', type: 'address' }], payable: false, stateMutability: 'view', type: 'function', 49 | }, { 50 | constant: true, inputs: [], name: 'minBet', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 51 | }, { 52 | constant: false, inputs: [], name: 'playerWithdrawPendingTransactions', outputs: [{ name: '', type: 'bool' }], payable: false, stateMutability: 'nonpayable', type: 'function', 53 | }, { 54 | constant: true, inputs: [], name: 'maxProfit', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 55 | }, { 56 | constant: true, inputs: [], name: 'totalBets', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 57 | }, { 58 | constant: true, inputs: [], name: 'randomQueryID', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 59 | }, { 60 | constant: true, inputs: [], name: 'gamePaused', outputs: [{ name: '', type: 'bool' }], payable: false, stateMutability: 'view', type: 'function', 61 | }, { 62 | constant: false, inputs: [{ name: 'originalPlayerBetId', type: 'bytes32' }, { name: 'sendTo', type: 'address' }, { name: 'originalPlayerProfit', type: 'uint256' }, { name: 'originalPlayerBetValue', type: 'uint256' }], name: 'ownerRefundPlayer', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 63 | }, { 64 | constant: false, inputs: [{ name: 'newSafeGasToOraclize', type: 'uint32' }], name: 'ownerSetOraclizeSafeGas', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 65 | }, { 66 | constant: false, inputs: [], name: 'ownerkill', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', 67 | }, { 68 | constant: true, inputs: [], name: 'houseEdge', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 69 | }, { 70 | constant: false, inputs: [{ name: 'rollUnder', type: 'uint256' }], name: 'playerRollDice', outputs: [], payable: true, stateMutability: 'payable', type: 'function', 71 | }, { 72 | constant: true, inputs: [], name: 'houseEdgeDivisor', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 73 | }, { 74 | constant: true, inputs: [], name: 'maxPendingPayouts', outputs: [{ name: '', type: 'uint256' }], payable: false, stateMutability: 'view', type: 'function', 75 | }, { 76 | inputs: [], payable: false, stateMutability: 'nonpayable', type: 'constructor', 77 | }, { payable: true, stateMutability: 'payable', type: 'fallback' }, { 78 | anonymous: false, inputs: [{ indexed: true, name: 'BetID', type: 'bytes32' }, { indexed: true, name: 'PlayerAddress', type: 'address' }, { indexed: true, name: 'RewardValue', type: 'uint256' }, { indexed: false, name: 'ProfitValue', type: 'uint256' }, { indexed: false, name: 'BetValue', type: 'uint256' }, { indexed: false, name: 'PlayerNumber', type: 'uint256' }, { indexed: false, name: 'RandomQueryID', type: 'uint256' }], name: 'LogBet', type: 'event', 79 | }, { 80 | anonymous: false, inputs: [{ indexed: true, name: 'ResultSerialNumber', type: 'uint256' }, { indexed: true, name: 'BetID', type: 'bytes32' }, { indexed: true, name: 'PlayerAddress', type: 'address' }, { indexed: false, name: 'PlayerNumber', type: 'uint256' }, { indexed: false, name: 'DiceResult', type: 'uint256' }, { indexed: false, name: 'Value', type: 'uint256' }, { indexed: false, name: 'Status', type: 'int256' }, { indexed: false, name: 'Proof', type: 'bytes' }], name: 'LogResult', type: 'event', 81 | }, { 82 | anonymous: false, inputs: [{ indexed: true, name: 'BetID', type: 'bytes32' }, { indexed: true, name: 'PlayerAddress', type: 'address' }, { indexed: true, name: 'RefundValue', type: 'uint256' }], name: 'LogRefund', type: 'event', 83 | }, { 84 | anonymous: false, inputs: [{ indexed: true, name: 'SentToAddress', type: 'address' }, { indexed: true, name: 'AmountTransferred', type: 'uint256' }], name: 'LogOwnerTransfer', type: 'event', 85 | }]; 86 | 87 | 88 | export default etherollAbi; 89 | -------------------------------------------------------------------------------- /src/components/Container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import { fromWei } from 'web3-utils'; 5 | import Alert from './Alert'; 6 | import CoinFlip from './CoinFlip'; 7 | import ContractInfo from './ContractInfo'; 8 | import RollUnder from './RollUnder'; 9 | import BetSize from './BetSize'; 10 | import ChanceOfWinning from './ChanceOfWinning'; 11 | import MetaMaskLink from './MetaMaskLink'; 12 | import getWeb3 from '../utils/get-web3'; 13 | import { 14 | EtherollContract, Networks, contractAddresses, 15 | } from '../utils/etheroll-contract'; 16 | 17 | 18 | const showMessage = (classType, message, updateAlertDict) => { 19 | const alertDict = { classType, message }; 20 | updateAlertDict(alertDict); 21 | }; 22 | 23 | const showFetchContractInfoWarning = (showWarningMessage, optionalMessage) => { 24 | const defaultMessage = "Can't fetch contract info."; 25 | const message = (typeof optionalMessage === 'undefined') ? defaultMessage : optionalMessage; 26 | showWarningMessage(message); 27 | }; 28 | 29 | const minBetCallback = (showWarningMessage, updateValue) => (error, minBetWei) => { 30 | error ? showFetchContractInfoWarning(showWarningMessage) : ( 31 | updateValue(Number(fromWei(minBetWei, 'ether'))) 32 | ); 33 | }; 34 | 35 | const minNumberCallback = (showWarningMessage, updateValue) => (error, minNumber) => { 36 | error ? showFetchContractInfoWarning(showWarningMessage) : updateValue(minNumber - 1); 37 | }; 38 | 39 | const maxNumberCallback = (showWarningMessage, updateValue) => (error, maxNumber) => { 40 | error ? showFetchContractInfoWarning(showWarningMessage) : updateValue(maxNumber - 1); 41 | }; 42 | 43 | const getBalanceCallback = (showWarningMessage, updateValue) => (error, balance) => { 44 | // error can be null with the balance also null in rare cases 45 | (error || balance === null) ? showFetchContractInfoWarning("Can't fetch contract balance.") : ( 46 | updateValue(Number(fromWei(balance, 'ether'))) 47 | ); 48 | }; 49 | 50 | const getAccountBalanceCallback = (showWarningMessage, updateValue) => (error, balance) => { 51 | // error can be null with the balance also null in rare cases 52 | (error || balance === null) ? showWarningMessage("Can't fetch account balance.") : ( 53 | updateValue(Number(fromWei(balance, 'ether'))) 54 | ); 55 | }; 56 | 57 | const getAccountsCallback = ( 58 | web3, showWarningMessage, updateAccountAddress, updateAccountBalance, 59 | ) => (error, accounts) => { 60 | if (error) { 61 | const message = "Can't retrieve accounts."; 62 | showWarningMessage(message); 63 | } else { 64 | const accountAddress = accounts.length === 0 ? null : accounts[0]; 65 | if (accountAddress !== null) { 66 | web3.eth.getBalance( 67 | accountAddress, 68 | getAccountBalanceCallback( 69 | showWarningMessage, 70 | updateAccountBalance, 71 | ), 72 | ); 73 | } 74 | updateAccountAddress(accountAddress); 75 | } 76 | }; 77 | 78 | const filterTransactions = ( 79 | accountAddress, transactionsFilter, allTransactions, 80 | updateFilteredTransactions, updateTransactionsFilter, 81 | ) => { 82 | let filteredTransactions = allTransactions.slice(); 83 | if (transactionsFilter === '#my-transactions') { 84 | filteredTransactions = allTransactions.filter(transaction => ( 85 | transaction.logBetEvent.returnValues.PlayerAddress.toLowerCase() 86 | === accountAddress.toLowerCase() 87 | )); 88 | } 89 | updateFilteredTransactions(filteredTransactions); 90 | updateTransactionsFilter(transactionsFilter); 91 | }; 92 | 93 | const getTransactions = ( 94 | contract, accountAddress, transactionsFilter, 95 | updateAllTransactions, updateFilteredTransactions, updateTransactionsFilter, 96 | ) => { 97 | contract.getMergedTransactionLogs((error, result) => { 98 | if (error) { 99 | console.log(error); 100 | } else { 101 | const allTransactions = result; 102 | updateAllTransactions(allTransactions); 103 | filterTransactions( 104 | accountAddress, transactionsFilter, allTransactions, 105 | updateFilteredTransactions, updateTransactionsFilter, 106 | ); 107 | } 108 | }); 109 | }; 110 | 111 | class Container extends React.Component { 112 | constructor(props) { 113 | super(props); 114 | this.state = { 115 | alertDict: {}, 116 | betSize: 0.1, 117 | chances: 50, 118 | minBet: BetSize.defaultProps.min, 119 | maxBet: BetSize.defaultProps.max, 120 | minChances: ChanceOfWinning.defaultProps.min, 121 | maxChances: ChanceOfWinning.defaultProps.max, 122 | accountAddress: null, 123 | accountBalance: 0, 124 | network: Networks.mainnet, 125 | contract: null, 126 | contractAddress: contractAddresses[Networks.mainnet], 127 | contractBalance: 0, 128 | // most recent transaction is last in the array 129 | allTransactions: [], 130 | filteredTransactions: [], 131 | transactionsFilter: '#all-transactions', 132 | }; 133 | this.onWeb3 = this.onWeb3.bind(this); 134 | this.updateState = this.updateState.bind(this); 135 | this.initWeb3(); 136 | } 137 | 138 | componentWillUnmount() { 139 | clearInterval(this.getTransactionsIntervalId); 140 | } 141 | 142 | /** 143 | * Retrieves web3 and contract info, then sets the following states: 144 | * - accountAddress 145 | * - accountBalance 146 | * - contract 147 | * - contractAddress 148 | * - contractBalance 149 | * - minBet 150 | * - maxBet (TODO) 151 | * - maxChances 152 | * - network 153 | */ 154 | onWeb3(web3) { 155 | const getIdCallback = (network) => { 156 | const contractAddress = contractAddresses[network]; 157 | const contract = new EtherollContract(web3, contractAddress); 158 | const pullIntervalSeconds = 10 * 1000; 159 | const { showWarningMessage, updateState } = this; 160 | const { transactionsFilter, accountAddress } = this.state; 161 | const getTransactionsAlias = () => getTransactions( 162 | contract, accountAddress, transactionsFilter, 163 | updateState('allTransactions'), updateState('filteredTransactions'), updateState('transactionsFilter'), 164 | ); 165 | // clearInterval() is in the componentWillUnmount() 166 | this.getTransactionsIntervalId = setInterval( 167 | () => getTransactionsAlias(), pullIntervalSeconds, 168 | ); 169 | getTransactionsAlias(); 170 | this.setState({ 171 | network, 172 | contract, 173 | contractAddress, 174 | }); 175 | contract.web3Contract.methods.minBet().call( 176 | minBetCallback( 177 | showWarningMessage, updateState('minBet'), 178 | ), 179 | ); 180 | contract.web3Contract.methods.minNumber().call( 181 | minNumberCallback( 182 | showWarningMessage, updateState('minChances'), 183 | ), 184 | ); 185 | contract.web3Contract.methods.maxNumber().call( 186 | maxNumberCallback( 187 | showWarningMessage, updateState('maxChances'), 188 | ), 189 | ); 190 | web3.eth.getBalance( 191 | contractAddress, 192 | getBalanceCallback( 193 | showWarningMessage, updateState('contractBalance'), 194 | ), 195 | ); 196 | web3.eth.getAccounts( 197 | getAccountsCallback( 198 | web3, showWarningMessage, updateState('accountAddress'), updateState('accountBalance'), 199 | ), 200 | ); 201 | }; 202 | web3.eth.net.getId().then(getIdCallback); 203 | } 204 | 205 | initWeb3() { 206 | const getWeb3CallbackOk = ({ web3 }) => { 207 | this.onWeb3(web3); 208 | }; 209 | const getWeb3CallbackError = () => { 210 | const classType = 'danger'; 211 | const message = ( 212 | }} 216 | /> 217 | ); 218 | showMessage(classType, message, this.updateState('alertDict')); 219 | }; 220 | getWeb3.then(getWeb3CallbackOk, getWeb3CallbackError); 221 | } 222 | 223 | showWarningMessage(message) { 224 | const classType = 'warning'; 225 | showMessage(classType, message, this.updateState('alertDict')); 226 | } 227 | 228 | updateState(key) { 229 | return (value) => { 230 | this.setState({ [key]: value }); 231 | }; 232 | } 233 | 234 | render() { 235 | const { 236 | alertDict, accountAddress, accountBalance, allTransactions, betSize, chances, contract, 237 | contractAddress, contractBalance, filteredTransactions, maxBet, minBet, maxChances, 238 | minChances, network, transactionsFilter, 239 | } = this.state; 240 | 241 | const gameProps = { 242 | accountAddress, 243 | betSize, 244 | chances, 245 | contract, 246 | filteredTransactions, 247 | transactionsFilter, 248 | maxBet, 249 | minBet, 250 | maxChances, 251 | minChances, 252 | network, 253 | updateState: this.updateState, 254 | filterTransactions: filter => filterTransactions( 255 | accountAddress, filter, allTransactions, 256 | this.updateState('filteredTransactions'), this.updateState('transactionsFilter'), 257 | ), 258 | }; 259 | const contractProps = { 260 | accountAddress, accountBalance, contractAddress, contractBalance, network, 261 | }; 262 | 263 | return ( 264 |
    265 | 266 | 267 |

    268 | 273 |

    274 | } /> 275 | } /> 276 |
    277 | ); 278 | } 279 | } 280 | 281 | export default Container; 282 | --------------------------------------------------------------------------------