├── .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 | 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 | 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 |
79 |
80 | 81 |
82 | 83 |
84 |
85 |
86 | 87 | 88 | 89 | Oh snap! 90 | 91 | 92 |

{this.state.modalMessage}

93 |
94 | 95 | 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 |
45 | 46 |
{this.props.message}
47 | 48 | { (this.props.note) && 49 |
{this.props.note}
} 50 | 51 | { (this.props.estimate) && 52 |
{formatMessage({id: 'confirm.estimate'})}: {this.props.estimate}
} 53 | 54 | {this.state.tradeTable} 55 |
56 | 57 | 58 | 59 | 60 |
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 |

See the GitHub readme for documentation

10 |

Or the FAQ on the website

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 |
22 | 23 |
} 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 |
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 | 79 | 82 | 85 | 88 | 91 | 94 | 97 | 98 | 99 | 100 | {marketRows} 101 | 102 |
77 | 78 | 80 | 81 | 83 | 84 | 86 | 87 | 89 | 90 | 92 | 93 | 95 | 96 |
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 | 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 |
102 |
103 | 104 | 109 | 121 | 122 | 123 | 124 |
125 | 126 | 127 | 128 |
129 |
130 |
131 |
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 |
122 | } labelClassName="sr-only" 124 | placeholder="0x" 125 | maxLength="42" pattern="0x[a-fA-F\d]+" 126 | onChange={this.handleChange} 127 | value={this.state.recipient || ""} /> 128 | 129 | } labelClassName="sr-only" 131 | placeholder="10.0000" 132 | min={1 / _.parseInt(fixtures.ether)} step={1 / _.parseInt(fixtures.ether)} 133 | onChange={this.handleChange} 134 | value={this.state.amount || ""} /> 135 | 136 |
137 | 140 |
141 | 142 | 149 | 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 |
102 | } labelClassName="sr-only" 104 | min={this.props.market.amountPrecision} 105 | step={this.props.market.amountPrecision} 106 | placeholder="10.0000" 107 | onChange={this.handleChange} 108 | value={this.state.amount || ""} /> 109 |
110 | 113 |
114 | 121 | 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 |
133 | 134 | 135 | 140 | 141 | 142 | 143 | 144 |
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 | 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 |
119 | } labelClassName="sr-only" 122 | maxLength="42" pattern="0x[a-fA-F\d]+" 123 | onChange={this.handleChange} 124 | value={this.state.recipient || ""} /> 125 | 126 | } labelClassName="sr-only" 128 | min={this.props.market.amountPrecision} 129 | step={this.props.market.amountPrecision} 130 | placeholder="10.0000" 131 | onChange={this.handleChange} 132 | value={this.state.amount || ""} /> 133 | 134 |
135 | 138 |
139 | 140 | 147 | 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 |
    103 | } labelClassName="sr-only" 106 | min={this.props.market.amountPrecision} 107 | step={this.props.market.amountPrecision} 108 | onChange={this.handleChange} 109 | value={this.state.amount || ""} /> 110 | 111 |
    112 | 115 |
    116 | 117 | 124 | 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 |
    52 |
    53 |
    54 |

    55 |
    56 |
    57 |
    58 | 59 |
    60 |
    61 |
    62 |
    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 | 60 | 67 | Buy 68 | Sell 69 | 70 |

    {formatMessage({id:'form.new'})}

    71 |
    72 |
    73 |
    74 | 75 |
    76 |
    77 | 80 |
    81 |
    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 |
    72 | 73 |
    } 74 |
    75 | { 76 | (this.props.trades.type == 1) ? 77 |
    78 | 80 | 82 |
    : 83 |
    84 | 86 | 88 |
    89 | } 90 |
    91 |
    92 |
    93 | 95 | 97 |
    98 |
    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 |
    143 | 144 | 145 | 146 |
    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 | 42 | 43 | 44 | 45 | {tradeRows} 46 | 47 |
    39 | { !this.props.review && 40 | } 41 |
    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 | 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 |
    39 | 40 |
    41 | } 42 | labelClassName='col-sm-3' wrapperClassName='col-sm-9' 43 | help={`Press ${this.state.ctrl}-C to copy to clipboard`}> 44 | 47 | {this.props.user.addresses ? 48 | this.props.user.addresses.map( function(address) { 49 | return {address}; 50 | }) : 51 | ""} 52 | 53 | 54 | 55 |
    56 | 57 |
    58 | 59 | } 60 | labelClassName='col-sm-3' wrapperClassName='col-sm-9'> 61 | { this.props.trades ? 62 | : 63 | '0' 64 | } 65 | 66 |
    67 |
    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 |
    36 | 37 |
    38 | 42 |
    43 |
    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 |
    44 |
    45 | 47 |
    48 |
    49 |
    50 | ); 51 | }, 52 | withdraw() { 53 | return ( 54 |
    55 |
    56 |

    57 | 58 |

    59 |
    60 |
    61 |
    62 | 64 |
    65 |
    66 |
    67 | ); 68 | }, 69 | transfer() { 70 | return ( 71 |
    72 |
    73 |

    74 | 75 |

    76 |
    77 |
    78 |
    79 | 81 |
    82 |
    83 |
    84 | ); 85 | }, 86 | sendEther() { 87 | return ( 88 |
    89 |
    90 |

    91 | 92 |

    93 |
    94 |
    95 |
    96 | 98 |
    99 |
    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 | 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 | 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 |
    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 |
    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 | 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 | } 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 |
    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 | --------------------------------------------------------------------------------