.
15 | Then commit and finish release.
16 | ```sh
17 | git commit -a -m "YYYY.MM.DD"
18 | git flow release finish
19 | ```
20 | Push everything, make sure tags are also pushed:
21 | ```sh
22 | git push
23 | git push origin master:master
24 | git push --tags
25 | ```
26 |
27 | ## Publish to GitHub Pages
28 | Publish to https://andremiras.github.io/etheroll via:
29 | ```sh
30 | make deploy
31 | ```
32 |
33 | ## Update GitHub release
34 | Got to GitHub [Release/Tags](https://github.com/AndreMiras/etheroll/tags), click "Add release notes" for the tag just created.
35 | Add the tag name in the "Release title" field and the relevant CHANGELOG.md section in the "Describe this release" textarea field.
36 | Click "Publish release".
37 |
--------------------------------------------------------------------------------
/src/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "betsize.betsize": "Bet size",
3 | "chanceofwinning.chanceofwinning": "Chance of winning",
4 | "coinfliprecap.flip-head-with-a-wager": "Flip Head with a wager of {betSize} for a profit of {profit}",
5 | "container.no-account-connected": "No account connected, connect with a Web3-compatible wallet like {metamaskLink}",
6 | "container.place-your-bet": "Place your bet",
7 | "contractinfo.account": "Account ({accountBalance} ETH)",
8 | "contractinfo.contract": "Contract ({contractBalance} ETH)",
9 | "contractinfo.not-connected": "Not connected, please login to MetaMask",
10 | "flipbutton.text": "Flip Head",
11 | "headers.navsections.navlink.about": "About",
12 | "headers.navsections.navlink.coin-flip": "Flip a coin",
13 | "headers.navsections.navlink.home": "Home",
14 | "merged-log.transaction": "Transaction:",
15 | "merged-log.wallet": "Wallet:",
16 | "rollunderrecap.for-a-profit-of": "For a profit of",
17 | "rollunderrecap.roll-under": "Roll under",
18 | "rollunderrecap.with-a-wager-of": "With a wager of",
19 | "transactions-filter-buttons.all-transactions": "All transactions",
20 | "transactions-filter-buttons.my-transactions": "My transactions"
21 | }
22 |
--------------------------------------------------------------------------------
/src/translations/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "betsize.betsize": "Размер ставки",
3 | "chanceofwinning.chanceofwinning": "Вероятность выигрыша",
4 | "coinfliprecap.flip-head-with-a-wager": "Ставка {betSize}; Орёл выигрывает {profit}",
5 | "container.no-account-connected": "Вы не подключены, зайти с помощью кошелька, совместимого с Web3 {metamaskLink}",
6 | "container.place-your-bet": "Сделать ставку",
7 | "contractinfo.account": "Счёт ({accountBalance} ETH)",
8 | "contractinfo.contract": "Контракт ({contractBalance} ETH)",
9 | "contractinfo.not-connected": "Вы не подключены, зайдите с помощью MetaMask",
10 | "flipbutton.text": "Подбросить на Орла",
11 | "headers.navsections.navlink.about": "О нас",
12 | "headers.navsections.navlink.coin-flip": "Подбросить монету",
13 | "headers.navsections.navlink.home": "Главная",
14 | "merged-log.transaction": "Операция:",
15 | "merged-log.wallet": "Кошелёк:",
16 | "rollunderrecap.for-a-profit-of": "Выигрыш",
17 | "rollunderrecap.roll-under": "Надо получить меньше, чем",
18 | "rollunderrecap.with-a-wager-of": "Ставка",
19 | "transactions-filter-buttons.all-transactions": "Все операции",
20 | "transactions-filter-buttons.my-transactions": "Мои операции"
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/src/translations/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "betsize.betsize": "Montant du pari",
3 | "chanceofwinning.chanceofwinning": "Probabilité de gagner",
4 | "coinfliprecap.flip-head-with-a-wager": "Mise de {betSize} sur Face pour un profit de {profit}",
5 | "container.no-account-connected": "Aucun compte connecté, connectez vous avec un wallet compatible Web3 tel que {metamaskLink}",
6 | "container.place-your-bet": "Placez votre pari",
7 | "contractinfo.account": "Compte ({accountBalance} ETH)",
8 | "contractinfo.contract": "Contrat ({contractBalance} ETH)",
9 | "contractinfo.not-connected": "Non connecté, connectez-vous avec MetaMask",
10 | "flipbutton.text": "",
11 | "headers.navsections.navlink.about": "À propos",
12 | "headers.navsections.navlink.coin-flip": "Pile ou Face",
13 | "headers.navsections.navlink.home": "",
14 | "merged-log.transaction": "Transaction :",
15 | "merged-log.wallet": "Compte :",
16 | "rollunderrecap.for-a-profit-of": "Pour un profit de",
17 | "rollunderrecap.roll-under": "",
18 | "rollunderrecap.with-a-wager-of": "Montant du pari",
19 | "transactions-filter-buttons.all-transactions": "Toutes les transactions",
20 | "transactions-filter-buttons.my-transactions": "Mes transactions"
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/FlipButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { bool, func, string } from 'prop-types';
3 | import { defineMessages, useIntl } from 'react-intl';
4 |
5 | const Button = ({ isDisabled, onClick, text }) => (
6 |
12 | {text}
13 |
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 | );
35 | };
36 | FlipButton.propTypes = {
37 | isDisabled: bool,
38 | onClick: func.isRequired,
39 | };
40 | FlipButton.defaultProps = {
41 | isDisabled: false,
42 | };
43 |
44 | export default FlipButton;
45 |
--------------------------------------------------------------------------------
/src/translations/vn.json:
--------------------------------------------------------------------------------
1 | {
2 | "betsize.betsize": "Tiền đặt cược",
3 | "chanceofwinning.chanceofwinning": "Cơ hội thắng cược",
4 | "coinfliprecap.flip-head-with-a-wager": "Đặc cược (50/50) với số tiền {betSize} tiền lời là {profit}",
5 | "container.no-account-connected": "Ví chưa được kết nối, hãy dùng một ví tương thích với Web3. Ví dụ: {metamaskLink}",
6 | "container.place-your-bet": "Đặt cược",
7 | "contractinfo.account": "Số tiền bạn đang có ({accountBalance} ETH)",
8 | "contractinfo.contract": "Số tiền nhà cái đang có ({contractBalance} ETH)",
9 | "contractinfo.not-connected": "Ví chưa được kết nối, hãy đăng nhập vào ví MetaMask",
10 | "flipbutton.text": "Cược 50/50",
11 | "headers.navsections.navlink.about": "Giới thiệu",
12 | "headers.navsections.navlink.coin-flip": "Cược 50/50",
13 | "headers.navsections.navlink.home": "Trang chính",
14 | "merged-log.transaction": "Thông tin về lần quay:",
15 | "merged-log.wallet": "Ví ETH:",
16 | "rollunderrecap.for-a-profit-of": "Số tiền lời sẽ là",
17 | "rollunderrecap.roll-under": "Quay dưới số",
18 | "rollunderrecap.with-a-wager-of": "Với số tiền cược là",
19 | "transactions-filter-buttons.all-transactions": "Tất cả các lần quay",
20 | "transactions-filter-buttons.my-transactions": "Các lần quay của tôi"
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Etheroll ReactJS
2 |
3 | [](https://travis-ci.org/AndreMiras/etheroll)
4 |
5 | * Production:
6 | * Staging:
7 |
8 | Experimental project running an alternative [Etheroll](http://etheroll.com) frontend on [ReactJS](https://reactjs.org).
9 | If you're looking for the mobile app instead, see [EtherollApp](https://github.com/AndreMiras/EtherollApp).
10 |
11 | ## Closed down
12 | The upstream project/smart-contract closed down.
13 |
14 |
15 | ## Run
16 | ```sh
17 | make start
18 | ```
19 |
20 | ## Install
21 | ```sh
22 | make
23 | ```
24 |
25 | ## Test
26 | ```sh
27 | make lint
28 | make test
29 | ```
30 |
31 | ## Docker
32 | We provide a [Dockerfile](Dockerfile) that can be used for development or production.
33 | Build and run with:
34 | ```sh
35 | docker-compose up
36 | ```
37 | The application will be served on both port `80` (default HTTP) and `3000` (default Node.js port).
38 | Find out more reading the [docker-compose.yml](docker-compose.yml) file.
39 |
40 | ## Deployment
41 | The app can be deployed on GitHub pages when releasing via:
42 | ```sh
43 | make deploy
44 | ```
45 | It can also be deployed on Heroku for staging:
46 | ```
47 | git push heroku develop:master
48 | ```
49 |
--------------------------------------------------------------------------------
/src/components/ValueSlider.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Slider from 'rc-slider/lib/Slider';
3 | import { func, number, string } from 'prop-types';
4 | import 'rc-slider/assets/index.css';
5 |
6 |
7 | const ValueSlider = ({
8 | value, updateValue, step, min, max, addonText, toFixedDigits,
9 | }) => {
10 | const addon = (addonText !== null) ? (
11 |
12 | {addonText}
13 |
14 | ) : null;
15 | const formattedValue = toFixedDigits === null ? value : value.toFixed(toFixedDigits);
16 | return (
17 |
18 |
19 | updateValue(Number(e.target.value))}
23 | value={formattedValue}
24 | />
25 | {addon}
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 | ValueSlider.propTypes = {
34 | value: number.isRequired,
35 | updateValue: func.isRequired,
36 | step: number,
37 | min: number,
38 | max: number,
39 | addonText: string,
40 | toFixedDigits: number,
41 | };
42 | ValueSlider.defaultProps = {
43 | step: 1,
44 | min: 0,
45 | max: 100,
46 | addonText: null,
47 | toFixedDigits: null,
48 | };
49 |
50 | export default ValueSlider;
51 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Docker image for installing dependencies & running tests.
2 | # Build with:
3 | # docker build --tag=andremiras/etheroll-js .
4 | # Run with:
5 | # docker run andremiras/etheroll-js /bin/sh -c 'make test CI=1'
6 | # Or for interactive shell:
7 | # docker run -it --rm andremiras/etheroll-js
8 | FROM ubuntu:18.04 AS base
9 |
10 | # install dependencies and configure locale
11 | RUN apt update -qq > /dev/null && apt --yes install --no-install-recommends \
12 | build-essential \
13 | ca-certificates \
14 | curl \
15 | git \
16 | gnupg \
17 | locales \
18 | make \
19 | nodejs \
20 | python3 \
21 | && locale-gen en_US.UTF-8 \
22 | && apt --yes autoremove && apt --yes clean
23 |
24 | ENV LANG="en_US.UTF-8" \
25 | LANGUAGE="en_US.UTF-8" \
26 | LC_ALL="en_US.UTF-8"
27 |
28 | # install yarn
29 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
30 | && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
31 | && apt update -qq > /dev/null && apt --yes install --no-install-recommends yarn
32 |
33 | WORKDIR /app
34 | COPY . /app
35 |
36 | FROM base AS full
37 | RUN make && yarn build-staging
38 |
39 | # prod environment
40 | FROM nginx:1.17.10 AS prod
41 | COPY default.conf.template /etc/nginx/conf.d/default.conf.template
42 | COPY --from=full /app/build /usr/share/nginx/html
43 | CMD /bin/bash -c "envsubst '\$PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf" && nginx -g 'daemon off;'
44 |
--------------------------------------------------------------------------------
/src/components/__snapshots__/LanguageUpdate.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders correctly 1`] = `
4 |
7 |
15 |
18 |
19 | EN
20 |
21 |
25 |
30 | en
31 |
32 |
37 | cn
38 |
39 |
44 | es
45 |
46 |
51 | fr
52 |
53 |
58 | ru
59 |
60 |
65 | vn
66 |
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 | onClick(text)}>{text}
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 |
37 |
38 |
39 | {locale.toUpperCase()}
40 |
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 |
26 | You need to enable JavaScript to run this app.
27 |
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 |
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 |
23 |
24 |
25 | );
26 |
27 | const NavSections = () => (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
38 | (current)
39 |
40 |
41 |
42 |
43 |
44 |
45 |
49 |
50 |
51 |
52 |
58 |
59 |
60 |
64 |
65 |
66 |
67 |
68 |
69 | );
70 |
71 | const Headers = () => (
72 |
73 |
74 |
75 |
76 |
77 |
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 |
13 |
18 | All transactions
19 |
20 |
25 | My transactions
26 |
27 |
28 |
29 |
36 |
37 | `;
38 |
39 | exports[`renders correctly on not empty transactions 1`] = `
40 |
43 |
46 |
49 |
54 | All transactions
55 |
56 |
61 | My transactions
62 |
63 |
64 |
65 |
68 |
71 |
74 |
77 |
78 | ?
79 |
80 |
81 |
84 |
87 | ?
88 | ETH
89 |
90 |
93 | ?
94 |
95 | ?
96 |
97 | 51
98 |
99 |
111 |
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 |
97 | Flip Head
98 | ,
99 |
102 |
105 |
108 |
113 | All transactions
114 |
115 |
120 | My transactions
121 |
122 |
123 |
124 |
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 |
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 |
65 | onClick('#all-transactions')}
69 | >
70 |
74 |
75 | onClick('#my-transactions')}
79 | >
80 |
84 |
85 |
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 |
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 |
73 | onClick('#all-transactions')}
77 | >
78 |
82 |
83 | onClick('#my-transactions')}
87 | >
88 |
92 |
93 |
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 |
--------------------------------------------------------------------------------