├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── contracts
├── EtherEx.yaml
├── etherex.se
├── etx.se
└── namereg.se
├── dev_requirements.txt
├── frontend
├── .eslintignore
├── .eslintrc
├── .gitignore
├── Gruntfile.js
├── app
│ ├── actions
│ │ ├── ConfigActions.js
│ │ ├── MarketActions.js
│ │ ├── NetworkActions.js
│ │ ├── TradeActions.js
│ │ ├── UserActions.js
│ │ └── btcswap
│ │ │ └── TicketActions.js
│ ├── app.jsx
│ ├── clients
│ │ └── EthereumClient.js
│ ├── components
│ │ ├── AlertDismissable.jsx
│ │ ├── AlertModal.jsx
│ │ ├── Balance.jsx
│ │ ├── BalanceSub.jsx
│ │ ├── Chat.jsx
│ │ ├── Clipboard.jsx
│ │ ├── ConfigPane.jsx
│ │ ├── ConfirmModal.jsx
│ │ ├── EtherExApp.jsx
│ │ ├── Favicon.jsx
│ │ ├── GraphPrice.jsx
│ │ ├── GraphPriceTechan.jsx
│ │ ├── Help.jsx
│ │ ├── LastPrice.jsx
│ │ ├── LoadingModal.jsx
│ │ ├── MarketList.jsx
│ │ ├── MarketRow.jsx
│ │ ├── MarketSelect.jsx
│ │ ├── MarketTable.jsx
│ │ ├── Markets.jsx
│ │ ├── NavBar.jsx
│ │ ├── Network.jsx
│ │ ├── Placeholder.jsx
│ │ ├── RangeSelect.jsx
│ │ ├── SendEther.jsx
│ │ ├── SubDeposit.jsx
│ │ ├── SubDepositModal.jsx
│ │ ├── SubNavBar.jsx
│ │ ├── SubRegister.jsx
│ │ ├── SubSend.jsx
│ │ ├── SubTab.jsx
│ │ ├── SubWithdraw.jsx
│ │ ├── Tools.jsx
│ │ ├── TradeForm.jsx
│ │ ├── TradeFormInstance.jsx
│ │ ├── TradeList.jsx
│ │ ├── TradeListBuys.jsx
│ │ ├── TradeListSells.jsx
│ │ ├── TradeRow.jsx
│ │ ├── TradeTable.jsx
│ │ ├── Trades.jsx
│ │ ├── TransitionGroup.jsx
│ │ ├── TransitionGroupChild.jsx
│ │ ├── TxRow.jsx
│ │ ├── TxsList.jsx
│ │ ├── TxsTable.jsx
│ │ ├── UserAddress.jsx
│ │ ├── UserBalances.jsx
│ │ ├── UserDetails.jsx
│ │ ├── UserLink.jsx
│ │ ├── Wallet.jsx
│ │ └── btcswap
│ │ │ ├── Blocks.jsx
│ │ │ ├── ClaimTicket.jsx
│ │ │ ├── CreateTicket.jsx
│ │ │ ├── Help.jsx
│ │ │ ├── Nav.jsx
│ │ │ ├── ReserveTicket.jsx
│ │ │ ├── TicketDetails.jsx
│ │ │ ├── TicketRow.jsx
│ │ │ └── Tickets.jsx
│ ├── css
│ │ ├── assets
│ │ │ └── logo-ex-orange.jpg
│ │ ├── bootstrap-darkly.css
│ │ ├── bootstrap-flatly.css
│ │ ├── bootstrap-superhero.css
│ │ ├── fonts.css
│ │ ├── fonts
│ │ │ ├── crypto.eot
│ │ │ ├── crypto.svg
│ │ │ ├── crypto.ttf
│ │ │ ├── crypto.woff
│ │ │ ├── lato-bold-webfont.eot
│ │ │ ├── lato-bold-webfont.svg
│ │ │ ├── lato-bold-webfont.ttf
│ │ │ ├── lato-bold-webfont.woff
│ │ │ ├── lato-bold-webfont.woff2
│ │ │ ├── lato-italic-webfont.eot
│ │ │ ├── lato-italic-webfont.svg
│ │ │ ├── lato-italic-webfont.ttf
│ │ │ ├── lato-italic-webfont.woff
│ │ │ ├── lato-italic-webfont.woff2
│ │ │ ├── lato-regular-webfont.eot
│ │ │ ├── lato-regular-webfont.svg
│ │ │ ├── lato-regular-webfont.ttf
│ │ │ ├── lato-regular-webfont.woff
│ │ │ └── lato-regular-webfont.woff2
│ │ ├── icons.css
│ │ └── styles.less
│ ├── favicon.ico
│ ├── index.html
│ ├── js
│ │ ├── abi.js
│ │ ├── abi
│ │ │ ├── etherex.js
│ │ │ └── sub.js
│ │ ├── constants.js
│ │ ├── fixtures.js
│ │ ├── intlData.js
│ │ ├── jsx_preprocessor.js
│ │ ├── units.js
│ │ └── utils.js
│ ├── stores
│ │ ├── ConfigStore.js
│ │ ├── MarketStore.js
│ │ ├── NetworkStore.js
│ │ ├── TradeStore.js
│ │ ├── UserStore.js
│ │ └── btcswap
│ │ │ └── TicketStore.js
│ └── tests
│ │ └── app.spec.jsx
├── karma.conf.js
├── package.json
├── screenshot.png
├── tests.karma.js
├── update_abi.py
└── webpack.config.js
├── setup.cfg
├── setup.py
└── tests
├── integration-user.py
├── integration.py
├── test_etherex.py
└── test_etx.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[co]
2 | __pycache__/
3 | *~
4 | [#]*[#]
5 | .*.swp
6 | .*.swo
7 | .*.swn
8 | .~
9 | .DS_Store
10 | /tmp/
11 | /.venv/
12 | /dist/
13 | /*.egg-info/
14 | /.tox/
15 | /bin/
16 | /develop-eggs/
17 | /eggs/
18 | .installed.cfg
19 | logging.conf
20 | *.log
21 | .coverage
22 | *.binary
23 | MANIFEST
24 | upload
25 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "5"
4 | before_install:
5 | - cd frontend && npm install
6 | - npm install -g grunt-cli
7 | install:
8 | - cd .. && pip install --user -r dev_requirements.txt
9 | - pip install --user -e .
10 | script:
11 | - py.test -vvrs
12 | - flake8
13 | after_script:
14 | - cd frontend && grunt eslint
15 | notifications:
16 | irc:
17 | channels:
18 | - "chat.freenode.net#etherex-dev"
19 | use_notice: true
20 | skip_join: true
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/contracts/etx.se:
--------------------------------------------------------------------------------
1 | # etx.se -- Ethereum contract
2 | #
3 | # Copyright (c) 2014-2015 EtherEx
4 | #
5 | # This software may be modified and distributed under the terms
6 | # of the MIT license. See the LICENSE file for details.
7 |
8 | #
9 | # Example ETX subcurrency
10 | #
11 |
12 | data created
13 | data accounts[2^160](balance, custodians[2^160](maxValue))
14 |
15 | event Transfer(_from:indexed, _to:indexed, _value)
16 | event Approval(_owner:indexed, _spender:indexed, _value)
17 |
18 | # Boolean success/failure
19 | macro SUCCESS: 1
20 | macro FAILURE: 0
21 |
22 | def init():
23 | self.created = block.timestamp
24 | self.accounts[msg.sender].balance = 1000000 * 10 ** 5
25 |
26 | def transfer(_to, _value):
27 | # Prevent negative send from stealing funds
28 | if _to <= 0 or _value <= 0:
29 | return(FAILURE)
30 |
31 | # Get user balance
32 | balance = self.accounts[msg.sender].balance
33 |
34 | # Make sure balance is above or equal to amount
35 | if balance >= _value:
36 |
37 | # Update balances
38 | self.accounts[msg.sender].balance = balance - _value
39 | self.accounts[_to].balance += _value
40 |
41 | log(type=Transfer, msg.sender, _to, _value)
42 |
43 | return(SUCCESS)
44 | return(FAILURE)
45 |
46 | def transferFrom(_from, _to, _value):
47 | # Prevent negative send from stealing funds
48 | if _from <= 0 or _value <= 0 or _to <= 0:
49 | return(FAILURE)
50 |
51 | # Get user balance
52 | balance = self.accounts[_from].balance
53 |
54 | # Make sure balance is above or equal to amount, and transfer is approved
55 | if balance >= _value and _value <= self.accounts[_from].custodians[msg.sender].maxValue:
56 | # Update balances
57 | self.accounts[_from].balance = balance - _value
58 | self.accounts[_to].balance += _value
59 |
60 | # Update approval
61 | self.accounts[_from].custodians[msg.sender].maxValue -= _value
62 |
63 | log(type=Transfer, _from, _to, _value)
64 |
65 | return(SUCCESS)
66 | return(FAILURE)
67 |
68 | def balance():
69 | return(self.accounts[msg.sender].balance)
70 |
71 | def balanceOf(_address):
72 | return(self.accounts[_address].balance)
73 |
74 | def approve(_spender, _value):
75 | if _spender <= 0 or _value < 0:
76 | return(FAILURE)
77 | self.accounts[msg.sender].custodians[_spender].maxValue = _value
78 | log(type=Approval, msg.sender, _spender, _value)
79 | return(SUCCESS)
80 |
81 | # Returns the amount which _spender is still allowed to transfer from _address
82 | def allowance(_address, _spender):
83 | return(self.accounts[_address].custodians[_spender].maxValue)
84 |
--------------------------------------------------------------------------------
/contracts/namereg.se:
--------------------------------------------------------------------------------
1 | # namereg.se -- Ethereum contract
2 | #
3 | # Copyright (c) 2014-2015 EtherEx
4 | #
5 | # This software may be modified and distributed under the terms
6 | # of the MIT license. See the LICENSE file for details.
7 |
8 | #
9 | # Example NameReg contract
10 | #
11 |
12 | def register(key, value):
13 | if !self.storage[key] or key == msg.sender:
14 | self.storage[key] = value
15 | self.storage[value] = key
16 | return(1)
17 | return(0)
18 |
19 | def unregister(key):
20 | if key == msg.sender:
21 | value = self.storage[key]
22 | self.storage[key] = 0
23 | self.storage[value] = 0
24 | return(1)
25 | return(0)
26 |
27 | def getname(key):
28 | return(self.storage[key])
29 |
--------------------------------------------------------------------------------
/dev_requirements.txt:
--------------------------------------------------------------------------------
1 | # https://github.com/ethereum/pyethereum/tarball/develop
2 | ethereum
3 | https://github.com/ethereum/serpent/tarball/develop
4 | # ethereum-serpent
5 | pyepm
6 | flake8
7 | pytest
8 | pytest-xdist
9 |
--------------------------------------------------------------------------------
/frontend/.eslintignore:
--------------------------------------------------------------------------------
1 | app/app.js
2 |
--------------------------------------------------------------------------------
/frontend/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 6,
4 | "ecmaFeatures": {
5 | "jsx": true
6 | },
7 | "sourceType": "module"
8 | },
9 | "env": {
10 | "browser": true,
11 | "es6": true,
12 | "mocha": true,
13 | "node": true
14 | },
15 | "plugins": [
16 | "react"
17 | ],
18 | "settings": {
19 | "react": {
20 | "pragma": "React"
21 | }
22 | },
23 | "extends": ["eslint:recommended", "plugin:react/recommended"],
24 | "rules": {
25 | "curly": 0,
26 | "eqeqeq": 0,
27 | "new-cap": 0,
28 | "quotes": 0,
29 | "jsx-quotes": 0,
30 | "strict": 0,
31 | "no-console": 0,
32 | "no-underscore-dangle": 0,
33 | "react/display-name": 0,
34 | "react/jsx-boolean-value": 0,
35 | "react/jsx-no-undef": 1,
36 | "react/jsx-quotes": 0,
37 | "react/jsx-sort-prop-types": 0,
38 | "react/jsx-sort-props": 0,
39 | "react/jsx-uses-react": 1,
40 | "react/jsx-uses-vars": 1,
41 | "react/no-danger": 1,
42 | "react/no-did-mount-set-state": 1,
43 | "react/no-did-update-set-state": 1,
44 | "react/no-multi-comp": 1,
45 | "react/no-unknown-property": 1,
46 | "react/prop-types": 0,
47 | "react/react-in-jsx-scope": 1,
48 | "react/require-extension": 1,
49 | "react/self-closing-comp": 1,
50 | "react/sort-comp": 1,
51 | "react/wrap-multilines": 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | app/app.js
3 | app/app.js.map
4 | app/*.svg
5 | app/*.woff
6 | app/*.woff2
7 | app/*.eot
8 | app/*.ttf
9 | app/*.jpg
10 | app/*.png
11 | .grunt/
12 |
--------------------------------------------------------------------------------
/frontend/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | var webpack = require("webpack");
3 | var webpackConfig = require("./webpack.config.js");
4 |
5 | grunt.initConfig({
6 | pkg: grunt.file.readJSON('package.json'),
7 |
8 | webpack: {
9 | options: webpackConfig,
10 | build: {
11 | plugins: webpackConfig.plugins.concat(
12 | new webpack.DefinePlugin({
13 | "process.env": {
14 | // This has effect on the react lib size
15 | "NODE_ENV": JSON.stringify("production")
16 | }
17 | }),
18 | new webpack.optimize.DedupePlugin(),
19 | new webpack.optimize.UglifyJsPlugin({
20 | mangle: {
21 | except: ["Array", "BigInteger", "Boolean", "Buffer", "ECPair", "Function", "Number", "Point", "Script"]
22 | }
23 | })
24 | )
25 | },
26 | "build-dev": {
27 | devtool: "sourcemap",
28 | debug: true
29 | }
30 | },
31 | "webpack-dev-server": {
32 | options: {
33 | webpack: webpackConfig
34 | },
35 | start: {
36 | keepAlive: true,
37 | port: 8089,
38 | contentBase: "app",
39 | hot: true,
40 | webpack: {
41 | devtool: "eval",
42 | debug: true,
43 | entry: webpackConfig.entry.concat(
44 | "webpack-dev-server/client?http://localhost:8089",
45 | "webpack/hot/dev-server"
46 | ),
47 | plugins: webpackConfig.plugins.concat(
48 | new webpack.HotModuleReplacementPlugin()
49 | )
50 | }
51 | }
52 | },
53 | eslint: {
54 | options: {
55 | configFile: '.eslintrc'
56 | },
57 | target: ['Gruntfile.js', 'webpack.config.js', 'app/**/*.js', 'app/**/*.jsx']
58 | },
59 | karma: {
60 | options: {
61 | configFile: 'karma.conf.js'
62 | },
63 | unit: {
64 | singleRun: true
65 | },
66 | continuous: {
67 | singleRun: false
68 | // browsers: ['PhantomJS']
69 | // background: true
70 | }
71 | },
72 | watch: {
73 | app: {
74 | files: ["app/**/*.js", "app/**/*.jsx", "app/**/*.css"],
75 | tasks: ["eslint", "webpack:build-dev"],
76 | options: {
77 | spawn: false
78 | }
79 | }
80 | },
81 | "gh-pages": {
82 | options: {
83 | base: 'app',
84 | repo: 'git@github.com:etherex/etherex.git'
85 | },
86 | src: ['index.html', 'app.js', '*.svg', '*.woff', '*.woff2', '*.eot', '*.ttf', '*.ico', '*.jpg', '*.png']
87 | },
88 | clean: ["app/*.svg", "app/*.woff", "app/*.woff2", "app/*.eot", "app/*.ttf", "app/app.js", "app/*.js.map", "app/*.jpg", "app/*.png"]
89 | });
90 |
91 | grunt.loadNpmTasks('grunt-contrib-clean');
92 | grunt.loadNpmTasks('grunt-contrib-watch');
93 | grunt.loadNpmTasks('grunt-eslint');
94 | grunt.loadNpmTasks('grunt-karma');
95 | grunt.loadNpmTasks('grunt-webpack');
96 | grunt.loadNpmTasks('grunt-gh-pages');
97 |
98 | grunt.registerTask("default", ["webpack-dev-server:start"]);
99 | grunt.registerTask("dev", ["eslint", "webpack:build-dev", "watch:app"]);
100 | grunt.registerTask("build", ["clean", "eslint", "webpack:build"]);
101 | grunt.registerTask("publish", ["build", "gh-pages"]);
102 | grunt.registerTask("lint", ["eslint"]);
103 | };
104 |
--------------------------------------------------------------------------------
/frontend/app/actions/UserActions.js:
--------------------------------------------------------------------------------
1 | var _ = require("lodash");
2 | import utils from '../js/utils';
3 | import bigRat from 'big-rational';
4 | var constants = require("../js/constants");
5 |
6 | var UserActions = function() {
7 |
8 | this.loadAddresses = function(init) {
9 | var _client = this.flux.stores.config.getEthereumClient();
10 |
11 | this.dispatch(constants.user.LOAD_ADDRESSES);
12 |
13 | _client.loadAddresses(function(addresses) {
14 | var defaultAccount = _client.loadCoinbase();
15 | this.dispatch(constants.user.LOAD_DEFAULT_ACCOUNT, defaultAccount);
16 |
17 | // Load primary address
18 | var primary = false;
19 | try {
20 | primary = _client.getHex('EtherEx', 'primary');
21 | }
22 | catch(e) {
23 | _client.putHex('EtherEx', 'primary', defaultAccount);
24 | }
25 | var valid = false;
26 | if (primary && typeof primary == 'string')
27 | valid = _.includes(addresses, primary);
28 | if (!valid)
29 | if (defaultAccount)
30 | primary = defaultAccount;
31 | else
32 | primary = addresses[0];
33 |
34 | this.dispatch(constants.user.LOAD_ADDRESSES_SUCCESS, {
35 | primary: primary,
36 | addresses: addresses
37 | });
38 |
39 | // Update balance after loading addresses TODO all addresses
40 | this.flux.actions.user.updateBalance();
41 |
42 | // Watch blocks to update the user's ETH balance
43 | var user = this.flux.store("UserStore").getState().user;
44 | _client.watchAddress(user.id);
45 |
46 | // Load BtcSwap client
47 | this.flux.actions.config.updateBtcSwapClient();
48 |
49 | // Load markets
50 | if (init && !this.flux.store("UserStore").getState().user.error)
51 | this.flux.actions.market.initializeMarkets();
52 |
53 | }.bind(this), function(error) {
54 | this.dispatch(constants.user.LOAD_ADDRESSES_FAIL, {error: error});
55 | }.bind(this));
56 | };
57 |
58 | this.switchAddress = function(payload) {
59 | var _client = this.flux.stores.config.getEthereumClient();
60 | _client.putHex('EtherEx', 'primary', payload.address);
61 |
62 | this.dispatch(constants.user.SWITCH_ADDRESS, payload);
63 |
64 | this.flux.actions.user.updateBalance();
65 | this.flux.actions.user.updateBalanceSub();
66 |
67 | this.flux.actions.market.reloadTransactions();
68 |
69 | // Reload BtcSwap client
70 | this.flux.actions.config.updateBtcSwapClient();
71 | };
72 |
73 | this.updateBalance = function() {
74 | var _client = this.flux.stores.config.getEthereumClient();
75 |
76 | var user = this.flux.store("UserStore").getState().user;
77 |
78 | _client.updateBalance(user.id, function(confirmed, unconfirmed) {
79 | this.dispatch(constants.user.UPDATE_BALANCE, {
80 | balance: confirmed,
81 | balancePending: unconfirmed
82 | });
83 | }.bind(this), function(error) {
84 | this.dispatch(constants.user.UPDATE_BALANCE_FAIL, {error: error});
85 | }.bind(this));
86 | };
87 |
88 | this.updateBalanceSub = function(market) {
89 | if (this.flux.stores.config.debug)
90 | console.count("updateBalanceSub triggered");
91 |
92 | var _client = this.flux.stores.config.getEthereumClient();
93 |
94 | var user = this.flux.store("UserStore").getState().user;
95 |
96 | var currentMarket = this.flux.store("MarketStore").getState().market;
97 | if (!market)
98 | market = currentMarket;
99 |
100 | _client.updateBalanceSub(market, user.id, function(_market, available, trading, balance) {
101 | if (_market.id == currentMarket.id)
102 | this.dispatch(constants.user.UPDATE_BALANCE_SUB, {
103 | available: available,
104 | trading: trading,
105 | balance: balance
106 | });
107 |
108 | this.flux.actions.market.updateMarketBalances(_market, available, trading, balance);
109 |
110 | }.bind(this), function(error) {
111 | this.dispatch(constants.user.UPDATE_BALANCE_SUB_FAIL, {error: error});
112 | }.bind(this));
113 | };
114 |
115 | this.sendEther = function(payload) {
116 | var _client = this.flux.stores.config.getEthereumClient();
117 |
118 | var user = this.flux.store("UserStore").getState().user;
119 |
120 | this.dispatch(constants.user.SEND_ETHER, payload);
121 |
122 | _client.sendEther(user, payload.amount, payload.recipient, function(result) {
123 | if (this.flux.stores.config.debug)
124 | utils.log("SEND_ETHER_RESULT", result);
125 | this.flux.actions.user.updateBalance();
126 | }.bind(this), function(error) {
127 | this.dispatch(constants.user.SEND_ETHER_FAIL, {error: error});
128 | }.bind(this));
129 | };
130 |
131 | this.sendSub = function(payload) {
132 | var _client = this.flux.stores.config.getEthereumClient();
133 |
134 | var user = this.flux.store("UserStore").getState().user;
135 | var market = this.flux.store("MarketStore").getState().market;
136 |
137 | var amount = bigRat(payload.amount).multiply(Math.pow(10, market.decimals)).toDecimal();
138 |
139 | this.dispatch(constants.user.SEND_SUB, { amount: amount });
140 |
141 | _client.sendSub(user, amount, payload.recipient, market, function(result) {
142 | if (this.flux.stores.config.debug)
143 | utils.log("SEND_SUB_RESULT", result);
144 | this.flux.actions.user.updateBalanceSub();
145 | }.bind(this), function(error) {
146 | this.dispatch(constants.user.SEND_SUB_FAIL, {error: error});
147 | }.bind(this));
148 | };
149 |
150 | this.depositSub = function(payload) {
151 | var _client = this.flux.stores.config.getEthereumClient();
152 |
153 | var user = this.flux.store("UserStore").getState().user;
154 | var market = this.flux.store("MarketStore").getState().market;
155 |
156 | var amount = bigRat(payload.amount).multiply(Math.pow(10, market.decimals)).toDecimal();
157 |
158 | this.dispatch(constants.user.DEPOSIT, { amount: amount });
159 |
160 | _client.depositSub(user, amount, market, function(result) {
161 | if (this.flux.stores.config.debug)
162 | utils.log("DEPOSIT_RESULT", result);
163 | this.flux.actions.user.updateBalanceSub();
164 | }.bind(this), function(error) {
165 | this.dispatch(constants.user.DEPOSIT_FAIL, {error: error});
166 | }.bind(this));
167 | };
168 |
169 | this.withdrawSub = function(payload) {
170 | var _client = this.flux.stores.config.getEthereumClient();
171 |
172 | var user = this.flux.store("UserStore").getState().user;
173 | var market = this.flux.store("MarketStore").getState().market;
174 |
175 | var amount = bigRat(payload.amount).multiply(Math.pow(10, market.decimals)).toDecimal();
176 |
177 | this.dispatch(constants.user.WITHDRAW, { amount: amount });
178 |
179 | _client.withdrawSub(user, amount, market, function(result) {
180 | if (this.flux.stores.config.debug)
181 | utils.log("WITHDRAW_RESULT", result);
182 | this.flux.actions.user.updateBalanceSub();
183 | }.bind(this), function(error) {
184 | this.dispatch(constants.user.WITHDRAW_FAIL, {error: error});
185 | }.bind(this));
186 | };
187 | };
188 |
189 | module.exports = UserActions;
190 |
--------------------------------------------------------------------------------
/frontend/app/app.jsx:
--------------------------------------------------------------------------------
1 | import Fluxxor from 'fluxxor';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import {Router, Route, IndexRoute, useRouterHistory } from 'react-router';
5 | import { createHashHistory } from 'history';
6 |
7 | import EtherExApp from './components/EtherExApp';
8 |
9 | import Trades from './components/Trades';
10 | import Markets from './components/Markets';
11 | import UserDetails from './components/UserDetails';
12 | import Wallet from './components/Wallet';
13 | import Tools from './components/Tools';
14 | import Help from './components/Help';
15 | import Placeholder from './components/Placeholder';
16 |
17 | import Tickets from './components/btcswap/Tickets';
18 | import CreateTicket from './components/btcswap/CreateTicket';
19 | import ReserveTicket from './components/btcswap/ReserveTicket';
20 | import ClaimTicket from './components/btcswap/ClaimTicket';
21 | import BtcHelp from './components/btcswap/Help';
22 |
23 | import ConfigStore from './stores/ConfigStore';
24 | import NetworkStore from './stores/NetworkStore';
25 | import UserStore from './stores/UserStore';
26 | import TradeStore from './stores/TradeStore';
27 | import MarketStore from './stores/MarketStore';
28 | import TicketStore from './stores/btcswap/TicketStore';
29 |
30 | import ConfigActions from './actions/ConfigActions';
31 | import NetworkActions from './actions/NetworkActions';
32 | import UserActions from './actions/UserActions';
33 | import TradeActions from './actions/TradeActions';
34 | import MarketActions from './actions/MarketActions';
35 | import TicketActions from './actions/btcswap/TicketActions';
36 |
37 | // Load fonts and icons
38 | require("./css/fonts.css");
39 | require("./css/icons.css");
40 |
41 | let stores = {
42 | config: new ConfigStore(),
43 | network: new NetworkStore(),
44 | UserStore: new UserStore(),
45 | MarketStore: new MarketStore(),
46 | TradeStore: new TradeStore(),
47 | TicketStore: new TicketStore()
48 | };
49 |
50 | let actions = {
51 | config: new ConfigActions(),
52 | network: new NetworkActions(),
53 | user: new UserActions(),
54 | market: new MarketActions(),
55 | trade: new TradeActions(),
56 | ticket: new TicketActions()
57 | };
58 |
59 | let flux = new Fluxxor.Flux(stores, actions);
60 |
61 | let createFluxComponent = function (Component, props) {
62 | return ;
63 | };
64 |
65 | flux.setDispatchInterceptor(function(action, dispatch) {
66 | ReactDOM.unstable_batchedUpdates(function() {
67 | dispatch(action);
68 | });
69 | });
70 |
71 | // Opt-out of fugly _k in query string
72 | const appHistory = useRouterHistory(createHashHistory)({ queryKey: false })
73 |
74 | let routes = (
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | );
98 |
99 | ReactDOM.render(routes, document.getElementById('app'));
100 |
--------------------------------------------------------------------------------
/frontend/app/components/AlertDismissable.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Alert, Button} from 'react-bootstrap';
3 |
4 | let AlertDismissable = React.createClass({
5 | getInitialState: function() {
6 | return {
7 | alertVisible: !this.props.show ? false : true
8 | };
9 | },
10 |
11 | handleAlertDismiss: function() {
12 | this.setState({alertVisible: false});
13 | },
14 |
15 | handleAlertShow: function() {
16 | this.setState({alertVisible: true});
17 | },
18 |
19 | render: function() {
20 | if (this.state.alertVisible) {
21 | return (
22 |
23 | Oh snap!
24 | {this.props.message}
25 |
26 | Got it
27 |
28 |
29 | );
30 | }
31 | return false;
32 | }
33 | });
34 |
35 | module.exports = AlertDismissable;
36 |
--------------------------------------------------------------------------------
/frontend/app/components/AlertModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Alert, Modal, Button} from 'react-bootstrap';
3 |
4 | let AlertModal = React.createClass({
5 | onHide: function(e) {
6 | e.preventDefault();
7 | this.props.onHide();
8 | },
9 |
10 | render: function() {
11 | return (
12 |
13 |
14 | { this.props.modalTitle ? this.props.modalTitle : "Oh snap!" }
15 |
16 |
17 |
18 | { this.props.message }
19 |
20 | { (this.props.note) &&
21 | { this.props.note }
}
22 |
23 |
24 | Got it
25 |
26 |
27 | );
28 | }
29 | });
30 |
31 | module.exports = AlertModal;
32 |
--------------------------------------------------------------------------------
/frontend/app/components/Balance.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {FormattedMessage, FormattedNumber} from 'react-intl';
3 | import {Popover, OverlayTrigger} from 'react-bootstrap';
4 |
5 | let Balance = React.createClass({
6 | render: function() {
7 | return (
8 |
9 |
11 |
12 |
13 |
14 | { this.props.market.market.name }{' / '}
15 | { this.props.user.user.balance } ETH
16 |
17 | } >
18 |
19 | :
20 |
21 | { this.props.market.market.name }{' / '}
22 |
23 | { this.props.user.user.balanceFormatted &&
24 | }
31 |
32 |
33 |
34 |
35 | );
36 | }
37 | });
38 |
39 | module.exports = Balance;
40 |
--------------------------------------------------------------------------------
/frontend/app/components/BalanceSub.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl, FormattedMessage} from 'react-intl';
3 | import {Popover, OverlayTrigger} from 'react-bootstrap';
4 |
5 | import utils from '../js/utils';
6 |
7 | let BalanceSub = injectIntl(React.createClass({
8 | render: function() {
9 | var formatMessage = this.props.intl.formatMessage;
10 | var available = this.props.user.user.balanceSubAvailable;
11 | var trading = this.props.user.user.balanceSubTrading;
12 | var balance = this.props.user.user.balanceSub;
13 |
14 | return (
15 |
16 |
17 |
Wallet
18 |
20 | { formatMessage({id: 'wallet.balance'}, {
21 | currency: this.props.market.market.name,
22 | balance: balance
23 | })
24 | }
25 | }>
26 |
27 | { this.props.si ?
28 | utils.format(balance) :
29 |
36 | }
37 |
38 |
39 |
40 |
41 |
Available to trade
42 |
43 |
45 | { formatMessage({id: 'wallet.available'}, {
46 | currency: this.props.market.market.name,
47 | balance: available
48 | })
49 | }
50 | }>
51 |
52 | { this.props.si ?
53 | utils.format(available) :
54 |
61 | }
62 |
63 |
64 |
65 |
67 |
68 | { formatMessage({id: 'wallet.pending'}, {
69 | currency: "ETH",
70 | balance: this.props.user.user.balance,
71 | pending: this.props.user.user.balancePending
72 | })
73 | }
74 |
75 | }>
76 |
77 | { this.props.user.user.balanceFormatted &&
78 |
85 | }
86 |
87 |
88 |
89 |
90 |
In trades
91 |
93 | { formatMessage({id: 'wallet.trading'}, {
94 | currency: this.props.market.market.name,
95 | balance: trading
96 | })
97 | }
98 | }>
99 |
100 | { this.props.si ?
101 | utils.format(trading) :
102 |
109 | }
110 |
111 |
112 |
113 |
114 | );
115 | }
116 | }));
117 |
118 | module.exports = BalanceSub;
119 |
--------------------------------------------------------------------------------
/frontend/app/components/Chat.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import {FormattedTime} from 'react-intl';
4 | import {Accordion, Glyphicon, Button, Modal, Panel, Input, Well} from 'react-bootstrap';
5 |
6 | let Chat = React.createClass({
7 | getInitialState() {
8 | return {
9 | activeKey: null,
10 | messages: [],
11 | message: null,
12 | showModal: false,
13 | modalMessage: null
14 | };
15 | },
16 |
17 | componentWillReceiveProps(nextProps) {
18 | this.setState({
19 | messages: _.orderBy(nextProps.market.messages, 'sent', 'desc')
20 | });
21 | },
22 |
23 | hideModal() {
24 | this.setState({
25 | showModal: false
26 | });
27 | },
28 |
29 | toggleActive(key) {
30 | this.setState({ activeKey: this.state.activeKey ? null : key});
31 | },
32 |
33 | handleChange(e) {
34 | e.preventDefault();
35 | this.setState({
36 | message: this.refs.message.getValue()
37 | });
38 | },
39 |
40 | handleSend(e) {
41 | e.preventDefault();
42 | var ethereumClient = this.props.flux.stores.config.getEthereumClient();
43 | if (ethereumClient.hasWhisper())
44 | this.props.flux.actions.market.postMessage(this.state.message);
45 | else
46 | this.setState({
47 | showModal: true,
48 | modalMessage:
49 | Whisper is not enabled on this node. Start geth with:
--shh --rpcapi "db,eth,net,web3,shh"
50 |
51 | });
52 | this.setState({
53 | message: null
54 | });
55 | },
56 |
57 | render() {
58 | return (
59 |
60 |
61 | { this.props.market.name } Whisper
63 | { this.state.activeKey && }
64 |
65 | } bsStyle="primary" eventKey="1">
66 |
67 | { this.state.messages.map( function(message, i) {
68 | if (!message.error)
69 | return (
70 |
71 | [{ message.sent && }]{' '}
72 | { message.from && message.from.substr(2, 7)}: { message.payload }
73 |
74 | );
75 | })
76 | }
77 |
78 |
84 |
85 |
86 |
87 |
88 |
89 | Oh snap!
90 |
91 |
92 | {this.state.modalMessage}
93 |
94 |
95 | Got it
96 |
97 |
98 |
99 | );
100 | }
101 | });
102 |
103 | module.exports = Chat;
104 |
--------------------------------------------------------------------------------
/frontend/app/components/Clipboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | function noop() {}
5 |
6 | let Clipboard = React.createClass({
7 | propTypes: {
8 | value : React.PropTypes.string.isRequired,
9 | className : React.PropTypes.string,
10 | style : React.PropTypes.object,
11 | onCopy : React.PropTypes.func
12 | },
13 |
14 | getDefaultProps: function() {
15 | return {
16 | className : "clipboard",
17 | style : {
18 | "position" : "fixed",
19 | "left" : 0,
20 | "top" : 0,
21 | "width" : 0,
22 | "height" : 0,
23 | "padding" : 0,
24 | "margin" : 0,
25 | "zIndex" : 100,
26 | "opacity" : 0
27 | },
28 | onCopy : noop
29 | };
30 | },
31 |
32 | componentDidMount: function() {
33 | document.addEventListener("keydown", this.handleKeyDown, false);
34 | document.addEventListener("keyup", this.handleKeyUp, false);
35 | },
36 |
37 | componentWillUnmount: function() {
38 | document.removeEventListener("keydown", this.handleKeyDown, false);
39 | document.removeEventListener("keyup", this.handleKeyUp, false);
40 | },
41 |
42 | handleCopy : function(e) {
43 | this.props.onCopy(e);
44 | },
45 |
46 | handleKeyDown : function(e) {
47 | var metaKeyIsDown = (e.ctrlKey || e.metaKey);
48 | var textIsSelected = window.getSelection().toString();
49 |
50 | if(!metaKeyIsDown || textIsSelected) {
51 | return;
52 | }
53 |
54 | var element = ReactDOM.findDOMNode(this);
55 | element.focus();
56 | element.select();
57 | },
58 |
59 | handleKeyUp: function() {
60 | var element = ReactDOM.findDOMNode(this);
61 | element.blur();
62 | },
63 |
64 | render: function() {
65 | return React.createElement("textarea", Object.assign({}, this.props, { readOnly: true, onCopy : this.handleCopy }));
66 | }
67 |
68 | });
69 |
70 | module.exports = Clipboard;
71 |
--------------------------------------------------------------------------------
/frontend/app/components/ConfirmModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl} from 'react-intl';
3 | import {Button, Modal} from 'react-bootstrap';
4 |
5 | import TradeTable from './TradeTable';
6 |
7 | let ConfirmModal = injectIntl(React.createClass({
8 | getInitialState() {
9 | return {
10 | tradeTable: null
11 | };
12 | },
13 |
14 | componentWillReceiveProps: function(nextProps) {
15 | if (nextProps.user && nextProps.market && nextProps.tradeList && nextProps.tradeList.length)
16 | this.setState({
17 | tradeTable:
18 |
25 | });
26 | else
27 | this.setState({
28 | tradeTable:
29 | });
30 | },
31 |
32 | onHide: function(e) {
33 | e.preventDefault();
34 | this.props.onHide();
35 | },
36 |
37 | render: function() {
38 | var formatMessage = this.props.intl.formatMessage;
39 | return (
40 |
41 |
42 | {formatMessage({id: 'confirm.required'})}
43 |
44 |
61 |
62 | );
63 | }
64 | }));
65 |
66 | module.exports = ConfirmModal;
67 |
--------------------------------------------------------------------------------
/frontend/app/components/Favicon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | let linkEl;
3 |
4 | let drawIcon = function (src, num, cb) {
5 | var img = document.createElement('img');
6 | img.onload = function () {
7 | var canvas = document.createElement('canvas');
8 | canvas.width = img.width;
9 | canvas.height = img.height;
10 |
11 | var context = canvas.getContext('2d');
12 | context.clearRect(0, 0, img.width, img.height);
13 | context.drawImage(img, 0, 0);
14 |
15 | var top = img.height / 2 - 6,
16 | left = img.width / 2 - 6,
17 | bottom = img.height,
18 | right = img.width,
19 | radius = 8;
20 |
21 | context.fillStyle = '#F03D25';
22 | context.strokeStyle = '#F03D25';
23 | context.lineWidth = 1;
24 |
25 | context.beginPath();
26 | context.moveTo(left + radius, top);
27 | context.quadraticCurveTo(left, top, left, top + radius);
28 | context.lineTo(left, bottom - radius);
29 | context.quadraticCurveTo(left, bottom, left + radius, bottom);
30 | context.lineTo(right - radius, bottom);
31 | context.quadraticCurveTo(right, bottom, right, bottom - radius);
32 | context.lineTo(right, top + radius);
33 | context.quadraticCurveTo(right, top, right - radius, top);
34 | context.closePath();
35 | context.fill();
36 |
37 | context.font = '16px Arial';
38 | context.fillStyle = '#FFF';
39 | context.textAlign = 'center';
40 | context.textBaseline = 'top';
41 | context.fillText(num, img.height / 2 + 5, img.height / 2 - 3);
42 |
43 | cb(null, context.canvas.toDataURL());
44 | };
45 | img.src = src;
46 | };
47 |
48 | var Favicon = React.createClass({
49 | displayName: 'Favicon',
50 |
51 | statics: {
52 | mountedInstances: [],
53 |
54 | getActiveInstance() {
55 | return Favicon.mountedInstances[Favicon.mountedInstances.length - 1];
56 | },
57 |
58 | draw() {
59 | if (typeof document === 'undefined')
60 | return;
61 |
62 | if (typeof linkEl === 'undefined') {
63 | var head = document.getElementsByTagName('head')[0];
64 | linkEl = document.createElement('link');
65 | linkEl.type = 'image/x-icon';
66 | linkEl.rel = 'icon';
67 |
68 | // remove existing favicons
69 | var links = head.getElementsByTagName('link');
70 | for (var i = links.length; --i >= 0; /\bicon\b/i.test(links[i].getAttribute('rel'))) {
71 | head.removeChild(links[i]);
72 | }
73 |
74 | head.appendChild(linkEl);
75 | }
76 |
77 | var activeInstance = Favicon.getActiveInstance();
78 | var currentUrl;
79 |
80 | if (activeInstance.props.url instanceof Array)
81 | currentUrl = activeInstance.props.url[activeInstance.state.animationIndex];
82 | else
83 | currentUrl = activeInstance.props.url;
84 |
85 | if (activeInstance.props.alertCount) {
86 | drawIcon(currentUrl, activeInstance.props.alertCount, function (err, url) {
87 | linkEl.href = url;
88 | });
89 | }
90 | else
91 | linkEl.href = currentUrl;
92 | },
93 |
94 | update() {
95 | if (typeof document === 'undefined')
96 | return;
97 |
98 | var activeInstance = Favicon.getActiveInstance();
99 | var isAnimated = activeInstance.props.url instanceof Array && activeInstance.props.animated;
100 |
101 | // clear any running animations
102 | var intervalId = null;
103 | clearInterval(activeInstance.state.animationLoop);
104 |
105 | if (isAnimated) {
106 | var animateFavicon = function() {
107 | var nextAnimationIndex = (activeInstance.state.animationIndex + 1) % activeInstance.props.url.length;
108 | Favicon.draw();
109 | activeInstance.setState({ animationIndex: nextAnimationIndex });
110 | };
111 | intervalId = setInterval(animateFavicon, activeInstance.props.animationDelay);
112 | animateFavicon();
113 | } else {
114 | Favicon.draw();
115 | }
116 |
117 | activeInstance.setState({ animationLoop: intervalId });
118 | }
119 | },
120 |
121 | getDefaultProps() {
122 | return {
123 | alertCount: null,
124 | animated: true,
125 | animationDelay: 500
126 | };
127 | },
128 |
129 | getInitialState() {
130 | return {
131 | animationIndex: 0,
132 | animationLoop: null,
133 | animationRunning: false
134 | };
135 | },
136 |
137 | componentWillMount() {
138 | Favicon.mountedInstances.push(this);
139 | Favicon.update();
140 | },
141 |
142 | shouldComponentUpdate(nextProps) {
143 | if (nextProps.alertCount === this.props.alertCount)
144 | return false;
145 | return true;
146 | },
147 |
148 | componentDidUpdate(prevProps) {
149 | if (prevProps.url === this.props.url &&
150 | prevProps.animated === this.props.animated &&
151 | prevProps.alertCount === this.props.alertCount) return;
152 | Favicon.update();
153 | },
154 |
155 | render() {
156 | return null;
157 | }
158 | });
159 |
160 | module.exports = Favicon;
161 |
--------------------------------------------------------------------------------
/frontend/app/components/GraphPrice.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | let Chart = require("./GraphPriceTechan");
4 |
5 | let GraphPrice = React.createClass({
6 | render: function() {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 | });
14 |
15 | module.exports = GraphPrice;
16 |
--------------------------------------------------------------------------------
/frontend/app/components/Help.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | let Help = React.createClass({
4 | render: function() {
5 | return (
6 |
7 |
{this.props.title}
8 |
9 |
10 |
11 | Complete dApp documentation will be here soon.
12 |
13 |
14 | );
15 | }
16 | });
17 |
18 | module.exports = Help;
19 |
--------------------------------------------------------------------------------
/frontend/app/components/LastPrice.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import utils from '../js/utils';
4 |
5 | let LastPrice = React.createClass({
6 | getInitialState: function () {
7 | return {
8 | lastMarket: null,
9 | lastPrice: null,
10 | priceChange: 'default'
11 | };
12 | },
13 |
14 | componentWillReceiveProps: function(nextProps) {
15 | if (nextProps.market.name == this.state.lastMarket) {
16 | if (nextProps.market.lastPrice > this.state.lastPrice)
17 | this.setState({
18 | priceChange: 'success'
19 | });
20 | else if (nextProps.market.lastPrice < this.state.lastPrice)
21 | this.setState({
22 | priceChange: 'danger'
23 | });
24 | }
25 | else
26 | this.setState({
27 | lastMarket: nextProps.market.name,
28 | lastPrice: "N/A",
29 | priceChange: 'default'
30 | });
31 |
32 | if (nextProps.market.lastPrice && nextProps.market.name) {
33 | this.setState({
34 | lastMarket: nextProps.market.name,
35 | lastPrice: utils.numeral(nextProps.market.lastPrice, String(nextProps.market.precision).length - 1)
36 | });
37 | }
38 | },
39 |
40 | render: function() {
41 | return (
42 |
43 | Last price: {' '}
44 |
45 | { this.state.lastPrice }
46 | {(this.state.lastPrice && this.state.lastMarket) ? this.state.lastMarket + "/ETH" : "N/A" }
47 |
48 |
49 | );
50 | }
51 | });
52 |
53 | module.exports = LastPrice;
54 |
--------------------------------------------------------------------------------
/frontend/app/components/MarketList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {FormattedMessage} from 'react-intl';
3 | import {ProgressBar} from 'react-bootstrap';
4 |
5 | import AlertDismissable from './AlertDismissable';
6 | import MarketTable from './MarketTable';
7 |
8 | let MarketList = React.createClass({
9 | render: function() {
10 | return (
11 |
12 |
13 |
14 | { this.props.title ?
15 |
{this.props.title} {
16 | this.props.market.loading &&
17 | ... } : "" }
18 |
19 |
20 | { (this.props.market.loading && this.props.config.percentLoaded < 100) &&
21 |
}
24 |
25 |
26 | {this.props.market.error &&
27 |
}
28 |
29 |
30 |
31 |
32 | );
33 | }
34 | });
35 |
36 | module.exports = MarketList;
37 |
--------------------------------------------------------------------------------
/frontend/app/components/MarketRow.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | let MarketRow = React.createClass({
4 | handleClick: function(e) {
5 | e.preventDefault();
6 |
7 | if (this.props.market)
8 | this.props.flux.actions.market.switchMarket(this.props.market);
9 | },
10 |
11 | toggleFavorite: function(e) {
12 | e.preventDefault();
13 | e.stopPropagation();
14 |
15 | var favorite = false;
16 | if (this.props.market.favorite === false)
17 | favorite = true;
18 |
19 | this.props.flux.actions.market.toggleFavorite({
20 | id: this.props.market.id,
21 | favorite: favorite
22 | });
23 | },
24 |
25 | render: function() {
26 | // draggable="true"
27 | // onDragEnd={this.dragEnd}
28 | // onDragStart={this.dragStart}
29 |
30 | return (
31 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {this.props.market.name}/ETH
41 |
42 |
43 |
44 |
45 |
46 | {this.props.market.daySign}
47 |
48 | {this.props.market.dayChange} /{' '}
49 |
50 | {this.props.market.weekSign}
51 |
52 | {this.props.market.weekChange} /{' '}
53 |
54 | {this.props.market.monthSign}
55 |
56 | {this.props.market.monthChange}
57 |
58 |
59 |
60 |
61 | { this.props.price }
62 |
63 |
64 |
65 |
66 | { this.props.available }
67 |
68 |
69 |
70 |
71 | { this.props.trading }
72 |
73 |
74 |
75 |
76 | { this.props.balance }
77 |
78 |
79 |
80 | );
81 | }
82 | });
83 |
84 | module.exports = MarketRow;
85 |
--------------------------------------------------------------------------------
/frontend/app/components/MarketSelect.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import {DropdownButton, MenuItem} from 'react-bootstrap';
4 |
5 | let MarketSelect = React.createClass({
6 | getInitialState() {
7 | return {
8 | items: [],
9 | market: this.props.market.market.name || '-'
10 | };
11 | },
12 |
13 | componentWillReceiveProps(nextProps) {
14 | var items = _.compact(this.props.market.markets.map(function(market) {
15 | if (!this.props.market.favorites || this.props.market.favorites.length === 0 || (market.id > 0 && market.favorite))
16 | return {market.name} ;
17 | }.bind(this)));
18 |
19 | this.setState({
20 | items: items,
21 | market: nextProps.market.market.name || '-'
22 | });
23 | },
24 |
25 | handleChange: function(e, key) {
26 | e.preventDefault();
27 | var index = _.findIndex(this.props.market.markets, {'id': key});
28 | this.props.flux.actions.market.switchMarket(this.props.market.markets[index]);
29 | },
30 |
31 | render: function() {
32 | return (
33 |
34 |
35 |
42 | { this.state.items }
43 |
44 |
45 |
46 |
52 | { this.state.items }
53 |
54 |
55 |
56 | );
57 | }
58 | });
59 |
60 | module.exports = MarketSelect;
61 |
--------------------------------------------------------------------------------
/frontend/app/components/MarketTable.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import {injectIntl, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
4 | import {Table} from 'react-bootstrap';
5 |
6 | import MarketRow from './MarketRow';
7 |
8 | let fixtures = require("../js/fixtures");
9 |
10 | let MarketTable = injectIntl(React.createClass({
11 | getInitialState: function() {
12 | return {
13 | markets: []
14 | };
15 | },
16 |
17 | componentDidMount: function() {
18 | this.componentWillReceiveProps(this.props);
19 | },
20 |
21 | componentWillReceiveProps: function(nextProps) {
22 | var markets = [];
23 |
24 | // Filter by category
25 | var category = _.filter(fixtures.categories, {key: nextProps.category})[0];
26 | if (nextProps.category != 'markets')
27 | markets = _.filter(nextProps.market.markets, {category: category.id});
28 | else
29 | markets = nextProps.market.markets;
30 |
31 | this.setState({
32 | markets: markets
33 | });
34 | },
35 |
36 | render: function() {
37 | //
38 |
39 | var marketRows = this.state.markets.map(function (market) {
40 | var price = this.props.intl.formatMessage({id: 'price'}, {
41 | price: market.lastPrice,
42 | currency: market.name
43 | }
44 | );
45 | var available = this.props.intl.formatMessage({id: 'wallet.sub'}, {
46 | currency: market.name,
47 | balance: market.available
48 | }
49 | );
50 | var trading = this.props.intl.formatMessage({id: 'wallet.sub'}, {
51 | currency: market.name,
52 | balance: market.trading
53 | }
54 | );
55 | var balance = this.props.intl.formatMessage({id: 'wallet.sub'}, {
56 | currency: market.name,
57 | balance: market.balance
58 | }
59 | );
60 | return (
61 |
67 | );
68 | }.bind(this));
69 |
70 | return (
71 |
72 |
{this.props.title}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | {marketRows}
101 |
102 |
103 |
104 | );
105 | }
106 |
107 | // Future drag-n-drop...
108 |
109 | // getInitialState: function() {
110 | // return {
111 | // markets: this.props.market.markets
112 | // };
113 | // },
114 |
115 | // getPlaceholder: function() {
116 | // if (!this.placeholder) {
117 | // var tr = document.createElement('tr');
118 | // tr.className = "warning";
119 | // tr.disabled = "disabled";
120 | // var td = document.createElement('td');
121 | // td.colSpan = 5;
122 | // td.appendChild(document.createTextNode("Drop here"));
123 | // tr.appendChild(td);
124 | // this.placeholder = tr;
125 | // }
126 | // return this.placeholder;
127 | // },
128 |
129 | // getTableRow: function(element) {
130 | // if (element.tagName !== 'tr' && element.className !== 'warning')
131 | // return $(element).closest('tr')[0];
132 | // else
133 | // return element;
134 | // },
135 |
136 | // dragStart: function(e) {
137 | // this.dragged = this.getTableRow(e.currentTarget);
138 | // e.dataTransfer.effectAllowed = 'move';
139 |
140 | // // Firefox requires calling dataTransfer.setData
141 | // // for the drag to properly work
142 | // e.dataTransfer.setData("text/html", e.currentTarget);
143 | // },
144 |
145 | // dragEnd: function(e) {
146 | // this.dragged.style.display = "table-row";
147 | // this.dragged.parentNode.removeChild(this.getPlaceholder());
148 |
149 | // // Update data
150 | // var markets = this.state.markets;
151 | // var from = Number(this.dragged.dataset.id);
152 | // var to = Number(this.over.dataset.id);
153 | // if (from < to) to--;
154 | // if (this.nodePlacement == "after") to++;
155 | // markets.splice(to, 0, markets.splice(from, 1)[0]);
156 |
157 | // this.setState({
158 | // markets: markets
159 | // });
160 | // },
161 |
162 | // dragOver: function(e) {
163 | // e.preventDefault();
164 | // var targetRow = this.getTableRow(e.target);
165 |
166 | // // if (!this.dragged.style)
167 | // // return;
168 | // if (this.dragged && this.dragged.style)
169 | // this.dragged.style.display = "none";
170 |
171 | // if (targetRow.className == "warning") return;
172 | // this.over = targetRow;
173 |
174 | // // Inside the dragOver method
175 | // var relY = e.pageY - $(this.over).offset().top;
176 | // var height = this.over.offsetHeight / 2;
177 | // var parent = targetRow.parentNode;
178 |
179 | // if (relY >= height) {
180 | // this.nodePlacement = "after";
181 | // parent.insertBefore(this.getPlaceholder(), targetRow.nextElementSibling);
182 | // }
183 | // else if (relY < height) {
184 | // this.nodePlacement = "before"
185 | // parent.insertBefore(this.getPlaceholder(), targetRow);
186 | // }
187 | // },
188 | }));
189 |
190 | module.exports = MarketTable;
191 |
--------------------------------------------------------------------------------
/frontend/app/components/Markets.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import RangeSelect from './RangeSelect';
4 | import GraphPrice from './GraphPrice';
5 | import MarketList from './MarketList';
6 | import SubNavBar from './SubNavBar';
7 |
8 | let Markets = React.createClass({
9 | getInitialState() {
10 | var path = this.props.routes[1].path;
11 | return {
12 | showGraph: false,
13 | category: path.slice(path.lastIndexOf('/') + 1, path.length)
14 | };
15 | },
16 |
17 | componentDidMount() {
18 | this.props.disableGraph();
19 | },
20 |
21 | componentWillReceiveProps(nextProps) {
22 | if (nextProps) {
23 | var path = nextProps.routes[1].path;
24 | this.setState({
25 | category: path.slice(path.lastIndexOf('/') + 1, path.length)
26 | });
27 | }
28 | },
29 |
30 | render() {
31 | return (
32 |
33 |
34 |
35 |
36 | {!this.props.market.error && (
37 |
38 |
39 |
40 |
46 |
47 | )}
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | });
55 |
56 | module.exports = Markets;
57 |
--------------------------------------------------------------------------------
/frontend/app/components/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router';
3 | import {injectIntl, FormattedMessage} from 'react-intl';
4 | import {Nav, Navbar} from 'react-bootstrap';
5 |
6 | let NavBar = injectIntl(React.createClass({
7 | render() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {' '}
17 |
18 |
19 |
20 |
21 |
22 | {' '}
23 |
24 |
25 |
26 |
27 |
28 | {' '}
29 | BTC
30 |
31 |
32 |
33 |
34 | {' '}
35 |
36 |
37 |
38 |
39 |
40 | {' '}
41 |
42 |
43 |
44 |
45 |
46 | {' '}
47 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 | }));
55 |
56 | module.exports = NavBar;
57 |
--------------------------------------------------------------------------------
/frontend/app/components/Network.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {StoreWatchMixin} from 'fluxxor';
3 | import {injectIntl, FormattedDate, FormattedNumber, FormattedMessage, FormattedRelative} from 'react-intl';
4 |
5 | import utils from '../js/utils';
6 |
7 | let Network = injectIntl(React.createClass({
8 | mixins: [StoreWatchMixin('config', 'network', 'UserStore')],
9 |
10 | getInitialState() {
11 | return {
12 | gasPrice: '-'
13 | };
14 | },
15 |
16 | componentDidMount() {
17 | this.startCounting();
18 | },
19 |
20 | componentWillReceiveProps() {
21 | var formattedGasPrice = '-';
22 | if (this.state.network.gasPrice) {
23 | var gasPrice = utils.formatEther(this.state.network.gasPrice);
24 | formattedGasPrice = { this.props.intl.formatNumber(gasPrice.value) } { gasPrice.unit } ;
25 | this.setState({
26 | gasPrice: formattedGasPrice
27 | });
28 | }
29 | },
30 |
31 | componentWillUnmount() {
32 | if (this.timer) {
33 | clearInterval(this.timer);
34 | this.timer = null;
35 | }
36 | },
37 |
38 | getStateFromFlux() {
39 | var networkState = this.props.flux.stores.network.getState();
40 | return {
41 | user: this.props.flux.stores.UserStore.getState().user,
42 | config: this.props.flux.stores.config.getState(),
43 | network: networkState,
44 | host: this.props.flux.stores.config.getState().host,
45 | blockTimestamp: networkState.blockTimestamp,
46 | blockTime: parseInt(networkState.blockTime),
47 | networkLag: networkState.networkLag
48 | };
49 | },
50 |
51 | startCounting() {
52 | this.timer = setInterval(this.count, 1000);
53 | },
54 |
55 | count() {
56 | var lastBlock = this.state.network.blockTimestamp ? new Date().getTime() / 1000 - this.state.network.blockTimestamp : null;
57 | var lastState = '';
58 | if (lastBlock) {
59 | if (lastBlock > 90)
60 | lastState = 'danger';
61 | else if (lastBlock > 30)
62 | lastState = 'warning';
63 | else if (lastBlock > 20)
64 | lastState = 'success';
65 | }
66 | this.setState({
67 | lastBlock: parseInt(lastBlock),
68 | lastState: lastState
69 | });
70 | },
71 |
72 | render() {
73 | return (
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | Host
83 |
84 | { this.state.host }
85 |
86 |
87 |
88 | Peers
89 | {
90 | this.state.network.peerCount ?
91 | : 0 }
92 |
93 |
94 |
95 | Blocks
96 | {
97 | this.state.network.blockNumber ?
98 | : '-' }
99 |
100 |
101 |
102 | Miner
103 |
104 | { this.state.network.mining ?
105 | : 'off' }
110 |
111 |
112 |
113 | Ether
114 |
115 | { this.state.user.balance ?
116 |
117 |
122 | : '-' }
123 |
124 |
125 |
126 | Gas price
127 |
128 | { this.state.gasPrice }
129 |
130 |
131 |
132 | Block time
133 |
134 | { this.state.blockTime ?
135 | : '-' } /{' '}
136 |
137 | { this.state.lastBlock ?
138 | ( this.state.lastBlock < this.state.config.timeout ?
139 | :
140 | ) : '-' }
141 |
142 |
143 |
144 |
145 | Network lag
146 |
147 | { this.state.networkLag ?
148 | : '-' }
149 |
150 |
151 |
152 | Last block
153 |
154 |
157 |
158 |
159 |
160 | Client
161 | { this.state.network.client }
162 |
163 |
164 |
165 | );
166 | }
167 | }));
168 |
169 | module.exports = Network;
170 |
--------------------------------------------------------------------------------
/frontend/app/components/Placeholder.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | let Placeholder = React.createClass({
4 | render: function() {
5 | return (
6 |
7 |
{this.props.title}
8 |
9 | );
10 | }
11 | });
12 |
13 | module.exports = Placeholder;
14 |
--------------------------------------------------------------------------------
/frontend/app/components/RangeSelect.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import {ButtonGroup, Button} from 'react-bootstrap';
4 |
5 | import utils from '../js/utils';
6 |
7 | let RangeSelect = React.createClass({
8 | getInitialState: function () {
9 | var configState = this.props.flux.store("config");
10 | return {
11 | block: configState.rangeEnd ? configState.rangeEnd : this.props.flux.stores.network.blockNumber,
12 | range: 75,
13 | live: configState.rangeEnd ? "" : "active",
14 | last15: configState.range == 75 ? "active" : "",
15 | last30: configState.range == 150 ? "active" : "",
16 | last60: configState.range == 300 ? "active" : ""
17 | };
18 | },
19 |
20 | componentWillReceiveProps: function(nextProps) {
21 | var rangeEnd = nextProps.flux.stores.config.rangeEnd;
22 | if (!rangeEnd && this.state.live === "active") {
23 | this.refs.range.defaultValue = nextProps.flux.stores.network.blockNumber;
24 | this.setState({
25 | block: nextProps.flux.stores.network.blockNumber
26 | });
27 | }
28 | else if (rangeEnd) {
29 | this.refs.range.value = rangeEnd;
30 | this.setState({
31 | block: rangeEnd,
32 | live: ''
33 | });
34 | }
35 | },
36 |
37 | handleRange: function(e) {
38 | e.preventDefault();
39 |
40 | var value = _.parseInt(e.target.value);
41 |
42 | this.setState({
43 | last15: "",
44 | last30: "",
45 | last60: "",
46 | range: value
47 | });
48 |
49 | switch (value) {
50 | case 75:
51 | this.setState({
52 | last15: "active"
53 | });
54 | break;
55 | case 150:
56 | this.setState({
57 | last30: "active"
58 | });
59 | break;
60 | case 300:
61 | this.setState({
62 | last60: "active"
63 | });
64 | break;
65 | default:
66 | break;
67 | }
68 |
69 | this.props.flux.actions.config.updateRange(value);
70 | },
71 |
72 | handleRangeEnd: function(e) {
73 | e.preventDefault();
74 |
75 | var block = this.props.flux.stores.network.blockNumber;
76 | var value = null;
77 | if (e.target.value == "live")
78 | value = block;
79 | else
80 | value = _.parseInt(e.target.value);
81 |
82 | if (value >= block - 25) {
83 | this.refs.range.value = block;
84 | this.setState({
85 | block: block,
86 | live: "active"
87 | });
88 | this.props.flux.actions.config.updateRangeEnd(0);
89 | }
90 | else {
91 | this.setState({
92 | block: value,
93 | live: ""
94 | });
95 | this.props.flux.actions.config.updateRangeEnd(value);
96 | }
97 | },
98 |
99 | render: function() {
100 | return (
101 |
132 | );
133 | }
134 | });
135 |
136 | module.exports = RangeSelect;
137 |
--------------------------------------------------------------------------------
/frontend/app/components/SendEther.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import {injectIntl, FormattedMessage} from 'react-intl';
4 | import {Button, Input} from 'react-bootstrap';
5 | import bigRat from 'big-rational';
6 |
7 | import ConfirmModal from './ConfirmModal';
8 |
9 | let fixtures = require("../js/fixtures");
10 |
11 | let SubSend = injectIntl(React.createClass({
12 | getInitialState: function() {
13 | return {
14 | amount: null,
15 | recipient: null,
16 | newSend: false,
17 | showModal: false,
18 | confirmMessage: null
19 | };
20 | },
21 |
22 | openModal: function() {
23 | this.setState({ showModal: true });
24 | },
25 |
26 | closeModal: function() {
27 | this.setState({ showModal: false });
28 | },
29 |
30 | handleChange: function(e) {
31 | e.preventDefault();
32 | this.validate(e);
33 | },
34 |
35 | handleValidation: function(e) {
36 | e.preventDefault();
37 | if (this.validate(e, true))
38 | this.openModal();
39 | },
40 |
41 | validate: function(e, showAlerts) {
42 | e.preventDefault();
43 |
44 | var address = this.refs.address.getValue().trim();
45 | var amount = this.refs.amount.getValue().trim();
46 |
47 | this.setState({
48 | recipient: address,
49 | amount: amount
50 | });
51 |
52 | if (!address || !amount) {
53 | this.props.setAlert('warning', this.props.intl.formatMessage({id: 'form.empty'}));
54 | }
55 | else if (!amount) {
56 | this.props.setAlert('warning', this.props.intl.formatMessage({id: 'form.cheap'}));
57 | }
58 | else if (parseFloat(amount) > this.props.user.balance) {
59 | this.props.setAlert('warning', this.props.intl.formatMessage({id: 'sub.not_enough'}, {
60 | currency: "ETH",
61 | balance: this.props.user.balance
62 | })
63 | );
64 | }
65 | else if (address.length != 42) {
66 | this.props.setAlert('warning',
67 | this.props.intl.formatMessage({id: 'address.size'}, {
68 | size: (address.length < 42 ? "short" : "long")
69 | })
70 | );
71 | }
72 | else {
73 | this.setState({
74 | newSend: true,
75 | confirmMessage:
76 |
82 | });
83 |
84 | this.props.showAlert(false);
85 |
86 | return true;
87 | }
88 |
89 | this.setState({
90 | newSend: false
91 | });
92 |
93 | if (showAlerts)
94 | this.props.showAlert(true);
95 |
96 | return false;
97 | },
98 |
99 | onSubmitForm: function(e, el) {
100 | e.preventDefault();
101 |
102 | if (!this.validate(e, el))
103 | return false;
104 |
105 | var payload = {
106 | recipient: this.state.recipient,
107 | amount: bigRat(this.state.amount).multiply(fixtures.ether).ceil().toDecimal()
108 | };
109 |
110 | this.props.flux.actions.user.sendEther(payload);
111 |
112 | this.setState({
113 | recipient: null,
114 | amount: null,
115 | newSend: false
116 | });
117 | },
118 |
119 | render: function() {
120 | return (
121 |
150 | );
151 | }
152 | }));
153 |
154 | module.exports = SubSend;
155 |
--------------------------------------------------------------------------------
/frontend/app/components/SubDeposit.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl, FormattedMessage} from 'react-intl';
3 | import {Button, Input} from 'react-bootstrap';
4 |
5 | import ConfirmModal from './ConfirmModal';
6 |
7 | let SubDeposit = injectIntl(React.createClass({
8 | getInitialState: function() {
9 | return {
10 | amount: null,
11 | newDeposit: false,
12 | showModal: false,
13 | confirmMessage: null
14 | };
15 | },
16 |
17 | openModal: function() {
18 | this.setState({ showModal: true });
19 | },
20 |
21 | closeModal: function() {
22 | this.setState({ showModal: false });
23 | },
24 |
25 | handleChange: function(e) {
26 | e.preventDefault();
27 | this.validate(e);
28 | },
29 |
30 | handleValidation: function(e) {
31 | e.preventDefault();
32 | if (this.validate(e, true))
33 | this.openModal();
34 | },
35 |
36 | validate: function(e, showAlerts) {
37 | e.preventDefault();
38 |
39 | var amount = this.refs.amount.getValue().trim();
40 |
41 | this.setState({
42 | amount: amount
43 | });
44 |
45 | if (!amount) {
46 | this.props.setAlert('warning', this.props.intl.formatMessage({id: 'form.empty'}));
47 | }
48 | else if (parseFloat(amount) > this.props.user.balanceSub) {
49 | this.props.setAlert('warning',
50 | this.props.intl.formatMessage({id: 'deposit.not_enough'}, {
51 | currency: this.props.market.name,
52 | balance: this.props.user.balanceSub,
53 | amount: amount
54 | })
55 | );
56 | }
57 | else {
58 | this.setState({
59 | newDeposit: true,
60 | confirmMessage:
61 |
66 | });
67 |
68 | this.props.showAlert(false);
69 |
70 | return true;
71 | }
72 |
73 | this.setState({
74 | newDeposit: false
75 | });
76 |
77 | if (showAlerts)
78 | this.props.showAlert(true);
79 |
80 | e.stopPropagation();
81 | },
82 |
83 | onSubmitForm: function(e, el) {
84 | e.preventDefault();
85 |
86 | if (!this.validate(e, el))
87 | return false;
88 |
89 | this.props.flux.actions.user.depositSub({
90 | amount: this.state.amount
91 | });
92 |
93 | this.setState({
94 | amount: null,
95 | newDeposit: false
96 | });
97 | },
98 |
99 | render: function() {
100 | return (
101 |
122 | );
123 | }
124 | }));
125 |
126 | module.exports = SubDeposit;
127 |
--------------------------------------------------------------------------------
/frontend/app/components/SubDepositModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl} from 'react-intl';
3 | import {Button, Input, Modal} from 'react-bootstrap';
4 |
5 | import ConfirmModal from './ConfirmModal';
6 |
7 | let SubDepositModal = injectIntl(React.createClass({
8 | getInitialState() {
9 | return {
10 | amount: null,
11 | newDeposit: false,
12 | isModalOpen: false,
13 | showConfirmModal: false,
14 | amountChanged: false
15 | };
16 | },
17 |
18 | componentDidMount() {
19 | this.validate(new Event('validate'));
20 | },
21 |
22 | componentWillReceiveProps(nextProps) {
23 | if (!this.state.amountChanged)
24 | this.setState({
25 | amount: nextProps.amount
26 | });
27 | },
28 |
29 | onHide(e) {
30 | e.preventDefault();
31 | if (!this.state.showConfirmModal)
32 | this.setState({ amountChanged: false });
33 | this.props.onHide();
34 | },
35 |
36 | openConfirmModal() {
37 | this.props.onHide();
38 | this.setState({
39 | showConfirmModal: true
40 | });
41 | },
42 |
43 | closeConfirmModal() {
44 | this.setState({
45 | amountChanged: false,
46 | showConfirmModal: false
47 | });
48 | },
49 |
50 | handleChange(e) {
51 | e.preventDefault();
52 | this.setState({
53 | amountChanged: true
54 | });
55 | this.validate(e);
56 | },
57 |
58 | handleValidation(e) {
59 | e.preventDefault();
60 | if (this.validate(e, true))
61 | this.openConfirmModal();
62 | },
63 |
64 | validate(e, showAlerts) {
65 | e.preventDefault();
66 | e.stopPropagation();
67 |
68 | var amount = this.refs.amount ? parseFloat(this.refs.amount.getValue().trim()) : 0;
69 |
70 | this.setState({
71 | amount: amount
72 | });
73 |
74 | if (!amount)
75 | this.props.setAlert('warning', this.props.intl.formatMessage({id: 'form.empty'}));
76 | else if (amount > this.props.user.balanceSub)
77 | this.props.setAlert('warning', this.props.intl.formatMessage({id: 'deposit.not_enough'}, {
78 | currency: this.props.market.name,
79 | balance: this.props.user.balanceSub,
80 | amount: amount
81 | })
82 | );
83 | else {
84 | this.setState({
85 | newDeposit: true
86 | });
87 |
88 | this.props.showAlert(false);
89 |
90 | return true;
91 | }
92 |
93 | this.setState({
94 | newDeposit: false
95 | });
96 |
97 | if (showAlerts) {
98 | this.props.showAlert(true);
99 | this.props.onHide();
100 | }
101 |
102 | return false;
103 | },
104 |
105 | onSubmitForm(e, el) {
106 | e.preventDefault();
107 |
108 | if (!this.validate(e, el)) {
109 | this.props.onHide();
110 | return false;
111 | }
112 |
113 | this.props.flux.actions.user.depositSub({
114 | amount: this.state.amount
115 | });
116 |
117 | this.setState({
118 | amount: null,
119 | newDeposit: false
120 | });
121 |
122 | this.props.onHide();
123 | },
124 |
125 | render() {
126 | return (
127 |
128 |
129 |
130 | {this.props.modalTitle}
131 |
132 |
145 |
146 |
157 |
158 | );
159 | }
160 | }));
161 |
162 | module.exports = SubDepositModal;
163 |
--------------------------------------------------------------------------------
/frontend/app/components/SubNavBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router';
3 | import {FormattedMessage} from 'react-intl';
4 | import {Nav} from 'react-bootstrap';
5 |
6 | let SubNavBar = React.createClass({
7 | render() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 | });
34 |
35 | module.exports = SubNavBar;
36 |
--------------------------------------------------------------------------------
/frontend/app/components/SubSend.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl, FormattedMessage} from 'react-intl';
3 | import {Button, Input} from 'react-bootstrap';
4 |
5 | import ConfirmModal from './ConfirmModal';
6 |
7 | let SubSend = injectIntl(React.createClass({
8 | getInitialState: function() {
9 | return {
10 | amount: null,
11 | recipient: null,
12 | newSend: false,
13 | showModal: false,
14 | confirmMessage: null
15 | };
16 | },
17 |
18 | openModal: function() {
19 | this.setState({ showModal: true });
20 | },
21 |
22 | closeModal: function() {
23 | this.setState({ showModal: false });
24 | },
25 |
26 | handleChange: function(e) {
27 | e.preventDefault();
28 | this.validate(e);
29 | },
30 |
31 | handleValidation: function(e) {
32 | e.preventDefault();
33 | if (this.validate(e, true))
34 | this.openModal();
35 | },
36 |
37 | validate: function(e, showAlerts) {
38 | e.preventDefault();
39 |
40 | var address = this.refs.address.getValue().trim();
41 | var amount = this.refs.amount.getValue().trim();
42 |
43 | this.setState({
44 | recipient: address,
45 | amount: amount
46 | });
47 |
48 | if (!address) {
49 | this.props.setAlert('warning', this.props.intl.formatMessage({id: 'form.empty'}));
50 | }
51 | else if (!amount) {
52 | this.props.setAlert('warning', this.props.intl.formatMessage({id: 'form.cheap'}));
53 | }
54 | else if (parseFloat(amount) > this.props.user.balanceSub) {
55 | this.props.setAlert('warning',
56 | this.props.intl.formatMessage({id: 'sub.not_enough'}, {
57 | currency: this.props.market.name,
58 | balance: this.props.user.balanceSub
59 | })
60 | );
61 | }
62 | else if (address.length != 42) {
63 | this.props.setAlert('warning',
64 | this.props.intl.formatMessage({id: 'address.size'}, {
65 | size: (address.length < 42 ? "short" : "long")
66 | })
67 | );
68 | }
69 | else {
70 | this.setState({
71 | newSend: true,
72 | confirmMessage:
73 |
79 | });
80 |
81 | this.props.showAlert(false);
82 |
83 | return true;
84 | }
85 |
86 | this.setState({
87 | newSend: false
88 | });
89 |
90 | if (showAlerts)
91 | this.props.showAlert(true);
92 |
93 | return false;
94 | },
95 |
96 | onSubmitForm: function(e, el) {
97 | e.preventDefault();
98 |
99 | if (!this.validate(e, el))
100 | return false;
101 |
102 | var payload = {
103 | recipient: this.state.recipient,
104 | amount: this.state.amount
105 | };
106 |
107 | this.props.flux.actions.user.sendSub(payload);
108 |
109 | this.setState({
110 | recipient: null,
111 | amount: null,
112 | newSend: false
113 | });
114 | },
115 |
116 | render: function() {
117 | return (
118 |
148 | );
149 | }
150 | }));
151 |
152 | module.exports = SubSend;
153 |
--------------------------------------------------------------------------------
/frontend/app/components/SubTab.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link, State} from 'react-router';
3 |
4 | let SubTab = React.createClass({
5 | mixins: [ State ],
6 |
7 | render: function() {
8 | var active = this.isActive(this.props.to, this.props.params, this.props.query);
9 | var className = active ? 'active' : '';
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | });
18 |
19 | module.exports = SubTab;
20 |
--------------------------------------------------------------------------------
/frontend/app/components/SubWithdraw.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl, FormattedMessage} from 'react-intl';
3 | import {Button, Input} from 'react-bootstrap';
4 |
5 | import ConfirmModal from './ConfirmModal';
6 |
7 | let SubWithdraw = injectIntl(React.createClass({
8 | getInitialState: function() {
9 | return {
10 | amount: null,
11 | recipient: null,
12 | newWithdrawal: false,
13 | showModal: false,
14 | confirmMessage: null
15 | };
16 | },
17 |
18 | openModal: function() {
19 | this.setState({ showModal: true });
20 | },
21 |
22 | closeModal: function() {
23 | this.setState({ showModal: false });
24 | },
25 |
26 | handleChange: function(e) {
27 | e.preventDefault();
28 | this.validate(e);
29 | },
30 |
31 | handleValidation: function(e) {
32 | e.preventDefault();
33 | if (this.validate(e, true))
34 | this.openModal();
35 | },
36 |
37 | validate: function(e, showAlerts) {
38 | e.preventDefault();
39 |
40 | var amount = this.refs.amount.getValue().trim();
41 |
42 | this.setState({
43 | amount: amount
44 | });
45 |
46 | if (!amount) {
47 | this.props.setAlert('warning', this.props.intl.formatMessage({id: 'withdraw.empty'}));
48 | }
49 | else if (parseFloat(amount) > this.props.user.balanceSubAvailable) {
50 | this.props.setAlert('warning',
51 | this.props.intl.formatMessage({id: 'withdraw.not_enough'}, {
52 | currency: this.props.market.name,
53 | balance: this.props.user.balanceSubAvailable,
54 | amount: amount
55 | })
56 | );
57 | }
58 | else {
59 | this.setState({
60 | newWithdrawal: true,
61 | confirmMessage:
62 |
67 | });
68 |
69 | this.props.showAlert(false);
70 |
71 | return true;
72 | }
73 |
74 | this.setState({
75 | newWithdrawal: false
76 | });
77 |
78 | if (showAlerts)
79 | this.props.showAlert(true);
80 |
81 | return false;
82 | },
83 |
84 | onSubmitForm: function(e, el) {
85 | e.preventDefault();
86 |
87 | if (!this.validate(e, el))
88 | return false;
89 |
90 | this.props.flux.actions.user.withdrawSub({
91 | amount: this.state.amount
92 | });
93 |
94 | this.setState({
95 | amount: null,
96 | newWithdrawal: false
97 | });
98 | },
99 |
100 | render: function() {
101 | return (
102 |
125 | );
126 | }
127 | }));
128 |
129 | module.exports = SubWithdraw;
130 |
--------------------------------------------------------------------------------
/frontend/app/components/Tools.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {FormattedMessage} from 'react-intl';
3 | import Perf from 'react-addons-perf';
4 |
5 | import AlertDismissable from './AlertDismissable';
6 | import Network from './Network';
7 | import ConfigPane from './ConfigPane';
8 | import SubRegister from './SubRegister';
9 |
10 | import utils from '../js/utils';
11 |
12 | let TradeList = React.createClass({
13 |
14 | getInitialState: function() {
15 | return {
16 | alertLevel: 'info',
17 | alertMessage: ''
18 | };
19 | },
20 |
21 | componentDidMount: function() {
22 | if (this.props.flux.stores.config.debug && Perf) {
23 | var measurements = Perf.stop();
24 | Perf.printInclusive(measurements);
25 | utils.debug("Inclusive", "^");
26 | Perf.printExclusive(measurements);
27 | utils.debug("Exclusive", "^");
28 | Perf.printWasted(measurements);
29 | utils.debug("Wasted", "^");
30 | }
31 | },
32 |
33 | setAlert: function(alertLevel, alertMessage) {
34 | this.setState({
35 | alertLevel: alertLevel,
36 | alertMessage: alertMessage
37 | });
38 | },
39 |
40 | showAlert: function(show) {
41 | this.refs.alerts.setState({alertVisible: show});
42 | },
43 |
44 | render: function() {
45 | var address = this.props.flux.stores.config.getState().address;
46 |
47 | return (
48 |
49 |
50 |
51 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | );
72 | }
73 | });
74 |
75 | module.exports = TradeList;
76 |
--------------------------------------------------------------------------------
/frontend/app/components/TradeForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl} from 'react-intl';
3 | import {DropdownButton, MenuItem} from 'react-bootstrap';
4 |
5 | import AlertDismissable from './AlertDismissable';
6 | import TradeFormInstance from './TradeFormInstance';
7 |
8 | let TradeForm = injectIntl(React.createClass({
9 | getInitialState() {
10 | return {
11 | type: 1,
12 | typename: this.props.intl.formatMessage({id:'form.buy'}),
13 | alertLevel: 'info',
14 | alertMessage: ''
15 | };
16 | },
17 |
18 | componentWillReceiveProps(nextProps) {
19 | if (nextProps.trades.newAmount && nextProps.trades.type != this.props.type)
20 | this.setState({
21 | type: nextProps.trades.type,
22 | typename: nextProps.trades.type == 1 ?
23 | this.props.intl.formatMessage({id: 'form.buy'}) :
24 | this.props.intl.formatMessage({id: 'form.sell'})
25 | });
26 | },
27 |
28 | setAlert(alertLevel, alertMessage) {
29 | this.setState({
30 | alertLevel: alertLevel,
31 | alertMessage: alertMessage
32 | });
33 | },
34 |
35 | showAlert(show) {
36 | this.refs.alerts.setState({alertVisible: show});
37 | },
38 |
39 | handleType(e, key) {
40 | e.preventDefault();
41 |
42 | this.setState({
43 | type: key,
44 | typename: this.refs.type.props.children[key - 1].props.children
45 | });
46 | this.props.flux.actions.trade.switchType(key);
47 | },
48 |
49 | render() {
50 | var formatMessage = this.props.intl.formatMessage;
51 | return (
52 |
53 |
54 |
55 |
56 |
{formatMessage({id:'form.new'})}
57 |
58 |
59 | {formatMessage({id:'form.buyorsell'})}
60 |
67 | Buy
68 | Sell
69 |
70 |
{formatMessage({id:'form.new'})}
71 |
72 |
73 |
74 |
75 |
82 |
83 |
84 |
85 |
{formatMessage({id:'form.buy'})}
86 |
89 |
90 |
91 |
92 |
93 |
{formatMessage({id:'form.sell'})}
94 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | }
105 | }));
106 |
107 | module.exports = TradeForm;
108 |
--------------------------------------------------------------------------------
/frontend/app/components/TradeList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl} from 'react-intl';
3 | import {ProgressBar} from 'react-bootstrap';
4 |
5 | import AlertDismissable from './AlertDismissable';
6 | import ConfirmModal from './ConfirmModal';
7 | import TradeBuys from './TradeListBuys';
8 | import TradeSells from './TradeListSells';
9 |
10 | let TradeList = injectIntl(React.createClass({
11 | getInitialState: function() {
12 | return {
13 | showModal: false,
14 | trade: null,
15 | message: null,
16 | submit: null
17 | };
18 | },
19 |
20 | openModal: function(trade) {
21 | var submit = (trade.owner == this.props.user.user.id) ?
22 | function() {
23 | this.handleCancelTrade(trade);
24 | }.bind(this) :
25 | function() {
26 | this.handleFillTrade(trade);
27 | }.bind(this);
28 |
29 | if (trade.owner != this.props.user.user.id)
30 | this.props.flux.actions.trade.estimateFillTrades([trade]);
31 |
32 | this.setState({
33 | showModal: true,
34 | trade: trade,
35 | message: trade.owner == this.props.user.user.id ?
36 | this.props.intl.formatMessage({id: 'trade.cancel'}) :
37 | this.props.intl.formatMessage({id: 'trade.confirm'}, {
38 | type: trade.type == "buys" ? "sell" : "buy",
39 | amount: trade.amount,
40 | price: trade.price,
41 | currency: this.props.market.market.name,
42 | total: trade.amount * trade.price
43 | }),
44 | submit: submit
45 | });
46 | },
47 |
48 | closeModal: function() {
49 | this.setState({ showModal: false });
50 | },
51 |
52 | handleFillTrade: function(trade) {
53 | this.props.flux.actions.trade.fillTrade(trade);
54 | },
55 |
56 | handleCancelTrade: function(trade) {
57 | this.props.flux.actions.trade.cancelTrade(trade);
58 | },
59 |
60 | render: function() {
61 | return (
62 |
63 |
64 |
65 | {this.props.trades.loading &&
66 |
}
67 |
68 |
69 |
70 | { this.props.trades.error &&
71 |
}
74 |
75 | {
76 | (this.props.trades.type == 1) ?
77 |
78 |
80 |
82 |
:
83 |
84 |
86 |
88 |
89 | }
90 |
91 |
99 |
100 | {
101 | // Per trade modal...
102 | }
103 |
114 |
115 | );
116 | }
117 | }));
118 |
119 | module.exports = TradeList;
120 |
--------------------------------------------------------------------------------
/frontend/app/components/TradeListBuys.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {FormattedMessage} from 'react-intl';
3 |
4 | import TradeTable from './TradeTable';
5 |
6 | let TradeBuys = React.createClass({
7 | render: function() {
8 | return (
9 |
10 |
11 | }
13 | type={1} trades={this.props.trades} tradeList={this.props.trades.tradeBuys}
14 | market={this.props.market} user={this.props.user.user} listOwn={this.props.listOwn} />
15 |
16 |
17 | );
18 | }
19 | });
20 |
21 | module.exports = TradeBuys;
22 |
--------------------------------------------------------------------------------
/frontend/app/components/TradeListSells.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {FormattedMessage} from 'react-intl';
3 |
4 | import TradeTable from './TradeTable';
5 |
6 | let TradeSells = React.createClass({
7 | render: function() {
8 | return (
9 |
10 |
11 | }
13 | type={2} trades={this.props.trades} tradeList={this.props.trades.tradeSells}
14 | market={this.props.market} user={this.props.user.user} listOwn={this.props.listOwn} />
15 |
16 |
17 | );
18 | }
19 | });
20 |
21 | module.exports = TradeSells;
22 |
--------------------------------------------------------------------------------
/frontend/app/components/TradeRow.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import {FormattedNumber} from 'react-intl';
4 | import {Glyphicon, Popover, OverlayTrigger} from 'react-bootstrap';
5 |
6 | import utils from '../js/utils';
7 |
8 | let TradeRow = React.createClass({
9 | getInitialState: function() {
10 | return {
11 | payload: {},
12 | showModal: false
13 | };
14 | },
15 |
16 | openModal: function(e) {
17 | e.preventDefault();
18 | e.stopPropagation();
19 | this.props.openModal(this.props.trade);
20 | },
21 |
22 | handleHover: function(e) {
23 | e.preventDefault();
24 | if (this.props.review)
25 | return;
26 |
27 | if (!this.props.trade.price || !this.props.trade.amount || !this.props.trade.total)
28 | return;
29 |
30 | // Select previous trades
31 | var trades = _.filter(this.props.tradeList, function(trade, i) {
32 | return (
33 | this.props.user.id != trade.owner &&
34 | // trade.status != "filling" &&
35 | trade.status != "pending" &&
36 | trade.status != "new" &&
37 | ((trade.type == "buys" && this.props.trade.price <= trade.price) ||
38 | (trade.type == "sells" && this.props.trade.price >= trade.price)) &&
39 | i <= this.props.count
40 | );
41 | }.bind(this));
42 |
43 | if (!trades.length)
44 | return;
45 |
46 | var totalAmount = 0;
47 | var totalValue = 0;
48 |
49 | totalAmount = _.reduce(_.map(trades, 'amount'), function(sum, num) {
50 | return parseFloat(sum) + parseFloat(num);
51 | });
52 | if (!totalAmount)
53 | return;
54 |
55 | totalValue = _.reduce(_.map(trades, 'total'), function(sum, num) {
56 | return parseFloat(sum) + parseFloat(num);
57 | });
58 | if (!totalValue)
59 | return;
60 |
61 | _.forEach(this.props.tradeList, function(trade) {
62 | // if (trade.status == "filling" && _.find(trades, {'id': trade.id}))
63 | // trade.status = "mined";
64 | if (trade.status == "mined" && _.find(trades, {'id': trade.id}))
65 | trade.status = "filling";
66 | });
67 |
68 | this.setState({
69 | payload: {
70 | type: (this.props.trade.type == "buys" ? 1 : 2),
71 | fills: trades.length,
72 | price: this.props.trade.price,
73 | amount: totalAmount,
74 | total: totalValue,
75 | market: this.props.trade.market,
76 | user: this.props.user
77 | }
78 | });
79 | },
80 |
81 | handleHoverOut: function(e) {
82 | e.preventDefault();
83 | if (!this.props.trades)
84 | return;
85 |
86 | this.props.flux.actions.trade.highlightFilling({
87 | type: this.props.trade.type == "buys" ? 2 : 1,
88 | price: this.props.trades.price,
89 | amount: this.props.trades.amount,
90 | total: this.props.trades.total,
91 | market: this.props.market,
92 | user: this.props.user
93 | });
94 | },
95 |
96 | handleClick: function(e) {
97 | e.preventDefault();
98 | if (this.props.review)
99 | return;
100 |
101 | if (this.state.payload)
102 | this.props.flux.actions.trade.clickFill(this.state.payload);
103 | },
104 |
105 | render: function() {
106 | return (
107 |
111 |
112 |
113 | {utils.numeral(this.props.trade.amount, this.props.market.decimals)}
114 |
115 |
116 |
117 |
118 | {this.props.trade.market.name}
119 |
120 |
121 |
122 |
123 | {utils.numeral(this.props.trade.price, this.props.market.priceDecimals)}
124 |
125 |
126 |
127 |
128 | ETH
129 |
130 |
131 |
132 | {!this.props.review &&
133 |
135 |
136 | ID: {this.props.trade.id}
137 |
138 |
139 | By: {this.props.trade.owner}
140 |
141 | }>
142 |
147 |
148 | }
149 |
150 |
151 | );
152 | }
153 | });
154 | module.exports = TradeRow;
155 |
--------------------------------------------------------------------------------
/frontend/app/components/TradeTable.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import {FormattedMessage} from 'react-intl';
4 | import {Table} from 'react-bootstrap';
5 |
6 | // var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
7 | import TransitionGroup from './TransitionGroup';
8 | import TradeRow from './TradeRow';
9 |
10 | let TradeTable = React.createClass({
11 | render: function() {
12 | var index = _.findIndex(this.props.market.markets, {'id': this.props.market.market.id});
13 | var market = this.props.market.markets[index];
14 | var tradeRows = this.props.tradeList.map(function (trade, i) {
15 | return (
16 |
23 | );
24 | }.bind(this));
25 |
26 | return (
27 |
28 |
29 |
{this.props.title}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | { !this.props.review &&
40 | }
41 |
42 |
43 |
44 |
45 | {tradeRows}
46 |
47 |
48 |
49 | );
50 | }
51 | });
52 |
53 | module.exports = TradeTable;
54 |
--------------------------------------------------------------------------------
/frontend/app/components/Trades.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import TradeForm from './TradeForm';
4 | import TradeList from './TradeList';
5 |
6 | let Trades = React.createClass({
7 | render: function() {
8 | return (
9 |
10 | {!this.props.market.error &&
11 | }
12 | {!this.props.market.error &&
13 | }
14 |
15 | );
16 | }
17 |
18 | });
19 |
20 | module.exports = Trades;
21 |
--------------------------------------------------------------------------------
/frontend/app/components/TransitionGroup.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * The CSSTransitionGroup component uses the 'transitionend' event, which
3 | * browsers will not send for any number of reasons, including the
4 | * transitioning node not being painted or in an unfocused tab.
5 | *
6 | * This TimeoutTransitionGroup instead uses a user-defined timeout to determine
7 | * when it is a good time to remove the component. Currently there is only one
8 | * timeout specified, but in the future it would be nice to be able to specify
9 | * separate timeouts for enter and leave, in case the timeouts for those
10 | * animations differ. Even nicer would be some sort of inspection of the CSS to
11 | * automatically determine the duration of the animation or transition.
12 | *
13 | * This is adapted from Facebook's CSSTransitionGroup which is in the React
14 | * addons and under the Apache 2.0 License.
15 | */
16 |
17 | import React from 'react';
18 | import ReactTransitionGroup from 'react-addons-transition-group';
19 | import TransitionGroupChild from './TransitionGroupChild';
20 |
21 | let TransitionGroup = React.createClass({
22 | propTypes: {
23 | enterTimeout: React.PropTypes.number.isRequired,
24 | leaveTimeout: React.PropTypes.number.isRequired,
25 | transitionName: React.PropTypes.string.isRequired,
26 | transitionEnter: React.PropTypes.bool,
27 | transitionLeave: React.PropTypes.bool
28 | },
29 |
30 | getDefaultProps: function() {
31 | return {
32 | transitionEnter: true,
33 | transitionLeave: true
34 | };
35 | },
36 |
37 | _wrapChild: function(child) {
38 | return (
39 |
45 | {child}
46 |
47 | );
48 | },
49 |
50 | render: function() {
51 | return (
52 |
55 | );
56 | }
57 | });
58 |
59 | module.exports = TransitionGroup;
60 |
--------------------------------------------------------------------------------
/frontend/app/components/TransitionGroupChild.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 |
5 | var TICK = 17;
6 | var endEvents = [];
7 |
8 | /**
9 | * EVENT_NAME_MAP is used to determine which event fired when a
10 | * transition/animation ends, based on the style property used to
11 | * define that event.
12 | */
13 | var EVENT_NAME_MAP = {
14 | transitionend: {
15 | 'transition': 'transitionend',
16 | 'WebkitTransition': 'webkitTransitionEnd',
17 | 'MozTransition': 'mozTransitionEnd',
18 | 'OTransition': 'oTransitionEnd',
19 | 'msTransition': 'MSTransitionEnd'
20 | },
21 |
22 | animationend: {
23 | 'animation': 'animationend',
24 | 'WebkitAnimation': 'webkitAnimationEnd',
25 | 'MozAnimation': 'mozAnimationEnd',
26 | 'OAnimation': 'oAnimationEnd',
27 | 'msAnimation': 'MSAnimationEnd'
28 | }
29 | };
30 |
31 | (function detectEvents() {
32 | if (typeof window === "undefined") {
33 | return;
34 | }
35 |
36 | var testEl = document.createElement('div');
37 | var style = testEl.style;
38 |
39 | // On some platforms, in particular some releases of Android 4.x, the
40 | // un-prefixed "animation" and "transition" properties are defined on the
41 | // style object but the events that fire will still be prefixed, so we need
42 | // to check if the un-prefixed events are useable, and if not remove them
43 | // from the map
44 | if (!('AnimationEvent' in window)) {
45 | delete EVENT_NAME_MAP.animationend.animation;
46 | }
47 |
48 | if (!('TransitionEvent' in window)) {
49 | delete EVENT_NAME_MAP.transitionend.transition;
50 | }
51 |
52 | for (var baseEventName in EVENT_NAME_MAP) {
53 | if (EVENT_NAME_MAP.hasOwnProperty(baseEventName)) {
54 | var baseEvents = EVENT_NAME_MAP[baseEventName];
55 | for (var styleName in baseEvents) {
56 | if (styleName in style) {
57 | endEvents.push(baseEvents[styleName]);
58 | break;
59 | }
60 | }
61 |
62 | }
63 | }
64 | })();
65 |
66 | function animationSupported() {
67 | return endEvents.length !== 0;
68 | }
69 |
70 | /**
71 | * Functions for element class management to replace dependency on jQuery
72 | * addClass, removeClass and hasClass
73 | */
74 | var hasClass = function (element, className) {
75 | if (element.classList) {
76 | return element.classList.contains(className);
77 | } else {
78 | return (' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1;
79 | }
80 | };
81 |
82 | var addClass = function (element, className) {
83 | if (element.classList) {
84 | element.classList.add(className);
85 | } else if (!hasClass(element, className)) {
86 | element.className = element.className + ' ' + className;
87 | }
88 | return element;
89 | };
90 |
91 | var removeClass = function (element, className) {
92 | if (hasClass(className)) {
93 | if (element.classList) {
94 | element.classList.remove(className);
95 | } else {
96 | element.className = (' ' + element.className + ' ')
97 | .replace(' ' + className + ' ', ' ').trim();
98 | }
99 | }
100 | return element;
101 | };
102 |
103 | let TransitionGroupChild = React.createClass({
104 | componentWillMount: function() {
105 | this.classNameQueue = [];
106 | },
107 |
108 | componentWillUnmount: function() {
109 | if (this.timeout) {
110 | clearTimeout(this.timeout);
111 | }
112 | if (this.animationTimeout) {
113 | clearTimeout(this.animationTimeout);
114 | }
115 | },
116 |
117 | transition: function(animationType, finishCallback) {
118 | var node = ReactDOM.findDOMNode(this);
119 | var className = this.props.name + '-' + animationType;
120 | var activeClassName = className + '-active';
121 |
122 | var endListener = function() {
123 | removeClass(node, className);
124 | removeClass(node, activeClassName);
125 |
126 | // Usually this optional callback is used for informing an owner of
127 | // a leave animation and telling it to remove the child.
128 | if (finishCallback)
129 | finishCallback();
130 | };
131 |
132 | if (!animationSupported()) {
133 | endListener();
134 | } else {
135 | if (animationType === "enter") {
136 | this.animationTimeout = setTimeout(endListener,
137 | this.props.enterTimeout);
138 | } else if (animationType === "leave") {
139 | this.animationTimeout = setTimeout(endListener,
140 | this.props.leaveTimeout);
141 | }
142 | }
143 |
144 | addClass(node, className);
145 |
146 | // Need to do this to actually trigger a transition.
147 | this.queueClass(activeClassName);
148 | },
149 |
150 | queueClass: function(className) {
151 | this.classNameQueue.push(className);
152 |
153 | if (!this.timeout) {
154 | this.timeout = setTimeout(this.flushClassNameQueue, TICK);
155 | }
156 | },
157 |
158 | flushClassNameQueue: function() {
159 | this.classNameQueue.forEach(function(name) {
160 | addClass(ReactDOM.findDOMNode(this), name);
161 | }.bind(this));
162 | this.classNameQueue.length = 0;
163 | this.timeout = null;
164 | },
165 |
166 | componentWillEnter: function(done) {
167 | if (this.props.enter) {
168 | this.transition('enter', done);
169 | } else {
170 | done();
171 | }
172 | },
173 |
174 | componentWillLeave: function(done) {
175 | if (this.props.leave) {
176 | this.transition('leave', done);
177 | } else {
178 | done();
179 | }
180 | },
181 |
182 | render: function() {
183 | return React.Children.only(this.props.children);
184 | }
185 | });
186 |
187 | module.exports = TransitionGroupChild;
188 |
--------------------------------------------------------------------------------
/frontend/app/components/TxRow.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl, FormattedMessage, FormattedNumber} from 'react-intl';
3 | import {Button, Popover, OverlayTrigger} from 'react-bootstrap';
4 | import bigRat from 'big-rational';
5 |
6 | let TxRow = injectIntl(React.createClass({
7 | render: function() {
8 | var amount = bigRat(this.props.tx.amount).divide(Math.pow(10, this.props.market.decimals)).valueOf();
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | { this.props.tx.inout }
19 |
20 |
21 |
22 |
23 | { this.props.tx.type }
24 |
25 |
26 |
27 |
28 |
29 | { this.props.tx.from }
30 |
31 |
32 |
33 | { this.props.tx.to }
34 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
48 | { this.props.tx.price ?
49 | :
50 | 'N/A' }
51 |
52 |
53 |
54 |
55 | { this.props.tx.total.value ?
56 | :
61 | "N/A" }
62 |
63 |
64 |
65 |
67 |
68 | { this.props.intl.formatMessage({id: 'txs.hash'}) }
69 |
70 | { this.props.tx.hash }
71 |
72 |
73 | { this.props.tx.id &&
74 |
75 | { this.props.intl.formatMessage({id: 'txs.id'}) }
76 |
77 | { this.props.tx.id }
78 |
79 |
}
80 | }>
81 |
82 |
83 | { this.props.tx.details }
84 |
85 |
86 |
87 |
88 |
89 | );
90 | }
91 | }));
92 |
93 | module.exports = TxRow;
94 |
--------------------------------------------------------------------------------
/frontend/app/components/TxsList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {FormattedMessage} from 'react-intl';
3 | import {ProgressBar} from 'react-bootstrap';
4 |
5 | import AlertDismissable from './AlertDismissable';
6 | import RangeSelect from './RangeSelect';
7 | import TxsTable from './TxsTable';
8 |
9 | let TxsList = React.createClass({
10 | render: function() {
11 | return (
12 |
13 |
14 |
15 |
{ this.props.title } {
16 | this.props.market.loading &&
17 | ... }
18 |
19 |
20 |
21 |
22 | {this.props.market.loading ?
23 |
:
24 |
}
25 |
26 |
27 |
28 | {this.props.market.market.error &&
29 |
}
30 |
31 |
32 |
33 |
34 | );
35 | }
36 | });
37 |
38 | module.exports = TxsList;
39 |
--------------------------------------------------------------------------------
/frontend/app/components/TxsTable.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import {FormattedMessage} from 'react-intl';
4 | import {Table} from 'react-bootstrap';
5 |
6 | import TransitionGroup from './TransitionGroup';
7 | import TxRow from './TxRow';
8 |
9 | let TxsTable = React.createClass({
10 | getInitialState: function() {
11 | var index = _.findIndex(this.props.market.markets, {'id': this.props.market.market.id});
12 | var market = this.props.market.markets[index];
13 | return {
14 | market: market
15 | };
16 | },
17 |
18 | componentDidMount: function() {
19 | this.componentWillReceiveProps(this.props);
20 | },
21 |
22 | componentWillReceiveProps: function(nextProps) {
23 | var index = _.findIndex(nextProps.market.markets, {'id': nextProps.market.market.id});
24 | var market = nextProps.market.markets[index];
25 |
26 | this.setState({
27 | market: market
28 | });
29 | },
30 |
31 | render: function() {
32 | var txsRows = _.sortBy(this.props.txs, 'block').map(function (tx) {
33 | return (
34 |
35 | );
36 | }.bind(this));
37 | txsRows.reverse();
38 |
39 | return (
40 |
41 |
{this.props.title}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | { txsRows }
57 |
58 |
59 |
60 | );
61 | }
62 | });
63 |
64 | module.exports = TxsTable;
65 |
--------------------------------------------------------------------------------
/frontend/app/components/UserAddress.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl, FormattedMessage, FormattedNumber} from 'react-intl';
3 | import {DropdownButton, MenuItem, Input, FormControls} from 'react-bootstrap';
4 | import UAParser from 'ua-parser-js';
5 | import QRCode from 'react-qr';
6 |
7 | import Clipboard from './Clipboard';
8 |
9 | import utils from '../js/utils';
10 |
11 | let UserAddress = injectIntl(React.createClass({
12 | getInitialState() {
13 | return {
14 | ctrl: new UAParser(navigator.userAgent).getOS().name == 'Mac OS' ? 'CMD' : 'CTRL'
15 | };
16 | },
17 |
18 | handleChange(e, address) {
19 | e.preventDefault();
20 |
21 | this.props.flux.actions.user.switchAddress({
22 | address: address
23 | });
24 | },
25 |
26 | handleCopy() {
27 | utils.log("Copied", this.props.user.id);
28 | },
29 |
30 | render() {
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
68 |
69 | );
70 | }
71 | }));
72 |
73 | module.exports = UserAddress;
74 |
--------------------------------------------------------------------------------
/frontend/app/components/UserBalances.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {FormattedMessage, FormattedNumber} from 'react-intl';
3 |
4 | let UserBalances = React.createClass({
5 | render() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ETH
18 |
19 |
20 |
21 |
22 |
23 |
24 | ( wei)
25 |
26 |
27 |
28 |
29 | { this.props.market.name }
30 |
31 |
32 |
33 |
34 |
35 |
44 |
45 |
46 | );
47 | }
48 | });
49 |
50 | module.exports = UserBalances;
51 |
--------------------------------------------------------------------------------
/frontend/app/components/UserDetails.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import {FormattedMessage} from 'react-intl';
4 |
5 | import UserBalances from './UserBalances';
6 | import UserAddress from './UserAddress';
7 | import TradeList from './TradeList';
8 |
9 | let UserDetails = React.createClass({
10 | getInitialState() {
11 | return {
12 | own: false
13 | };
14 | },
15 |
16 | componentDidMount() {
17 | this.componentWillReceiveProps(this.props);
18 | },
19 |
20 | componentWillReceiveProps(nextProps) {
21 | if (nextProps.user.user.id) {
22 | var own = {tradeBuys: [], tradeSells: []};
23 | if (this.isYours(nextProps)) {
24 | own.tradeBuys = _.filter(nextProps.trades.tradeBuys, {'owner': nextProps.user.user.id});
25 | own.tradeSells = _.filter(nextProps.trades.tradeSells, {'owner': nextProps.user.user.id});
26 | own.title = ;
27 | }
28 | this.setState({
29 | own: own
30 | });
31 | }
32 | else
33 | this.setState({
34 | own: false
35 | });
36 | },
37 |
38 | isYours(nextProps) {
39 | return (
40 | nextProps.user &&
41 | nextProps.trades &&
42 | (nextProps.trades.tradeBuys.length > 0) ||
43 | (nextProps.trades.tradeSells.length > 0)
44 | );
45 | },
46 |
47 | render() {
48 | return (
49 |
50 |
51 |
52 |
53 |
54 | { this.state.own ?
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
:
63 |
}
64 |
65 | { this.state.own &&
66 |
}
67 |
68 | );
69 | }
70 | });
71 |
72 | module.exports = UserDetails;
73 |
--------------------------------------------------------------------------------
/frontend/app/components/UserLink.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router';
3 |
4 | let UserLink = React.createClass({
5 | propTypes: {
6 | address: React.PropTypes.string.isRequired
7 | },
8 |
9 | render() {
10 | return (
11 |
12 | { this.props.showIcon &&
13 | } { this.props.address.substr(0, 8) + '\u2026' }
14 |
15 | );
16 | }
17 | });
18 |
19 | module.exports = UserLink;
20 |
--------------------------------------------------------------------------------
/frontend/app/components/Wallet.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl, FormattedMessage} from 'react-intl';
3 | import {Tabs, Tab} from 'react-bootstrap';
4 |
5 | import AlertDismissable from './AlertDismissable';
6 | import SendEther from './SendEther';
7 | import SubSend from './SubSend';
8 | import SubDeposit from './SubDeposit';
9 | import SubWithdraw from './SubWithdraw';
10 | import TxsList from './TxsList';
11 |
12 | let Wallet = injectIntl(React.createClass({
13 | getInitialState() {
14 | return {
15 | alertLevel: 'info',
16 | alertMessage: ''
17 | };
18 | },
19 |
20 | componentDidMount() {
21 | this.props.flux.actions.config.updateAlertCount(null);
22 | },
23 |
24 | setAlert(alertLevel, alertMessage) {
25 | this.setState({
26 | alertLevel: alertLevel,
27 | alertMessage: alertMessage
28 | });
29 | },
30 |
31 | showAlert(show) {
32 | this.refs.alerts.setState({alertVisible: show});
33 | },
34 |
35 | deposit() {
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
49 |
50 | );
51 | },
52 | withdraw() {
53 | return (
54 |
55 |
56 |
57 |
58 |
59 |
60 |
66 |
67 | );
68 | },
69 | transfer() {
70 | return (
71 |
72 |
73 |
74 |
75 |
76 |
77 |
83 |
84 | );
85 | },
86 | sendEther() {
87 | return (
88 |
89 |
90 |
91 |
92 |
93 |
94 |
100 |
101 | );
102 | },
103 |
104 | render() {
105 | return (
106 |
107 |
108 |
109 |
110 |
111 |
112 | { this.deposit() }
113 |
114 |
115 | { this.withdraw() }
116 |
117 |
118 | { this.transfer() }
119 |
120 |
121 | { this.sendEther() }
122 |
123 |
124 |
125 |
126 | { this.deposit() }
127 | { this.withdraw() }
128 | { this.transfer() }
129 | { this.sendEther() }
130 |
131 |
132 |
133 |
134 | {(!this.props.market.market.txs.error) &&
135 | }
137 |
138 |
139 |
140 | );
141 | }
142 |
143 | }));
144 |
145 | module.exports = Wallet;
146 |
--------------------------------------------------------------------------------
/frontend/app/components/btcswap/Blocks.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl, FormattedNumber, FormattedMessage} from 'react-intl';
3 | import {Button, Popover, OverlayTrigger} from 'react-bootstrap';
4 |
5 | let Blocks = injectIntl(React.createClass({
6 | getInitialState: function () {
7 | return {
8 | updatingBtcHeaders: false
9 | };
10 | },
11 |
12 | handleUpdateBlockHeader(e) {
13 | e.preventDefault();
14 |
15 | this.setState({
16 | updatingBtcHeaders: true
17 | });
18 |
19 | this.props.flux.actions.ticket.updateBlockHeader();
20 | },
21 |
22 | render() {
23 | return (
24 |
25 |
26 | { this.props.ticket &&
27 |
29 | BTC block # { this.props.intl.formatNumber(this.props.ticket.btcHeight) }
30 | BTC block hash: { this.props.ticket.btcHead }
31 | Block validation fee: { this.props.ticket.formattedBlockFee.value } { this.props.ticket.formattedBlockFee.unit }
32 | Real BTC block # { this.props.intl.formatNumber(this.props.ticket.btcRealHeight) }
33 | Real BTC block hash: { this.props.ticket.btcRealHead }
34 | { this.props.ticket.btcBehind &&
35 |
36 | { this.props.intl.formatMessage({id: 'btc.behind'}, {
37 | behind: this.props.ticket.btcBehind
38 | }) }
39 |
}
40 | }>
41 |
42 | BTC block #
43 |
44 | }
45 | { (this.props.ticket && this.props.ticket.btcBehind) &&
46 |
48 | { this.props.intl.formatMessage({id: 'btc.behind'}, {
49 | behind: this.props.ticket.btcBehind
50 | }) }
51 | }>
52 |
55 |
56 |
57 | }
58 |
59 |
60 | );
61 | }
62 | }));
63 |
64 | module.exports = Blocks;
65 |
--------------------------------------------------------------------------------
/frontend/app/components/btcswap/CreateTicket.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl, FormattedMessage} from 'react-intl';
3 | import {Button, Input} from 'react-bootstrap';
4 |
5 | import ConfirmModal from '../ConfirmModal';
6 | import AlertDismissable from '../AlertDismissable';
7 |
8 | import Nav from './Nav';
9 | import Blocks from './Blocks';
10 |
11 | let CreateTicket = injectIntl(React.createClass({
12 | getInitialState() {
13 | return {
14 | isValid: false,
15 | showModal: false,
16 | alertLevel: 'info',
17 | alertMessage: null
18 | };
19 | },
20 |
21 | setAlert: function(alertLevel, alertMessage) {
22 | this.setState({
23 | alertLevel: alertLevel,
24 | alertMessage: alertMessage
25 | });
26 | },
27 |
28 | showAlert: function(show) {
29 | this.refs.alerts.setState({alertVisible: show});
30 | },
31 |
32 | openModal: function() {
33 | this.setState({ showModal: true });
34 | },
35 |
36 | closeModal: function() {
37 | this.setState({ showModal: false });
38 | },
39 |
40 | validate: function(showAlerts) {
41 | var address = this.refs.address.getValue();
42 | var amount = this.refs.amount.getValue().trim();
43 | var btc = this.refs.btc.getValue().trim();
44 |
45 | this.setState({
46 | address: address,
47 | amount: amount,
48 | btc: btc,
49 | price: (parseFloat(btc) / parseFloat(amount)).toFixed(8)
50 | });
51 |
52 | if (!amount || !btc || !address) {
53 | this.setAlert('warning', this.props.intlformatMessage({id: 'form.empty'}));
54 | }
55 | else {
56 | this.setState({
57 | isValid: true,
58 | confirmMessage:
67 | });
68 |
69 | this.showAlert(false);
70 |
71 | return true;
72 | }
73 |
74 | this.setState({
75 | isValid: false
76 | });
77 |
78 | if (showAlerts)
79 | this.showAlert(true);
80 |
81 | return false;
82 | },
83 |
84 | handleChange(e) {
85 | e.preventDefault();
86 | this.validate(false);
87 | },
88 |
89 | handleValidation(e) {
90 | e.preventDefault();
91 | if (this.validate(true))
92 | this.openModal();
93 | },
94 |
95 | handleSubmit(e) {
96 | e.preventDefault();
97 |
98 | if (!this.validate(true))
99 | return false;
100 |
101 | this.props.flux.actions.ticket.createTicket(this.state.address, this.state.amount, this.state.btc);
102 |
103 | this.setState({
104 | address: null,
105 | amount: null,
106 | btc: null,
107 | price: null,
108 | isValid: false
109 | });
110 |
111 | // setTimeout(function() {
112 | // this.context.router.transitionTo('ticket', {ticketId: '-'});
113 | // }.bind(this), 300);
114 | },
115 |
116 | render() {
117 | return (
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | Offer your ether for bitcoin
129 |
130 |
131 |
132 |
161 |
162 |
163 |
164 |
165 |
172 |
173 | );
174 | }
175 | }));
176 |
177 | module.exports = CreateTicket;
178 |
--------------------------------------------------------------------------------
/frontend/app/components/btcswap/Help.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router';
3 |
4 | import Nav from './Nav';
5 | import Blocks from './Blocks';
6 |
7 | let Help = React.createClass({
8 |
9 | render() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
Buying Ether
17 |
18 |
19 | Choose the ticket you want to purchase
20 | ether from and click the Reserve button for that ticket.
21 |
22 |
23 | Generate an intermediate BTC wallet in the Reserve section by
24 | clicking on the Generate wallet button.
25 |
26 |
27 | Send the total BTC amount of the ticket plus a 0.3mBTC miner fee
28 | to your intermediate address. You can still recover your bitcoins
29 | if you change your mind later on, at least until you broadcast
30 | the transaction you'll generate on step 5.
31 |
32 | Wait for at least 3 confirmations.
33 |
34 | Click on Create transaction to generate a signed transaction.
35 | Do NOT broadcast the transaction just yet, you only need
36 | the transaction hash.
37 |
38 |
39 | Your transaction hash should show up in the Proof of Work panel.
40 | Click Compute to get a valid nonce. This can take up to a
41 | few minutes on slower computers.
42 |
43 |
44 | Once you have a valid nonce, click Verify to make sure
45 | your nonce is valid, then click Reserve .
46 |
47 | After the ticket is successfully reserved, broadcast the signed
48 | Bitcoin Transaction to the Bitcoin network by clicking on Broadcast
49 | transaction .
50 |
51 | Wait for 6 confirmations.
52 |
53 | Go the the Claim section and lookup your
54 | ticket ID.
55 |
56 | Click Claim .
57 |
58 |
Notes:
59 |
60 |
61 | A ticket is exclusively reserved for 2 hours; afterwards, anyone with ether
62 | can Claim the ticket (thus getting the ether fee). If a further 2 hours
63 | elapses without the ticket being claimed, the ticket becomes open for
64 | someone else to Reserve.
65 |
66 |
67 | If you do not have any ether already, you will need to provide the Bitcoin
68 | transaction hash (only the hash) and Proof of Work, to a 3rd party who has ether
69 | and can Reserve a ticket for you. Once the ticket is reserved, broadcast your
70 | Bitcoin transaction and then inform the 3rd party to Claim the ticket on your
71 | behalf. If the 3rd party doesn't Claim within 2 hours of the reservation,
72 | any other 3rd party may Claim the ticket for you (and get the ether fee).
73 |
74 |
75 |
76 |
77 | );
78 | }
79 | });
80 |
81 | module.exports = Help;
82 |
--------------------------------------------------------------------------------
/frontend/app/components/btcswap/Nav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router';
3 | import {FormattedMessage} from 'react-intl';
4 | import {Nav, Glyphicon} from 'react-bootstrap';
5 |
6 | let BtcNav = React.createClass({
7 | render() {
8 | return (
9 |
10 |
11 |
12 | ether
13 |
14 |
15 |
16 |
17 | ether
18 |
19 |
20 |
21 |
22 | Reserve
23 |
24 |
25 |
26 |
27 | Claim
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 | });
39 |
40 | module.exports = BtcNav;
41 |
--------------------------------------------------------------------------------
/frontend/app/components/btcswap/TicketDetails.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import utils from '../../js/utils';
4 |
5 | let TicketDetails = React.createClass({
6 | render() {
7 | var amount = utils.formatEther(this.props.ticket.amount);
8 | return (
9 |
10 |
Amount : { amount.value } { amount.unit }
11 |
Price : { this.props.ticket.price } BTC/ETH
12 |
Total : { this.props.ticket.total } BTC
13 |
Total with fee : { this.props.ticket.totalWithFee } BTC
14 |
BTC address : { this.props.ticket.address }
15 |
16 | );
17 | }
18 | });
19 |
20 | module.exports = TicketDetails;
21 |
--------------------------------------------------------------------------------
/frontend/app/components/btcswap/TicketRow.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {injectIntl, FormattedMessage, FormattedDate, FormattedNumber} from 'react-intl';
3 | import {Button} from 'react-bootstrap';
4 |
5 | import utils from '../../js/utils';
6 |
7 | let TicketRow = injectIntl(React.createClass({
8 | handleClick(e) {
9 | e.preventDefault();
10 | e.stopPropagation();
11 | this.props.openModal(this.props.ticket);
12 | },
13 |
14 | handleAction(e) {
15 | e.preventDefault();
16 | e.stopPropagation();
17 | this.props.openConfirmModal(this.props.ticket);
18 | },
19 |
20 | render() {
21 | var totalEther = utils.formatEther(this.props.ticket.amount);
22 | var disabled = ((this.props.isOwn && !this.props.user.own) ? " disabled" : "");
23 | var className = "trade-" + (this.props.ticket.status ? this.props.ticket.status : "mined") + disabled;
24 | return (
25 |
26 |
27 |
28 | { this.props.ticket.id == '-' ? '-' :
29 | }
30 |
31 |
32 |
33 |
34 |
39 |
40 |
41 |
42 |
43 | {
44 | utils.numeral(this.props.ticket.price, 8)
45 | //
46 | }
47 |
48 |
49 |
50 |
51 | { this.props.ticket.total } BTC
52 |
53 |
54 |
55 |
56 | {this.props.ticket.address}
57 |
58 |
59 |
60 |
61 | {this.props.ticket.owner}
62 |
63 |
64 |
65 |
66 | { (this.props.ticket.expiry > 1 && this.props.ticket.expiry > new Date().getTime() / 1000) ?
67 | : '-' }
68 |
69 |
70 |
71 |
72 | { !this.props.review &&
73 |
74 | { this.props.isOwn ? "Cancel" : (
75 | (!this.props.ticket.claimer || (this.props.ticket.expiry > 1 && this.props.ticket.expiry < new Date().getTime() / 1000)) ?
76 | "Reserve" : "Claim") }
77 | }
78 |
79 |
80 |
81 | );
82 | }
83 | }));
84 |
85 | module.exports = TicketRow;
86 |
--------------------------------------------------------------------------------
/frontend/app/components/btcswap/Tickets.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Table, Button, Modal} from 'react-bootstrap';
3 |
4 | import AlertModal from '../AlertModal';
5 | import ConfirmModal from '../ConfirmModal';
6 | import TransitionGroup from '../TransitionGroup';
7 |
8 | import Nav from './Nav';
9 | import Blocks from './Blocks';
10 | import TicketRow from './TicketRow';
11 | import TicketDetails from './TicketDetails';
12 |
13 | var Tickets = React.createClass({
14 | getInitialState() {
15 | return {
16 | alertLevel: 'info',
17 | alertMessage: '',
18 | showAlert: false,
19 | showModal: false,
20 | showConfirmModal: false,
21 | message: null,
22 | note: null,
23 | submit: null
24 | };
25 | },
26 |
27 | componentDidMount() {
28 | // TODO smooth / less hackish scroll to ticketId
29 | if (this.props.params.ticketId && this.refs["ticket-" + this.props.params.ticketId]) {
30 | var ticketOffset = this.refs["ticket-" + this.props.params.ticketId].offsetTop;
31 | window.scroll(0, ticketOffset);
32 | }
33 | },
34 |
35 | componentWillReceiveProps(nextProps) {
36 | if (nextProps.ticket.error)
37 | this.setState({
38 | showAlert: true,
39 | alertLevel: 'danger',
40 | alertMessage: nextProps.ticket.error
41 | });
42 | },
43 |
44 | openConfirmModal: function(ticket) {
45 | this.setState({
46 | showConfirmModal: true,
47 | message: ticket.owner == this.props.user.user.id ?
48 | "Are you sure you want to cancel this ticket?" :
49 |
50 | { (!ticket.claimer || (ticket.expiry > 1 && ticket.expiry < new Date().getTime() / 1000)) ?
51 |
{`Reserve ticket #${ticket.id}?`}
:
52 |
{`Claim ticket #${ticket.id}?`}
}
53 |
,
54 | note: ,
55 | submit: function() { this.handleAction(ticket); }.bind(this)
56 | });
57 | },
58 |
59 | closeConfirmModal: function() {
60 | this.setState({ showConfirmModal: false });
61 | },
62 |
63 | openModal: function(ticket) {
64 | this.setState({
65 | showModal: true,
66 | message:
67 | });
68 | },
69 |
70 | closeModal: function() {
71 | this.setState({ showModal: false });
72 | },
73 |
74 | setAlert: function(alertLevel, alertMessage) {
75 | this.setState({
76 | alertLevel: alertLevel,
77 | alertMessage: alertMessage
78 | });
79 | },
80 |
81 | showAlert: function() {
82 | this.setState({ showAlert: true });
83 | },
84 |
85 | hideAlert: function() {
86 | this.props.flux.actions.ticket.closeAlert();
87 | this.setState({ showAlert: false});
88 | },
89 |
90 | handleAction(ticket) {
91 | if (ticket.owner == this.props.user.user.id)
92 | this.props.flux.actions.ticket.cancelTicket(ticket.id);
93 | else if (!ticket.claimer || ticket.reservable) {
94 | this.props.flux.actions.ticket.lookupTicket(ticket.id);
95 | this.props.history.pushState(null, '/btc/reserve', null);
96 | }
97 | else {
98 | this.props.flux.actions.ticket.lookupTicket(ticket.id);
99 | this.props.history.pushState(null, '/btc/claim', null);
100 | }
101 | },
102 |
103 | render() {
104 | var ticketRows = this.props.ticket.tickets.map(function (ticket, i) {
105 | var key = "ticket-" + (ticket.id == '-' ? ticket.pendingHash : ticket.id);
106 | return (
107 |
111 | );
112 | }.bind(this));
113 |
114 | return (
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | ID
123 | AMOUNT
124 | PRICE BTC/ETH
125 | TOTAL
126 | BTC ADDRESS
127 | BY
128 | EXPIRY
129 |
130 |
131 |
132 |
133 | {ticketRows}
134 |
135 |
136 |
137 |
144 |
145 |
152 |
153 |
154 |
155 | Ticket details
156 |
157 |
158 | { this.state.message }
159 |
160 |
161 | Close
162 |
163 |
164 |
165 | );
166 | }
167 | });
168 |
169 | module.exports = Tickets;
170 |
--------------------------------------------------------------------------------
/frontend/app/css/assets/logo-ex-orange.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/assets/logo-ex-orange.jpg
--------------------------------------------------------------------------------
/frontend/app/css/fonts.css:
--------------------------------------------------------------------------------
1 |
2 | @font-face {
3 | font-family: 'Lato';
4 | src: url('fonts/lato-regular-webfont.eot');
5 | src: url('fonts/lato-regular-webfont.eot?#iefix') format('embedded-opentype'),
6 | url('fonts/lato-regular-webfont.woff') format('woff'),
7 | url('fonts/lato-regular-webfont.woff2') format('woff2'),
8 | url('fonts/lato-regular-webfont.ttf') format('truetype'),
9 | url('fonts/lato-regular-webfont.svg#latoregular') format('svg');
10 | font-weight: normal;
11 | font-style: normal;
12 | }
13 |
14 | @font-face {
15 | font-family: 'Lato';
16 | src: url('fonts/lato-bold-webfont.eot');
17 | src: url('fonts/lato-bold-webfont.eot?#iefix') format('embedded-opentype'),
18 | url('fonts/lato-bold-webfont.woff') format('woff'),
19 | url('fonts/lato-bold-webfont.woff2') format('woff2'),
20 | url('fonts/lato-bold-webfont.ttf') format('truetype'),
21 | url('fonts/lato-bold-webfont.svg#latobold') format('svg');
22 | font-weight: bold;
23 | font-style: normal;
24 | }
25 |
26 | @font-face {
27 | font-family: 'Lato';
28 | src: url('fonts/lato-italic-webfont.eot');
29 | src: url('fonts/lato-italic-webfont.eot?#iefix') format('embedded-opentype'),
30 | url('fonts/lato-italic-webfont.woff') format('woff'),
31 | url('fonts/lato-italic-webfont.woff2') format('woff2'),
32 | url('fonts/lato-italic-webfont.ttf') format('truetype'),
33 | url('fonts/lato-italic-webfont.svg#latoitalic') format('svg');
34 | font-weight: normal;
35 | font-style: italic;
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/app/css/fonts/crypto.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/crypto.eot
--------------------------------------------------------------------------------
/frontend/app/css/fonts/crypto.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/crypto.ttf
--------------------------------------------------------------------------------
/frontend/app/css/fonts/crypto.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/crypto.woff
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-bold-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-bold-webfont.eot
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-bold-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-bold-webfont.ttf
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-bold-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-bold-webfont.woff
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-bold-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-bold-webfont.woff2
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-italic-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-italic-webfont.eot
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-italic-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-italic-webfont.ttf
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-italic-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-italic-webfont.woff
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-italic-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-italic-webfont.woff2
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-regular-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-regular-webfont.eot
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-regular-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-regular-webfont.ttf
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-regular-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-regular-webfont.woff
--------------------------------------------------------------------------------
/frontend/app/css/fonts/lato-regular-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/css/fonts/lato-regular-webfont.woff2
--------------------------------------------------------------------------------
/frontend/app/css/icons.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'crypto';
3 | src: url('fonts/crypto.eot');
4 | src: url('fonts/crypto.eot?#iefix') format('embedded-opentype'),
5 | url('fonts/crypto.woff') format('woff'),
6 | url('fonts/crypto.ttf') format('truetype'),
7 | url('fonts/crypto.svg#crypto') format('svg');
8 | font-weight: normal;
9 | font-style: normal;
10 | }
11 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
12 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
13 | /*
14 | @media screen and (-webkit-min-device-pixel-ratio:0) {
15 | @font-face {
16 | font-family: 'crypto';
17 | src: url('../font/crypto.svg?43893243#crypto') format('svg');
18 | }
19 | }
20 | */
21 |
22 | [class^="icon-"]:before, [class*=" icon-"]:before {
23 | font-family: "crypto";
24 | font-style: normal;
25 | font-weight: normal;
26 | speak: none;
27 |
28 | display: inline-block;
29 | text-decoration: inherit;
30 | width: 1em;
31 | margin-right: .2em;
32 | text-align: center;
33 | /* opacity: .8; */
34 |
35 | /* For safety - reset parent styles, that can break glyph codes*/
36 | font-variant: normal;
37 | text-transform: none;
38 |
39 | /* fix buttons height, for twitter bootstrap */
40 | line-height: 1em;
41 |
42 | /* Animation center compensation - margins should be symmetric */
43 | /* remove if not needed */
44 | margin-left: .2em;
45 |
46 | /* you can be more comfortable with increased icons size */
47 | /* font-size: 120%; */
48 |
49 | /* Font smoothing. That was taken from TWBS */
50 | -webkit-font-smoothing: antialiased;
51 | -moz-osx-font-smoothing: grayscale;
52 |
53 | /* Uncomment for 3D effect */
54 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
55 | }
56 |
57 | .icon-diamond:before { content: '\e800'; } /* '' */
58 | .icon-money:before { content: '\e801'; } /* '' */
59 | .icon-bitcoin:before { content: '\e802'; } /* '' */
60 | .icon-chart-line:before { content: '\e803'; } /* '' */
61 | .icon-wallet:before { content: '\e804'; } /* '' */
62 | .icon-forward:before { content: '\e805'; } /* '' */
63 | .icon-cancel:before { content: '\e806'; } /* '' */
64 | .icon-ok:before { content: '\e807'; } /* '' */
65 | .icon-user:before { content: '\e808'; } /* '' */
66 | .icon-users:before { content: '\e809'; } /* '' */
67 | .icon-help:before { content: '\e80a'; } /* '' */
68 | .icon-contacts:before { content: '\e80b'; } /* '' */
69 | .icon-cog-alt:before { content: '\e80c'; } /* '' */
70 | .icon-chart-pie:before { content: '\e80d'; } /* '' */
71 |
--------------------------------------------------------------------------------
/frontend/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/app/favicon.ico
--------------------------------------------------------------------------------
/frontend/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | EtherEx
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/app/js/abi.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | etherex: require('./abi/etherex'),
3 | sub: require('./abi/sub')
4 | };
5 |
--------------------------------------------------------------------------------
/frontend/app/js/abi/sub.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | "constant": false,
4 | "type": "function",
5 | "name": "allowance(int256,int256)",
6 | "outputs": [
7 | {
8 | "type": "int256",
9 | "name": "out"
10 | }
11 | ],
12 | "inputs": [
13 | {
14 | "type": "int256",
15 | "name": "_address"
16 | },
17 | {
18 | "type": "int256",
19 | "name": "_spender"
20 | }
21 | ]
22 | },
23 | {
24 | "constant": false,
25 | "type": "function",
26 | "name": "approve(int256,int256)",
27 | "outputs": [
28 | {
29 | "type": "int256",
30 | "name": "out"
31 | }
32 | ],
33 | "inputs": [
34 | {
35 | "type": "int256",
36 | "name": "_spender"
37 | },
38 | {
39 | "type": "int256",
40 | "name": "_value"
41 | }
42 | ]
43 | },
44 | {
45 | "constant": false,
46 | "type": "function",
47 | "name": "balance()",
48 | "outputs": [
49 | {
50 | "type": "int256",
51 | "name": "out"
52 | }
53 | ],
54 | "inputs": []
55 | },
56 | {
57 | "constant": false,
58 | "type": "function",
59 | "name": "balanceOf(int256)",
60 | "outputs": [
61 | {
62 | "type": "int256",
63 | "name": "out"
64 | }
65 | ],
66 | "inputs": [
67 | {
68 | "type": "int256",
69 | "name": "_address"
70 | }
71 | ]
72 | },
73 | {
74 | "constant": false,
75 | "type": "function",
76 | "name": "transfer(int256,int256)",
77 | "outputs": [
78 | {
79 | "type": "int256",
80 | "name": "out"
81 | }
82 | ],
83 | "inputs": [
84 | {
85 | "type": "int256",
86 | "name": "_to"
87 | },
88 | {
89 | "type": "int256",
90 | "name": "_value"
91 | }
92 | ]
93 | },
94 | {
95 | "constant": false,
96 | "type": "function",
97 | "name": "transferFrom(int256,int256,int256)",
98 | "outputs": [
99 | {
100 | "type": "int256",
101 | "name": "out"
102 | }
103 | ],
104 | "inputs": [
105 | {
106 | "type": "int256",
107 | "name": "_from"
108 | },
109 | {
110 | "type": "int256",
111 | "name": "_to"
112 | },
113 | {
114 | "type": "int256",
115 | "name": "_value"
116 | }
117 | ]
118 | },
119 | {
120 | "inputs": [
121 | {
122 | "indexed": true,
123 | "type": "int256",
124 | "name": "_owner"
125 | },
126 | {
127 | "indexed": true,
128 | "type": "int256",
129 | "name": "_spender"
130 | },
131 | {
132 | "indexed": false,
133 | "type": "int256",
134 | "name": "_value"
135 | }
136 | ],
137 | "type": "event",
138 | "name": "Approval(int256,int256,int256)"
139 | },
140 | {
141 | "inputs": [
142 | {
143 | "indexed": true,
144 | "type": "int256",
145 | "name": "_from"
146 | },
147 | {
148 | "indexed": true,
149 | "type": "int256",
150 | "name": "_to"
151 | },
152 | {
153 | "indexed": false,
154 | "type": "int256",
155 | "name": "_value"
156 | }
157 | ],
158 | "type": "event",
159 | "name": "Transfer(int256,int256,int256)"
160 | }
161 | ];
162 |
--------------------------------------------------------------------------------
/frontend/app/js/constants.js:
--------------------------------------------------------------------------------
1 | var keyMirror = require('keymirror');
2 |
3 | module.exports = {
4 | SECONDS_PER_BLOCK: 15,
5 | CHANGE_EVENT: "change",
6 | config: keyMirror({
7 | UPDATE_ETHEREUM_CLIENT_SUCCESS: null,
8 | UPDATE_ETHEREUM_CLIENT_FAILED: null,
9 | UPDATE_PERCENT_LOADED_SUCCESS: null,
10 | UPDATE_DEMO_MODE: null,
11 | UPDATE_CONFIG: null,
12 | UPDATE_BTC_SWAP_CLIENT: null
13 | }),
14 | network: keyMirror({
15 | UPDATE_READY: null,
16 | UPDATE_NETWORK: null,
17 | UPDATE_ETHEREUM_STATUS: null,
18 | UPDATE_BLOCK_CHAIN_AGE: null,
19 | UPDATE_IS_MONITORING_BLOCKS: null,
20 | ETHEREUM_STATUS_CONNECTED: null,
21 | ETHEREUM_STATUS_FAILED: null,
22 | ETHEREUM_STATUS_LOADING: null,
23 | LOAD_DEMO_DATA: null
24 | }),
25 | trade: keyMirror({
26 | LOAD_TRADE_IDS: null,
27 | LOAD_TRADE_IDS_FAIL: null,
28 | LOAD_TRADE: null,
29 | LOAD_TRADES: null,
30 | LOAD_TRADES_FAIL: null,
31 | LOAD_TRADES_SUCCESS: null,
32 | LOAD_TRADES_PROGRESS: null,
33 | LOAD_DEMO_DATA: null,
34 | UPDATE_TRADE: null,
35 | UPDATE_TRADES: null,
36 | UPDATE_TRADES_FAIL: null,
37 | UPDATE_TRADES_SUCCESS: null,
38 | UPDATE_TRADES_MESSAGE: null,
39 | CHECK_PENDING: null,
40 | ADD_TRADE: null,
41 | ADD_TRADE_SUCCESS: null,
42 | ADD_TRADE_FAIL: null,
43 | FILL_TRADE: null,
44 | FILL_TRADE_FAIL: null,
45 | FILL_TRADES: null,
46 | FILL_TRADES_FAIL: null,
47 | CANCEL_TRADE: null,
48 | CANCEL_TRADE_FAIL: null,
49 | ESTIMATE_GAS: null,
50 | ESTIMATE_GAS_ADD: null,
51 | ESTIMATE_GAS_FILL: null,
52 | HIGHLIGHT_FILLING: null,
53 | HIGHLIGHT_FILLING_FAIL: null,
54 | CLICK_FILL: null,
55 | SWITCH_MARKET: null,
56 | SWITCH_MARKET_FAIL: null,
57 | SWITCH_TYPE: null,
58 | SWITCH_TYPE_FAIL: null
59 | }),
60 | user: keyMirror({
61 | LOAD_USER: null,
62 | LOAD_USER_FAIL: null,
63 | LOAD_USER_SUCCESS: null,
64 | LOAD_ADDRESSES: null,
65 | LOAD_ADDRESSES_FAIL: null,
66 | LOAD_ADDRESSES_SUCCESS: null,
67 | LOAD_DEFAULT_ACCOUNT: null,
68 | UPDATE_USER: null,
69 | UPDATE_BALANCE: null,
70 | UPDATE_BALANCE_FAIL: null,
71 | UPDATE_BALANCE_SUB: null,
72 | UPDATE_BALANCE_SUB_FAIL: null,
73 | DEPOSIT: null,
74 | DEPOSIT_FAIL: null,
75 | WITHDRAW: null,
76 | WITHDRAW_FAIL: null,
77 | SEND_SUB: null,
78 | SEND_SUB_FAIL: null,
79 | SEND_ETHER: null,
80 | SEND_ETHER_FAIL: null,
81 | SWITCH_ADDRESS: null
82 | }),
83 | market: keyMirror({
84 | LOAD_MARKET: null,
85 | LOAD_MARKETS: null,
86 | LOAD_MARKETS_FAIL: null,
87 | LOAD_MARKETS_SUCCESS: null,
88 | LOAD_MARKETS_PROGRESS: null,
89 | LOAD_DEMO_DATA: null,
90 | UPDATE_LAST_PRICE: null,
91 | UPDATE_MARKET: null,
92 | UPDATE_MARKETS: null,
93 | UPDATE_MARKET_BALANCE: null,
94 | CHANGE_MARKET: null,
95 | RELOAD_PRICES: null,
96 | UPDATE_PRICES: null,
97 | UPDATE_PRICES_DATA: null,
98 | RELOAD_TRANSACTIONS: null,
99 | UPDATE_TRANSACTIONS: null,
100 | TOGGLE_FAVORITE: null,
101 | UPDATE_MESSAGES: null,
102 | UPDATE_MESSAGES_FAIL: null,
103 | REGISTER_MARKET: null,
104 | REGISTER_MARKET_FAIL: null
105 | }),
106 | ticket: keyMirror({
107 | LOAD_TICKETS: null,
108 | LOAD_TICKETS_PROGRESS: null,
109 | LOAD_TICKETS_SUCCESS: null,
110 | LOAD_TICKET_IDS: null,
111 | LOAD_TICKET_IDS_FAIL: null,
112 | LOAD_TICKET: null,
113 | LOAD_TICKET_FAIL: null,
114 | LOOKUP_TICKET: null,
115 | LOOKUP_TICKET_FAIL: null,
116 | LOAD_DEMO_DATA: null,
117 | UPDATE_TICKET: null,
118 | UPDATE_TICKETS: null,
119 | UPDATE_TICKETS_MESSAGE: null,
120 | UPDATE_TICKETS_SUCCESS: null,
121 | UPDATE_TICKETS_FAIL: null,
122 | UPDATE_WALLET: null,
123 | UPDATE_WALLET_FAIL: null,
124 | UPDATE_TX: null,
125 | UPDATE_TX_FAIL: null,
126 | UPDATE_BTC_HEAD: null,
127 | UPDATE_BTC_HEAD_FAIL: null,
128 | UPDATE_BTC_HEIGHT: null,
129 | UPDATE_BTC_HEIGHT_FAIL: null,
130 | UPDATE_BTC_HEADER: null,
131 | UPDATE_BLOCK_FEE: null,
132 | UPDATE_POW: null,
133 | PROPAGATE_TX: null,
134 | CREATE_TICKET: null,
135 | CREATE_TICKET_SUCCESS: null,
136 | CREATE_TICKET_FAIL: null,
137 | RESERVE_TICKET: null,
138 | RESERVE_TICKET_SUCCESS: null,
139 | RESERVE_TICKET_FAIL: null,
140 | CLAIM_TICKET: null,
141 | CLAIM_TICKET_SUCCESS: null,
142 | CLAIM_TICKET_FAIL: null,
143 | CANCEL_TICKET: null,
144 | CANCEL_TICKET_SUCCESS: null,
145 | CANCEL_TICKET_FAIL: null,
146 | ESTIMATE_GAS: null,
147 | ESTIMATE_GAS_ACTION: null,
148 | COMPUTE_POW_FAIL: null,
149 | VERIFY_POW: null,
150 | VERIFY_POW_FAIL: null,
151 | CLOSE_ALERT: null
152 | })
153 | };
154 |
--------------------------------------------------------------------------------
/frontend/app/js/jsx_preprocessor.js:
--------------------------------------------------------------------------------
1 | // preprocessor.js
2 | var ReactTools = require('react-tools');
3 | module.exports = {
4 | process: function(src) {
5 | return ReactTools.transform(src);
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/frontend/app/js/units.js:
--------------------------------------------------------------------------------
1 | import bigRat from 'big-rational';
2 |
3 | var units = {
4 | "Uether": bigRat("1000000000000000000000000000000000000000000000000000000"),
5 | "Vether": bigRat("1000000000000000000000000000000000000000000000000000"),
6 | "Dether": bigRat("1000000000000000000000000000000000000000000000000"),
7 | "Nether": bigRat("1000000000000000000000000000000000000000000000"),
8 | "Yether": bigRat("1000000000000000000000000000000000000000000"),
9 | "Zether": bigRat("1000000000000000000000000000000000000000"),
10 | "Eether": bigRat("1000000000000000000000000000000000000"),
11 | "Pether": bigRat("1000000000000000000000000000000000"),
12 | "Tether": bigRat("1000000000000000000000000000000"),
13 | "Gether": bigRat("1000000000000000000000000000"),
14 | "Mether": bigRat("1000000000000000000000000"),
15 | "Kether": bigRat("1000000000000000000000"),
16 | "ether": bigRat("1000000000000000000"),
17 | "finney": bigRat("1000000000000000"),
18 | "szabo": bigRat("1000000000000"),
19 | "Gwei": bigRat("1000000000"),
20 | "Mwei": bigRat("1000000"),
21 | "Kwei": bigRat("1000"),
22 | "wei": bigRat("1")
23 | };
24 |
25 | module.exports = units;
26 |
--------------------------------------------------------------------------------
/frontend/app/js/utils.js:
--------------------------------------------------------------------------------
1 | var crypto = require('crypto');
2 | var units = require('./units');
3 | import bigRat from 'big-rational';
4 | var numeral = require("numeral");
5 | var si = require("si-prefix");
6 |
7 | var scale = new si.Scale({
8 | minor: true,
9 | above: {
10 | 2: 'hundred',
11 | 3: 'thousand',
12 | 6: 'million',
13 | 9: 'billion',
14 | 12: 'trillion',
15 | 15: 'quadrillion',
16 | 18: 'quintillion',
17 | 21: 'sextillion',
18 | 24: 'septillion',
19 | 27: 'octillion',
20 | 30: 'nonillion',
21 | 33: 'decillion',
22 | 36: 'undecillion',
23 | 39: 'duodecillion',
24 | 42: 'tredecillion',
25 | 45: 'quattuordecillion',
26 | 48: 'quindecillion',
27 | 51: 'sexdecillion',
28 | 54: 'septendecillion',
29 | 57: 'octodecillion',
30 | 60: 'novemdecillion',
31 | 63: 'vigintillion'
32 | },
33 | below: {
34 | 1: 'deci',
35 | 2: 'centi',
36 | 3: 'milli',
37 | 6: 'micro',
38 | 9: 'nano',
39 | 12: 'pico',
40 | 15: 'femto',
41 | 18: 'atto',
42 | 21: 'zepto',
43 | 24: 'yocto'
44 | }
45 | });
46 | var scaleUnit = new si.Unit(scale, '');
47 |
48 | var utils = {
49 | formatEther(wei) {
50 | if (!wei)
51 | return 0;
52 | return this.formatBalance(wei, 0, true);
53 | },
54 |
55 | formatBalance(wei, precision, split) {
56 | var value = 0;
57 | var unit = null;
58 | var big = bigRat(wei);
59 | if (typeof precision === 'undefined')
60 | precision = 4;
61 | if (big.compare(units.Uether.multiply(1000)) > 0) {
62 | value = big.divide(units.Uether).valueOf();
63 | unit = Object.keys(units)[0];
64 | if (split)
65 | return { value: value, unit: unit };
66 | return this.numeral(value, precision) + " " + unit;
67 | }
68 | for (var i in units)
69 | if (units[i].valueOf() != 1 && big.compare(units[i]) >= 0) {
70 | value = big.divide(units[i].divide(1000)).divide(1000).valueOf();
71 | if (split)
72 | return { value: value, unit: i };
73 | return this.numeral(value, precision) + " " + i;
74 | }
75 | if (split)
76 | return { value: wei.valueOf(), unit: "wei"};
77 | return this.numeral(wei.valueOf(), 0) + " wei";
78 | },
79 |
80 | randomId() {
81 | return crypto.randomBytes(32).toString('hex');
82 | },
83 |
84 | format(n) {
85 | return n ? scaleUnit.format(n, ' ') : '0';
86 | },
87 |
88 | numeral(n, p) {
89 | return numeral(n).format('0,0.' + this.repeat('0', p));
90 | },
91 |
92 | padLeft(string, chars, sign) {
93 | return new Array(chars - string.length + 1).join(sign ? sign : "0") + string;
94 | },
95 |
96 | padRight(string, chars, sign) {
97 | return string + (new Array(chars - string.length + 1).join(sign ? sign : "0"));
98 | },
99 |
100 | repeat(str, num) {
101 | return new Array(num + 1).join(str);
102 | },
103 |
104 | consoleLog: 'background-color: #222; color: #fff; padding: 2px 6px;',
105 | consoleWarn: 'background-color: #c87f0a; color: #fff; padding: 2px 6px;',
106 | consoleError: 'background-color: #d62c1a; color: #fff; padding: 2px 6px;',
107 | consoleDebug: 'background-color: #217dbb; color: #fff; padding: 2px 6px;',
108 |
109 | log(prefix, message) {
110 | console.log('%cEtherEx', this.consoleLog, prefix, message);
111 | },
112 |
113 | warn(prefix, message) {
114 | console.warn('%cEtherEx', this.consoleWarn, prefix, message);
115 | },
116 |
117 | error(prefix, message) {
118 | console.error('%cEtherEx', this.consoleError, prefix, message);
119 | },
120 |
121 | debug(prefix, message) {
122 | console.log('%cEtherEx', this.consoleDebug, prefix, message);
123 | }
124 | };
125 |
126 | module.exports = utils;
127 |
--------------------------------------------------------------------------------
/frontend/app/stores/ConfigStore.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 | var Fluxxor = require('fluxxor');
3 | var fixtures = require("../js/fixtures");
4 | var constants = require('../js/constants');
5 |
6 | var ConfigStore = Fluxxor.createStore({
7 | initialize: function () {
8 | this.host = process.env.RPC_HOST || fixtures.host;
9 | this.address = fixtures.addresses.etherex;
10 | this.network = 2; // default to testnet
11 | this.ethereumClient = null;
12 | this.btcSwapAddress = fixtures.addresses.btcswap;
13 | this.btcSwapClient = null;
14 | this.storeBlockFee = 10000000000000000; // 0.01 ETH =~ storing 1 block
15 | this.debug = false;
16 | this.debugHandler = null;
17 | this.demoMode = false;
18 | this.percentLoaded = null;
19 | this.range = 75; // max 300 blocks / ~ 1 hour
20 | this.rangeEnd = 0;
21 | this.si = false;
22 | this.timeout = 300;
23 | this.alertCount = null;
24 |
25 | this.bindActions(
26 | constants.config.UPDATE_CONFIG, this.handleUpdateConfig,
27 | constants.config.UPDATE_DEMO_MODE, this.handleDemoMode,
28 | constants.config.UPDATE_ETHEREUM_CLIENT_SUCCESS, this.handleUpdateEthereumClientSuccess,
29 | constants.config.UPDATE_ETHEREUM_CLIENT_FAILED, this.handleUpdateEthereumClientFailed,
30 | constants.config.UPDATE_PERCENT_LOADED_SUCCESS, this.handleUpdatePercentLoadedSuccess,
31 | constants.config.UPDATE_BTC_SWAP_CLIENT, this.handleUpdateBtcSwapClient
32 | );
33 | },
34 |
35 | getState: function () {
36 | return {
37 | host: this.host,
38 | address: this.address,
39 | network: this.network,
40 | ethereumClient: this.ethereumClient,
41 | btcSwapAddress: this.btcSwapAddress,
42 | btcSwapClient: this.btcSwapClient,
43 | storeBlockFee: this.storeBlockFee,
44 | debug: this.debug,
45 | debugHandler: this.debugHandler,
46 | demoMode: this.demoMode,
47 | percentLoaded: this.percentLoaded,
48 | range: this.range,
49 | rangeEnd: this.rangeEnd,
50 | si: this.si,
51 | timeout: this.timeout,
52 | alertCount: this.alertCount
53 | };
54 | },
55 |
56 | getEthereumClient: function () {
57 | return this.ethereumClient;
58 | },
59 |
60 | getBtcSwapClient: function () {
61 | return this.btcSwapClient;
62 | },
63 |
64 | handleUpdatePercentLoadedSuccess: function (payload) {
65 | this.percentLoaded = payload.percentLoaded;
66 | this.emit(constants.CHANGE_EVENT);
67 | },
68 |
69 | handleUpdateEthereumClientSuccess: function (payload) {
70 | this.ethereumClient = payload.ethereumClient;
71 | this.ethereumClientFailed = false;
72 | this.emit(constants.CHANGE_EVENT);
73 | },
74 |
75 | handleUpdateEthereumClientFailed: function () {
76 | this.ethereumClient = null;
77 | this.ethereumClientFailed = true;
78 | this.emit(constants.CHANGE_EVENT);
79 | },
80 |
81 | handleUpdateBtcSwapClient: function(payload) {
82 | this.btcSwapClient = payload.btcSwapClient;
83 | this.emit(constants.CHANGE_EVENT);
84 | },
85 |
86 | handleUpdateConfig: function (payload) {
87 | _.merge(this, payload);
88 | this.emit(constants.CHANGE_EVENT);
89 | },
90 |
91 | handleDemoMode: function (payload) {
92 | this.demoMode = payload.enable;
93 | this.emit(constants.CHANGE_EVENT);
94 | }
95 | });
96 |
97 | module.exports = ConfigStore;
98 |
--------------------------------------------------------------------------------
/frontend/app/stores/NetworkStore.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 | var Fluxxor = require('fluxxor');
3 | var constants = require('../js/constants');
4 |
5 | var NetworkStore = Fluxxor.createStore({
6 |
7 | initialize: function () {
8 | this.ready = null;
9 | this.client = null;
10 | this.peerCount = null;
11 | this.blockNumber = 0;
12 | this.blockTime = null;
13 | this.blockTimestamp = null;
14 | this.networkLag = null;
15 | this.ether = null;
16 | this.gasPrice = null;
17 | this.ethereumStatus = null;
18 | this.mining = null;
19 | this.hashrate = null;
20 | this.blockChainAge = null;
21 | this.isMonitoring = false;
22 |
23 | this.bindActions(
24 | constants.network.UPDATE_NETWORK, this.handleUpdateNetwork,
25 | constants.network.UPDATE_READY, this.handleUpdateReady,
26 | constants.network.UPDATE_ETHEREUM_STATUS, this.handleUpdateEthereumStatus,
27 | constants.network.UPDATE_IS_MONITORING_BLOCKS, this.handleUpdateIsMonitoring,
28 | constants.network.UPDATE_BLOCK_CHAIN_AGE, this.handleUpdateBlockChainAge
29 | );
30 | },
31 |
32 | getState: function () {
33 | return {
34 | ready: this.ready,
35 | client: this.client,
36 | peerCount: this.peerCount,
37 | blockNumber: this.blockNumber,
38 | blockTime: this.blockTime,
39 | blockTimestamp: this.blockTimestamp,
40 | networkLag: this.networkLag,
41 | ether: this.ether,
42 | gasPrice: this.gasPrice,
43 | ethereumStatus: this.ethereumStatus,
44 | mining: this.mining,
45 | hashrate: this.hashrate,
46 | blockChainAge: this.blockChainAge,
47 | isMonitoring: this.isMonitoring
48 | };
49 | },
50 |
51 | handleUpdateReady: function (payload) {
52 | this.ready = payload.ready;
53 | // this.emit(constants.CHANGE_EVENT);
54 | },
55 |
56 | handleUpdateNetwork: function (payload) {
57 | _.merge(this, payload);
58 | this.emit(constants.CHANGE_EVENT);
59 | },
60 |
61 | handleUpdateBlockChainAge: function (payload) {
62 | this.blockChainAge = payload.blockChainAge;
63 | this.emit(constants.CHANGE_EVENT);
64 | },
65 |
66 | handleUpdateEthereumStatus: function (payload) {
67 | this.ethereumStatus = payload.ethereumStatus;
68 | this.emit(constants.CHANGE_EVENT);
69 | },
70 |
71 | handleUpdateIsMonitoring: function (payload) {
72 | this.isMonitoring = payload.isMonitoring;
73 | this.emit(constants.CHANGE_EVENT);
74 | }
75 | });
76 |
77 | module.exports = NetworkStore;
78 |
--------------------------------------------------------------------------------
/frontend/app/stores/UserStore.js:
--------------------------------------------------------------------------------
1 | var _ = require("lodash");
2 | var Fluxxor = require("fluxxor");
3 |
4 | var constants = require("../js/constants");
5 | var fixtures = require("../js/fixtures");
6 | import utils from '../js/utils';
7 | import bigRat from 'big-rational';
8 |
9 | var UserStore = Fluxxor.createStore({
10 |
11 | initialize: function(options) {
12 | this.user = options.user || {
13 | loading: true,
14 | id: 'loading...',
15 | balance: 0,
16 | balanceFormatted: {value: 0, unit: 'ether'},
17 | balanceWei: 0,
18 | balancePending: 0,
19 | balanceSub: 0,
20 | balanceSubAvailable: 0,
21 | balanceSubTrading: 0,
22 | balanceSubPending: 0,
23 | addresses: []
24 | };
25 | this.defaultAccount = null;
26 | this.loading = true;
27 | this.error = null;
28 |
29 | this.bindActions(
30 | constants.user.LOAD_USER, this.onLoadUser,
31 | constants.user.LOAD_USER_FAIL, this.onUserFail,
32 | constants.user.LOAD_USER_SUCCESS, this.onLoadUserSuccess,
33 | constants.user.LOAD_ADDRESSES, this.onLoadAddresses,
34 | constants.user.LOAD_ADDRESSES_FAIL, this.onUserFail,
35 | constants.user.LOAD_ADDRESSES_SUCCESS, this.onLoadAddressesSuccess,
36 | constants.user.LOAD_DEFAULT_ACCOUNT, this.onLoadDefaultAccount,
37 | constants.user.UPDATE_USER, this.onUpdateUser,
38 | constants.user.UPDATE_BALANCE, this.onUpdateBalance,
39 | constants.user.UPDATE_BALANCE_FAIL, this.onUserFail,
40 | constants.user.UPDATE_BALANCE_SUB, this.onUpdateBalanceSub,
41 | constants.user.UPDATE_BALANCE_SUB_FAIL, this.onUserFail,
42 | constants.user.DEPOSIT, this.onDeposit,
43 | constants.user.DEPOSIT_FAIL, this.onUserFail,
44 | constants.user.WITHDRAW, this.onWithdraw,
45 | constants.user.WITHDRAW_FAIL, this.onUserFail,
46 | constants.user.SEND_ETHER, this.onSendEther,
47 | constants.user.SEND_ETHER_FAIL, this.onUserFail,
48 | constants.user.SEND_SUB, this.onSendSub,
49 | constants.user.SEND_SUB_FAIL, this.onUserFail,
50 | constants.user.SWITCH_ADDRESS, this.onSwitchAddress
51 | );
52 |
53 | this.setMaxListeners(1024); // prevent "possible EventEmitter memory leak detected"
54 | },
55 |
56 | onLoadUser: function() {
57 | this.loading = true;
58 | this.error = null;
59 | this.emit(constants.CHANGE_EVENT);
60 | },
61 |
62 | onLoadUserSuccess: function(payload) {
63 | this.user = payload;
64 | this.loading = false;
65 | this.error = null;
66 | this.emit(constants.CHANGE_EVENT);
67 | },
68 |
69 | onLoadDefaultAccount: function(payload) {
70 | this.defaultAccount = payload;
71 | this.emit(constants.CHANGE_EVENT);
72 | },
73 |
74 | onLoadAddresses: function() {
75 | this.user.id = 'loading...';
76 | this.loading = true;
77 | this.error = null;
78 | this.emit(constants.CHANGE_EVENT);
79 | },
80 |
81 | onLoadAddressesSuccess: function(payload) {
82 | this.user.id = payload.primary;
83 | this.user.addresses = payload.addresses;
84 | this.loading = false;
85 | this.error = null;
86 | this.emit(constants.CHANGE_EVENT);
87 | },
88 |
89 | onUpdateUser: function(payload) {
90 | _.merge(this, payload);
91 | this.emit(constants.CHANGE_EVENT);
92 | },
93 |
94 | onSwitchAddress: function(payload) {
95 | this.user.id = payload.address;
96 | this.emit(constants.CHANGE_EVENT);
97 | },
98 |
99 | onDeposit: function(payload) {
100 | if (this.flux.stores.config.debug)
101 | utils.log("DEPOSIT", payload.amount);
102 | this.emit(constants.CHANGE_EVENT);
103 | },
104 |
105 | onWithdraw: function(payload) {
106 | if (this.flux.stores.config.debug)
107 | utils.log("WITHDRAW", payload.amount);
108 | this.emit(constants.CHANGE_EVENT);
109 | },
110 |
111 | onUpdateBalance: function(payload) {
112 | // console.log("BALANCE", payload.balance);
113 | this.user.balance = bigRat(payload.balance).divide(bigRat(fixtures.ether)).valueOf();
114 | this.user.balanceFormatted = utils.formatEther(payload.balance);
115 | this.user.balanceWei = payload.balance;
116 | this.user.balancePending = bigRat(payload.balancePending).divide(bigRat(fixtures.ether)).valueOf();
117 | this.emit(constants.CHANGE_EVENT);
118 | },
119 |
120 | onUpdateBalanceSub: function(payload) {
121 | // console.log("BALANCE_SUB", payload);
122 | this.user.balanceSubAvailable = payload.available;
123 | this.user.balanceSubTrading = payload.trading;
124 | this.user.balanceSub = payload.balance;
125 | this.emit(constants.CHANGE_EVENT);
126 | },
127 |
128 | onSendEther: function(payload) {
129 | if (this.flux.stores.config.debug)
130 | utils.log("SEND_ETHER", payload);
131 | this.emit(constants.SEND_ETHER);
132 | },
133 |
134 | onSendSub: function(payload) {
135 | if (this.flux.stores.config.debug)
136 | utils.log("SEND_SUB", payload);
137 | this.emit(constants.SEND_SUB);
138 | },
139 |
140 | onUserFail: function(payload) {
141 | utils.error("ERROR: ", payload.error);
142 | this.loading = false;
143 | this.error = payload.error;
144 | this.emit(constants.CHANGE_EVENT);
145 | },
146 |
147 | getState: function() {
148 | return {
149 | user: this.user,
150 | defaultAccount: this.defaultAccount,
151 | loading: this.loading,
152 | error: this.error
153 | };
154 | }
155 | });
156 |
157 | module.exports = UserStore;
158 |
--------------------------------------------------------------------------------
/frontend/app/tests/app.spec.jsx:
--------------------------------------------------------------------------------
1 | // var React = require('react/addons');
2 | // var TestUtils = React.addons.TestUtils;
3 | // var expect = require('expect');
4 |
5 | describe('EtherEx', function () {
6 | it('renders without problems', function (done) {
7 | require('../app');
8 | done();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/frontend/karma.conf.js:
--------------------------------------------------------------------------------
1 | var webpackConfig = require('./webpack.config');
2 | webpackConfig.devtool = 'inline-source-map';
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | browsers: [ 'Chrome' ],
7 | singleRun: true,
8 | frameworks: [ 'mocha' ],
9 | files: [
10 | 'tests.karma.js'
11 | ],
12 | preprocessors: {
13 | 'tests.karma.js': [ 'webpack', 'sourcemap' ]
14 | },
15 | reporters: [ 'dots' ],
16 | // https://github.com/karma-runner/karma/issues/1497
17 | // https://github.com/webpack/karma-webpack/pull/63
18 | webpack: webpackConfig,
19 | webpackServer: {
20 | noInfo: true
21 | }
22 | });
23 | };
24 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "EtherEx",
3 | "version": "0.9.11",
4 | "description": "EtherEx frontend",
5 | "main": "app/app.jsx",
6 | "scripts": {
7 | "build": "grunt build",
8 | "dist": "grunt build",
9 | "prestart": "npm install",
10 | "prebuild": "npm install",
11 | "test": "",
12 | "start": "grunt",
13 | "upgrade": "npm-check-updates -u && npm test"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/etherex/etherex.git"
18 | },
19 | "keywords": [
20 | "Ethereum",
21 | "EtherEx"
22 | ],
23 | "author": "caktux ",
24 | "license": "MIT",
25 | "private": true,
26 | "bugs": {
27 | "url": "https://github.com/etherex/etherex/issues"
28 | },
29 | "homepage": "https://github.com/etherex/etherex",
30 | "devDependencies": {
31 | "babel-core": "^6.7.6",
32 | "babel-loader": "^6.2.4",
33 | "babel-preset-es2015": "^6.6.0",
34 | "babel-preset-react": "^6.5.0",
35 | "css-loader": "^0.23.1",
36 | "eslint": "^2.7.0",
37 | "eslint-plugin-react": "^4.3.0",
38 | "expect": "^1.16.0",
39 | "file-loader": "^0.8.5",
40 | "grunt": "^1.0.1",
41 | "grunt-contrib-clean": "^1.0.0",
42 | "grunt-contrib-watch": "^1.0.0",
43 | "grunt-eslint": "^18.0.0",
44 | "grunt-gh-pages": "^1.0.0",
45 | "grunt-karma": "^0.12.2",
46 | "grunt-webpack": "^1.0.11",
47 | "json-loader": "^0.5.4",
48 | "karma": "^0.13.22",
49 | "karma-chrome-launcher": "^0.2.3",
50 | "karma-cli": "^0.1.2",
51 | "karma-mocha": "^0.2.2",
52 | "karma-sourcemap-loader": "^0.3.7",
53 | "karma-webpack": "^1.7.0",
54 | "less": "^2.6.1",
55 | "less-loader": "^2.2.3",
56 | "mocha": "^2.4.5",
57 | "node-libs-browser": "^1.0.0",
58 | "react-hot-loader": "^1.3.0",
59 | "style-loader": "^0.13.1",
60 | "url-loader": "^0.5.7",
61 | "webpack": "^1.12.14",
62 | "webpack-dev-server": "^1.14.1"
63 | },
64 | "dependencies": {
65 | "big-rational": "^0.10.5",
66 | "bootstrap": "^3.3.6",
67 | "bootswatch": "^3.3.6",
68 | "btc-swap": "etherex/btc-swap",
69 | "d3": "^3.5.16",
70 | "flat": "^2.0.0",
71 | "fluxxor": "^1.7.3",
72 | "history": "^2.0.1",
73 | "intl": "^1.1.0",
74 | "intl-locales-supported": "^1.0.0",
75 | "keymirror": "^0.1.1",
76 | "lodash": "^4.9.0",
77 | "numeral": "^1.5.3",
78 | "react": "^15.0.1",
79 | "react-addons-perf": "^15.0.1",
80 | "react-addons-transition-group": "^15.0.1",
81 | "react-bootstrap": "^0.28.5",
82 | "react-dom": "^15.0.1",
83 | "react-intl": "^2.0.1",
84 | "react-qr": "^0.0.2",
85 | "react-router": "^2.0.1",
86 | "si-prefix": "^0.1.0",
87 | "techan": "caktux/techan.js",
88 | "ua-parser-js": "^0.7.10",
89 | "web3": "^0.15.3"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/frontend/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etherex/etherex/c91832e0c4c97aa55280557cd6773cb89cb5e858/frontend/screenshot.png
--------------------------------------------------------------------------------
/frontend/tests.karma.js:
--------------------------------------------------------------------------------
1 | var context = require.context('./app/tests', true, /.+\.spec\.jsx?$/);
2 | context.keys().forEach(context);
3 | module.exports = context;
4 |
--------------------------------------------------------------------------------
/frontend/update_abi.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import json
4 | from serpent import mk_full_signature
5 |
6 | contracts = {
7 | 'etherex': '../contracts/etherex.se',
8 | 'sub': '../contracts/etx.se'
9 | }
10 |
11 | for c in contracts:
12 | sig = mk_full_signature(contracts[c])
13 | # print sig
14 |
15 | abi = json.dumps(sig, indent=4, separators=(',', ': '))
16 | # print abi
17 |
18 | with open('app/js/abi/%s.js' % c, 'w') as out:
19 | out.write("module.exports = %s;\n" % abi)
20 |
--------------------------------------------------------------------------------
/frontend/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | module.exports = {
4 | entry: [
5 | "./app/app.jsx"
6 | ],
7 | resolveLoader: {
8 | root: path.join(__dirname, 'node_modules')
9 | },
10 | node: {
11 | fs: 'empty',
12 | net: 'empty',
13 | tls: 'empty'
14 | },
15 | output: {
16 | /* global __dirname */
17 | path: path.join(__dirname, "app"),
18 | filename: "app.js"
19 | },
20 | plugins: [],
21 | resolve: {
22 | extensions: ['', '.js', '.jsx', '.json']
23 | },
24 | module: {
25 | loaders: [
26 | { test: /\.css$/, loader: "style!css" },
27 | { test: /\.less$/, loader: "style!css!less" },
28 | { test: /\.jsx$/, loader: "react-hot" },
29 | { test: /\.js$/, include: /app/, loader: "babel", query: { presets: ["es2015"] }},
30 | { test: /\.jsx$/, include: /app/, loader: "babel", query: { presets: ["react", "es2015"] }},
31 | { test: /\.json$/, loader: "json" },
32 | { test: /\.woff|woff2$/, loader: "url?limit=10000&minetype=application/font-woff" },
33 | { test: /\.ttf$/, loader: "file" },
34 | { test: /\.eot$/, loader: "file" },
35 | { test: /\.svg$/, loader: "file" },
36 | { test: /\.gif/, loader: 'url?limit=10000&mimetype=image/gif' },
37 | { test: /\.jpg/, loader: 'url?limit=10000&mimetype=image/jpg' },
38 | { test: /\.png/, loader: 'url?limit=10000&mimetype=image/png' }
39 | ]
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = E302
3 | max-line-length = 160
4 | exclude = frontend,tests/integration.py,tests/integration-user.py
5 |
6 | [pytest]
7 | norecursedirs = .git frontend
8 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from distutils.core import setup
4 |
5 | setup(name='etherex',
6 | version='1.0',
7 | description='A Decentralized Future Calls For A Decentralized Exchange',
8 | author='caktux',
9 | author_email='caktux@gmail.com',
10 | url='https://github.com/etherex/etherex/')
11 |
--------------------------------------------------------------------------------
/tests/integration-user.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from selenium import webdriver
3 | from selenium.webdriver.common.by import By
4 | from selenium.webdriver.common.keys import Keys
5 | from selenium.webdriver.support.ui import Select
6 | from selenium.common.exceptions import NoSuchElementException
7 | from selenium.common.exceptions import NoAlertPresentException
8 | import unittest, time, re
9 |
10 | class EndUser(unittest.TestCase):
11 | def setUp(self):
12 | self.driver = webdriver.Firefox()
13 | self.driver.implicitly_wait(30)
14 | self.base_url = "http://etherex.github.io"
15 | self.verificationErrors = []
16 | self.accept_next_alert = True
17 | self.driver.set_window_position(0, 0)
18 | self.driver.set_window_size(1280, 1200)
19 |
20 | def test_integration(self):
21 | driver = self.driver
22 | driver.get(self.base_url + "/etherex/#/trades")
23 | time.sleep(5)
24 | driver.save_screenshot('screenshot-user.png')
25 |
26 | try:
27 | error = driver.find_element_by_css_selector("div.alert.alert-danger > span").text
28 | if "Unable to load addresses" in error:
29 | self.fail(error)
30 | if "No market found" in error:
31 | self.fail(error)
32 | error = driver.find_element_by_xpath("//div[@id='wrap']/div/div[4]/div[2]/div[2]/h4").text
33 | if error == "No trades found":
34 | self.fail(error)
35 | except: pass
36 |
37 | try:
38 | # Assert balance and sub-balances
39 | # self.assertEqual("1,039.1100 ether", driver.find_element_by_xpath("//div[@id='wrap']/div/div[2]/div/div/span").text)
40 | self.assertEqual("0", driver.find_element_by_xpath("//div[@id='wrap']/div/div[2]/div[2]/div/div/div").text)
41 | self.assertEqual("0", driver.find_element_by_xpath("//div[@id='wrap']/div/div[2]/div[2]/div/div[2]/div").text)
42 | # self.assertEqual("10 billion", driver.find_element_by_xpath("//div[@id='wrap']/div/div[2]/div[2]/div/div[3]/div").text)
43 |
44 | # Assert no price set
45 | self.assertEqual("N/A ETX/ETH", driver.find_element_by_xpath("//div[@id='wrap']/div/div[3]/div/div/span[2]").text)
46 |
47 | # Assert trade table headers
48 | self.assertEqual("Trades", driver.find_element_by_xpath("//*[@id='wrap']/div/div[4]/div[2]/div[1]/div[1]/h3/span[1]").text)
49 | self.assertEqual("Asks", driver.find_element_by_xpath("//*[@id='wrap']/div/div[4]/div[2]/div[3]/div/div[1]/div/h4").text)
50 | self.assertEqual("Bids", driver.find_element_by_xpath("//div[@id='wrap']/div/div[4]/div[2]/div[3]/div/div[2]/div/h4").text)
51 |
52 | # Assert added asks
53 | self.assertEqual("50.00000", driver.find_element_by_xpath("//div[@id='wrap']/div/div[4]/div[2]/div[3]/div/div/div/div/table/tbody/tr/td/div").text)
54 | self.assertEqual("150.00000", driver.find_element_by_xpath("//div[@id='wrap']/div/div[4]/div[2]/div[3]/div/div/div/div/table/tbody/tr[2]/td/div").text)
55 | self.assertEqual("0.32000000", driver.find_element_by_xpath("//div[@id='wrap']/div/div[4]/div[2]/div[3]/div/div/div/div/table/tbody/tr[10]/td[3]/div").text)
56 |
57 | # Assert added bids
58 | self.assertEqual("0.24000000", driver.find_element_by_xpath("//div[@id='wrap']/div/div[4]/div[2]/div[3]/div/div[2]/div/div/table/tbody/tr/td[3]/div").text)
59 | self.assertEqual("10.5000", driver.find_element_by_xpath("//div[@id='wrap']/div/div[4]/div[2]/div[3]/div/div[2]/div/div/table/tbody/tr[4]/td[4]/div/span").text)
60 |
61 | # Click-fill first three trades
62 | driver.find_element_by_xpath("//*[@id='wrap']/div/div[4]/div[2]/div[3]/div/div[1]/div/div/table/tbody/tr[3]").click()
63 | time.sleep(1)
64 |
65 | # Assert amount, price and total got filled
66 | self.assertEqual("450", driver.find_element_by_xpath("//div[@id='wrap']/div/div[4]/div[1]/div[2]/div[2]/div[1]/div/form/div[1]/div/input").get_attribute("value"))
67 | self.assertEqual("0.25", driver.find_element_by_xpath("//div[@id='wrap']/div/div[4]/div[1]/div[2]/div[2]/div[1]/div/form/div[2]/div/input").get_attribute("value"))
68 | self.assertEqual("112.5", driver.find_element_by_xpath("//div[@id='wrap']/div/div[4]/div[1]/div[2]/div[2]/div[1]/div/form/div[3]/div/input").get_attribute("value"))
69 |
70 | # Fill trades
71 | driver.find_element_by_xpath("//*[@id='wrap']/div/div[4]/div[1]/div[2]/div[2]/div[1]/div/form/div[4]/button").click()
72 | time.sleep(1)
73 |
74 | # Assert filling trades message
75 | self.assertEqual("You will be filling 3 trades for a total of 112.50000 ether.", driver.find_element_by_css_selector("div.modal.fade.in > div > div > form > div.modal-body > span").text)
76 | time.sleep(1)
77 |
78 | # Click Yes
79 | driver.find_element_by_css_selector("div.modal-footer > button.btn.btn-primary").click()
80 | time.sleep(1)
81 |
82 | # Assert new sub-balance
83 | time.sleep(10)
84 | self.assertEqual("4.5 hundred", driver.find_element_by_xpath("//div[@id='wrap']/div/div[2]/div[2]/div/div[1]/div").text)
85 |
86 | # Assert new Last price
87 | self.assertEqual("0.25000000 ETX/ETH", driver.find_element_by_xpath("//div[@id='wrap']/div/div[3]/div/div/span[2]").text)
88 |
89 | driver.save_screenshot('screenshot-user-passed.png')
90 |
91 | except Exception as e:
92 | driver.save_screenshot('screenshot-user-fail.png')
93 | self.fail(e)
94 | driver.quit()
95 |
96 | self.assertEqual([], self.verificationErrors)
97 |
98 | driver.quit()
99 |
--------------------------------------------------------------------------------
/tests/test_etx.py:
--------------------------------------------------------------------------------
1 | from ethereum import tester
2 |
3 | class TestEtxContract(object):
4 |
5 | CONTRACT = 'contracts/etx.se'
6 |
7 | def setup_class(cls):
8 | cls.s = tester.state()
9 | cls.c = cls.s.abi_contract(cls.CONTRACT)
10 | cls.snapshot = cls.s.snapshot()
11 | cls.initialBalance = 1000000 * 10 ** 5
12 |
13 | def setup_method(self, method):
14 | self.s.revert(self.snapshot)
15 |
16 | def test_negative_send_should_fail(self):
17 | assert self.c.transfer(tester.a0, -1000, sender=tester.k1) == 0
18 | assert self.c.balanceOf(tester.a0) == self.initialBalance
19 | assert self.c.balanceOf(tester.a1) == 0
20 |
21 | def test_send_with_invalid_recipient_should_not_overwrite_internal_settings(self):
22 | assert self.c.transfer(0, 1) == 0
23 | assert self.s.block.get_storage_data(self.c.address, 0) == 1410973349
24 |
25 | assert self.c.transfer(-1000, 1) == 0
26 |
27 | def test_simple_transfer_without_balance_should_fail(self):
28 | assert self.c.transfer(tester.a2, 1, sender=tester.k1) == 0
29 |
30 | def test_simple_transfer(self):
31 | assert self.c.transfer(tester.a1, 10000) == 1
32 | assert self.c.balanceOf(tester.a1) == 10000
33 |
34 | def test_approve_then_transfer(self):
35 | assert self.c.approve(tester.a1, 10000) == 1
36 | assert self.c.allowance(tester.a0, tester.a1) == 10000
37 | assert self.c.transferFrom(tester.a0, tester.a1, 10000, sender=tester.k1) == 1
38 | assert self.c.balanceOf(tester.a1) == 10000
39 | assert self.c.allowance(tester.a0, tester.a1) == 0
40 |
41 | def test_approve_twice_then_transfer(self):
42 | assert self.c.approve(tester.a1, 10000) == 1
43 | assert self.c.allowance(tester.a0, tester.a1) == 10000
44 | assert self.c.approve(tester.a1, 20000) == 1
45 | assert self.c.allowance(tester.a0, tester.a1) == 20000
46 | assert self.c.transferFrom(tester.a0, tester.a1, 10000, sender=tester.k1) == 1
47 | assert self.c.balanceOf(tester.a1) == 10000
48 | assert self.c.balanceOf(tester.a0) == self.initialBalance - 10000
49 | assert self.c.allowance(tester.a0, tester.a1) == 10000
50 | assert self.c.transferFrom(tester.a0, tester.a1, 10000, sender=tester.k1) == 1
51 | assert self.c.balanceOf(tester.a1) == 20000
52 | assert self.c.balanceOf(tester.a0) == self.initialBalance - 20000
53 | assert self.c.allowance(tester.a0, tester.a1) == 0
54 |
55 | def test_approve_send_to_another(self):
56 | assert self.c.approve(tester.a1, 10000) == 1
57 | assert self.c.allowance(tester.a0, tester.a1) == 10000
58 | assert self.c.transferFrom(tester.a0, tester.a2, 10000, sender=tester.k1) == 1
59 | assert self.c.balanceOf(tester.a2) == 10000
60 | assert self.c.allowance(tester.a0, tester.a1) == 0
61 |
62 | def test_approve_then_unapprove(self):
63 | assert self.c.approve(tester.a1, 10000) == 1
64 | assert self.c.allowance(tester.a0, tester.a1) == 10000
65 | assert self.c.approve(tester.a1, 0) == 1
66 | assert self.c.allowance(tester.a0, tester.a1) == 0
67 | assert self.c.transferFrom(tester.a0, tester.a2, 10000, sender=tester.k1) == 0
68 |
--------------------------------------------------------------------------------