From 780f3ba94f887d2aa66836d08b1d3b9742868b8a Mon Sep 17 00:00:00 2001 From: crptm Date: Fri, 14 Jul 2017 01:02:39 +0400 Subject: [PATCH] balance fetching (#41) * balance fetching * fix tests * bump deps * validate custom token form * equiv values * fix rates saga naming * address review comments --- .eslintrc.json | 4 +- common/actions/customTokens.js | 28 ++ common/actions/rates.js | 15 + common/actions/wallet.js | 42 +- .../BalanceSidebar/AddCustomTokenForm.jsx | 100 ++++ .../BalanceSidebar/BalanceSidebar.jsx | 169 +++++++ .../BalanceSidebar/TokenBalances.jsx | 77 +++ common/components/BalanceSidebar/TokenRow.jsx | 57 +++ common/components/BalanceSidebar/index.js | 3 + common/components/index.js | 1 + common/components/ui/UnlockHeader.jsx | 4 +- common/config/data.js | 45 +- common/config/tokens/eth.js | 439 ++++++++++++++++++ .../containers/Tabs/SendTransaction/index.jsx | 51 +- common/libs/nodes/base.js | 6 +- common/libs/nodes/rpc.js | 72 +++ common/libs/units.js | 44 ++ common/libs/validators.js | 21 +- common/libs/wallet/base.js | 4 + common/libs/wallet/index.js | 4 + common/reducers/customTokens.js | 33 ++ common/reducers/index.js | 12 +- common/reducers/rates.js | 22 + common/reducers/wallet.js | 39 +- common/sagas/rates.js | 19 + common/sagas/wallet.js | 84 +++- common/selectors/config.js | 7 +- common/selectors/wallet.js | 37 ++ common/store.js | 19 +- common/utils/formatters.js | 15 +- common/utils/localStorage.js | 2 +- flow-typed/npm/big.js_v3.x.x.js | 55 +++ package.json | 10 +- spec/sagas/wallet.spec.js | 2 + 34 files changed, 1426 insertions(+), 116 deletions(-) create mode 100644 common/actions/customTokens.js create mode 100644 common/actions/rates.js create mode 100644 common/components/BalanceSidebar/AddCustomTokenForm.jsx create mode 100644 common/components/BalanceSidebar/BalanceSidebar.jsx create mode 100644 common/components/BalanceSidebar/TokenBalances.jsx create mode 100644 common/components/BalanceSidebar/TokenRow.jsx create mode 100644 common/components/BalanceSidebar/index.js create mode 100644 common/config/tokens/eth.js create mode 100644 common/libs/units.js create mode 100644 common/libs/wallet/index.js create mode 100644 common/reducers/customTokens.js create mode 100644 common/reducers/rates.js create mode 100644 common/sagas/rates.js create mode 100644 common/selectors/wallet.js create mode 100644 flow-typed/npm/big.js_v3.x.x.js diff --git a/.eslintrc.json b/.eslintrc.json index 0ca86358..bd44c9d8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -33,6 +33,8 @@ "globals": { "SyntheticInputEvent": false, "SyntheticKeyboardEvent": false, - "Generator": false + "Generator": false, + "$Keys": false, + "SyntheticMouseEvent": false } } diff --git a/common/actions/customTokens.js b/common/actions/customTokens.js new file mode 100644 index 00000000..215a4376 --- /dev/null +++ b/common/actions/customTokens.js @@ -0,0 +1,28 @@ +// @flow +import type { Token } from 'config/data'; + +export type AddCustomTokenAction = { + type: 'CUSTOM_TOKEN_ADD', + payload: Token +}; + +export type RemoveCustomTokenAction = { + type: 'CUSTOM_TOKEN_REMOVE', + payload: string +}; + +export type CustomTokenAction = AddCustomTokenAction | RemoveCustomTokenAction; + +export function addCustomToken(payload: Token): AddCustomTokenAction { + return { + type: 'CUSTOM_TOKEN_ADD', + payload + }; +} + +export function removeCustomToken(payload: string): RemoveCustomTokenAction { + return { + type: 'CUSTOM_TOKEN_REMOVE', + payload + }; +} diff --git a/common/actions/rates.js b/common/actions/rates.js new file mode 100644 index 00000000..37b1d5b6 --- /dev/null +++ b/common/actions/rates.js @@ -0,0 +1,15 @@ +// @flow + +export type SetRatesAction = { + type: 'RATES_SET', + payload: { [string]: number } +}; + +export type RatesAction = SetRatesAction; + +export function setRates(payload: { [string]: number }): SetRatesAction { + return { + type: 'RATES_SET', + payload + }; +} diff --git a/common/actions/wallet.js b/common/actions/wallet.js index 79a9ffde..7142ea23 100644 --- a/common/actions/wallet.js +++ b/common/actions/wallet.js @@ -1,44 +1,60 @@ // @flow import type { PrivateKeyUnlockParams } from 'libs/wallet/privkey'; import BaseWallet from 'libs/wallet/base'; +import Big from 'big.js'; export type UnlockPrivateKeyAction = { type: 'WALLET_UNLOCK_PRIVATE_KEY', payload: PrivateKeyUnlockParams }; -export type SaveWalletAction = { - type: 'WALLET_SAVE', +export type SetWalletAction = { + type: 'WALLET_SET', payload: BaseWallet }; -export type InitWalletAction = { - type: 'WALLET_INIT' +export type SetBalanceAction = { + type: 'WALLET_SET_BALANCE', + payload: Big +}; + +export type SetTokenBalancesAction = { + type: 'WALLET_SET_TOKEN_BALANCES', + payload: { + [string]: Big + } }; export type WalletAction = | UnlockPrivateKeyAction - | SaveWalletAction - | InitWalletAction; + | SetWalletAction + | SetBalanceAction + | SetTokenBalancesAction; -export function unlockPrivateKey( - value: PrivateKeyUnlockParams -): UnlockPrivateKeyAction { +export function unlockPrivateKey(value: PrivateKeyUnlockParams): UnlockPrivateKeyAction { return { type: 'WALLET_UNLOCK_PRIVATE_KEY', payload: value }; } -export function saveWallet(value: BaseWallet): SaveWalletAction { +export function setWallet(value: BaseWallet): SetWalletAction { return { - type: 'WALLET_SAVE', + type: 'WALLET_SET', payload: value }; } -export function initWallet(): InitWalletAction { +export function setBalance(value: Big): SetBalanceAction { return { - type: 'WALLET_INIT' + type: 'WALLET_SET_BALANCE', + payload: value + }; +} + +export function setTokenBalances(payload: { [string]: Big }): SetTokenBalancesAction { + return { + type: 'WALLET_SET_TOKEN_BALANCES', + payload }; } diff --git a/common/components/BalanceSidebar/AddCustomTokenForm.jsx b/common/components/BalanceSidebar/AddCustomTokenForm.jsx new file mode 100644 index 00000000..ad5f5043 --- /dev/null +++ b/common/components/BalanceSidebar/AddCustomTokenForm.jsx @@ -0,0 +1,100 @@ +// @flow +import React from 'react'; +import { isValidETHAddress, isPositiveIntegerOrZero } from 'libs/validators'; +import translate from 'translations'; + +export default class AddCustomTokenForm extends React.Component { + props: { + onSave: ({ address: string, symbol: string, decimal: number }) => void + }; + state = { + address: '', + symbol: '', + decimal: '' + }; + + render() { + return ( +
+ + + + + + +
+ {translate('x_Save')} +
+
+ ); + } + + isValid() { + const { address, symbol, decimal } = this.state; + if (!isPositiveIntegerOrZero(parseInt(decimal))) { + return false; + } + if (!isValidETHAddress(address)) { + return false; + } + if (this.state.symbol === '') { + return false; + } + + return true; + } + + onFieldChange = (e: SyntheticInputEvent) => { + var name = e.target.name; + var value = e.target.value; + this.setState(state => { + var newState = Object.assign({}, state); + newState[name] = value; + return newState; + }); + }; + + onSave = () => { + if (!this.isValid()) { + return; + } + const { address, symbol, decimal } = this.state; + + this.props.onSave({ address, symbol, decimal: parseInt(decimal) }); + }; +} diff --git a/common/components/BalanceSidebar/BalanceSidebar.jsx b/common/components/BalanceSidebar/BalanceSidebar.jsx new file mode 100644 index 00000000..4303888d --- /dev/null +++ b/common/components/BalanceSidebar/BalanceSidebar.jsx @@ -0,0 +1,169 @@ +// @flow +import React from 'react'; +import Big from 'big.js'; +import { BaseWallet } from 'libs/wallet'; +import type { NetworkConfig } from 'config/data'; +import type { State } from 'reducers'; +import { connect } from 'react-redux'; +import { getWalletInst, getTokenBalances } from 'selectors/wallet'; +import type { TokenBalance } from 'selectors/wallet'; +import { getNetworkConfig } from 'selectors/config'; +import { Link } from 'react-router'; +import TokenBalances from './TokenBalances'; +import { formatNumber } from 'utils/formatters'; +import { Identicon } from 'components/ui'; +import translate from 'translations'; +import * as customTokenActions from 'actions/customTokens'; + +type Props = { + wallet: BaseWallet, + balance: Big, + network: NetworkConfig, + tokenBalances: TokenBalance[], + rates: { [string]: number }, + addCustomToken: typeof customTokenActions.addCustomToken, + removeCustomToken: typeof customTokenActions.removeCustomToken +}; + +export class BalanceSidebar extends React.Component { + props: Props; + state = { + showLongBalance: false + }; + + render() { + const { wallet, balance, network, tokenBalances, rates } = this.props; + const { blockExplorer, tokenExplorer } = network; + if (!wallet) { + return null; + } + + return ( + + ); + } + + toggleShowLongBalance = (e: SyntheticMouseEvent) => { + e.preventDefault(); + this.setState(state => { + return { + showLongBalance: !state.showLongBalance + }; + }); + }; +} + +function mapStateToProps(state: State, props: Props) { + return { + wallet: getWalletInst(state), + balance: state.wallet.balance, + tokenBalances: getTokenBalances(state), + network: getNetworkConfig(state), + rates: state.rates + }; +} + +export default connect(mapStateToProps, customTokenActions)(BalanceSidebar); diff --git a/common/components/BalanceSidebar/TokenBalances.jsx b/common/components/BalanceSidebar/TokenBalances.jsx new file mode 100644 index 00000000..47943c55 --- /dev/null +++ b/common/components/BalanceSidebar/TokenBalances.jsx @@ -0,0 +1,77 @@ +// @flow +import React from 'react'; +import translate from 'translations'; +import TokenRow from './TokenRow'; +import AddCustomTokenForm from './AddCustomTokenForm'; +import type { TokenBalance } from 'selectors/wallet'; +import type { Token } from 'config/data'; + +type Props = { + tokens: TokenBalance[], + onAddCustomToken: (token: Token) => any, + onRemoveCustomToken: (symbol: string) => any +}; + +export default class TokenBalances extends React.Component { + props: Props; + state = { + showAllTokens: false, + showCustomTokenForm: false + }; + + render() { + const { tokens } = this.props; + return ( +
+
+ {translate('sidebar_TokenBal')} +
+ + + {tokens + .filter(token => !token.balance.eq(0) || token.custom || this.state.showAllTokens) + .map(token => + + )} + +
+ + {!this.state.showAllTokens ? 'Show All Tokens' : 'Hide Tokens'}{' '} + + + + {translate('SEND_custom')} + + + {this.state.showCustomTokenForm && } +
+ ); + } + + toggleShowAllTokens = () => { + this.setState(state => { + return { + showAllTokens: !state.showAllTokens + }; + }); + }; + + toggleShowCustomTokenForm = () => { + this.setState(state => { + return { + showCustomTokenForm: !state.showCustomTokenForm + }; + }); + }; + + addCustomToken = (token: Token) => { + this.props.onAddCustomToken(token); + this.setState({ showCustomTokenForm: false }); + }; +} diff --git a/common/components/BalanceSidebar/TokenRow.jsx b/common/components/BalanceSidebar/TokenRow.jsx new file mode 100644 index 00000000..b75fcfed --- /dev/null +++ b/common/components/BalanceSidebar/TokenRow.jsx @@ -0,0 +1,57 @@ +// @flow +import React from 'react'; +import Big from 'big.js'; +import translate from 'translations'; +import { formatNumber } from 'utils/formatters'; + +export default class TokenRow extends React.Component { + props: { + balance: Big, + symbol: string, + custom?: boolean, + onRemove: (symbol: string) => void + }; + + state = { + showLongBalance: false + }; + render() { + const { balance, symbol, custom } = this.props; + return ( + + + {!!custom && + } + + {this.state.showLongBalance ? balance.toString() : formatNumber(balance)} + + + + {symbol} + + + ); + } + + toggleShowLongBalance = (e: SyntheticInputEvent) => { + e.preventDefault(); + this.setState(state => { + return { + showLongBalance: !state.showLongBalance + }; + }); + }; + + onRemove = () => { + this.props.onRemove(this.props.symbol); + }; +} diff --git a/common/components/BalanceSidebar/index.js b/common/components/BalanceSidebar/index.js new file mode 100644 index 00000000..2aa8b7c4 --- /dev/null +++ b/common/components/BalanceSidebar/index.js @@ -0,0 +1,3 @@ +// @flow + +export { default } from './BalanceSidebar'; diff --git a/common/components/index.js b/common/components/index.js index e4fe4b8f..c5926815 100644 --- a/common/components/index.js +++ b/common/components/index.js @@ -3,3 +3,4 @@ export { default as Header } from './Header'; export { default as Footer } from './Footer'; export { default as Root } from './Root'; +export { default as BalanceSidebar } from './BalanceSidebar'; diff --git a/common/components/ui/UnlockHeader.jsx b/common/components/ui/UnlockHeader.jsx index 81999b16..05e6e370 100644 --- a/common/components/ui/UnlockHeader.jsx +++ b/common/components/ui/UnlockHeader.jsx @@ -21,7 +21,7 @@ export class UnlockHeader extends React.Component { state: { expanded: boolean } = { - expanded: true + expanded: !this.props.wallet }; componentDidUpdate(prevProps: Props) { @@ -69,7 +69,7 @@ export class UnlockHeader extends React.Component { function mapStateToProps(state: State) { return { - wallet: state.wallet + wallet: state.wallet.inst }; } diff --git a/common/config/data.js b/common/config/data.js index 2253589b..3f9cb82b 100644 --- a/common/config/data.js +++ b/common/config/data.js @@ -104,12 +104,44 @@ export const languages = [ } ]; -export const NETWORKS = { +export type Token = { + address: string, + symbol: string, + decimal: number +}; + +export type NetworkConfig = { + name: string, + // unit: string, + blockExplorer?: { + name: string, + tx: string, + address: string + }, + tokenExplorer?: { + name: string, + address: string + }, + chainId: number, + tokens: Token[] +}; + +export const NETWORKS: { [key: string]: NetworkConfig } = { ETH: { name: 'ETH', - blockExplorerTX: 'https://etherscan.io/tx/[[txHash]]', - blockExplorerAddr: 'https://etherscan.io/address/[[address]]', - chainId: 1 + // unit: 'ETH', + chainId: 1, + blockExplorer: { + name: 'https://etherscan.io', + tx: 'https://etherscan.io/tx/[[txHash]]', + address: 'https://etherscan.io/address/[[address]]' + }, + tokenExplorer: { + name: 'Ethplorer.io', + address: 'https://ethplorer.io/address/[[address]]' + }, + tokens: require('./tokens/eth').default + // 'abiList': require('./abiDefinitions/ethAbi.json'), } }; @@ -118,9 +150,6 @@ export const NODES = { network: 'ETH', lib: new RPCNode('https://api.myetherapi.com/eth'), service: 'MyEtherWallet', - estimateGas: true, - eip155: true - // 'tokenList': require('./tokens/ethTokens.json'), - // 'abiList': require('./abiDefinitions/ethAbi.json'), + estimateGas: true } }; diff --git a/common/config/tokens/eth.js b/common/config/tokens/eth.js new file mode 100644 index 00000000..85ed113f --- /dev/null +++ b/common/config/tokens/eth.js @@ -0,0 +1,439 @@ +export default [ + { + address: '0xAf30D2a7E90d7DC361c8C4585e9BB7D2F6f15bc7', + symbol: '1ST', + decimal: 18 + }, + { + address: '0x422866a8F0b032c5cf1DfBDEf31A20F4509562b0', + symbol: 'ADST', + decimal: 0 + }, + { + address: '0xD0D6D6C5Fe4a677D343cC433536BB717bAe167dD', + symbol: 'ADT', + decimal: 9 + }, + { + address: '0x4470bb87d77b963a013db939be332f927f2b992e', + symbol: 'ADX', + decimal: 4 + }, + { + address: '0x960b236A07cf122663c4303350609A66A7B288C0', + symbol: 'ANT', + decimal: 18 + }, + { + address: '0xAc709FcB44a43c35F0DA4e3163b117A17F3770f5', + symbol: 'ARC', + decimal: 18 + }, + { + address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', + symbol: 'BAT', + decimal: 18 + }, + { + address: '0x74C1E4b8caE59269ec1D85D3D4F324396048F4ac', + symbol: 'BeerCoin 🍺 ', + decimal: 0 + }, + { + address: '0x1e797Ce986C3CFF4472F7D38d5C4aba55DfEFE40', + symbol: 'BCDN', + decimal: 15 + }, + { + address: '0xdD6Bf56CA2ada24c683FAC50E37783e55B57AF9F', + symbol: 'BNC', + decimal: 12 + }, + { + address: '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', + symbol: 'BNT', + decimal: 18 + }, + { + address: '0x5af2be193a6abca9c8817001f45744777db30756', + symbol: 'BQX', + decimal: 8 + }, + { + address: '0x12FEF5e57bF45873Cd9B62E9DBd7BFb99e32D73e', + symbol: 'CFI', + decimal: 18 + }, + { + address: '0xAef38fBFBF932D1AeF3B808Bc8fBd8Cd8E1f8BC5', + symbol: 'CRB', + decimal: 8 + }, + { + address: '0xbf4cfd7d1edeeea5f6600827411b41a21eb08abd', + symbol: 'CTL', + decimal: 2 + }, + { + address: '0xE4c94d45f7Aef7018a5D66f44aF780ec6023378e', + symbol: 'CryptoCarbon', + decimal: 6 + }, + { + address: '0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413', + symbol: 'DAO', + decimal: 16 + }, + { + address: '0x5c40eF6f527f4FbA68368774E6130cE6515123f2', + symbol: 'DAO_extraBalance', + decimal: 0 + }, + { + address: '0xcC4eF9EEAF656aC1a2Ab886743E98e97E090ed38', + symbol: 'DDF', + decimal: 18 + }, + { + address: '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A', + symbol: 'DGD', + decimal: 9 + }, + { + address: '0x55b9a11c2e8351b4Ffc7b11561148bfaC9977855', + symbol: 'DGX 1.0', + decimal: 9 + }, + { + address: '0x2e071D2966Aa7D8dECB1005885bA1977D6038A65', + symbol: 'DICE', + decimal: 16 + }, + { + address: '0x621d78f2ef2fd937bfca696cabaf9a779f59b3ed', + symbol: 'DRP', + decimal: 2 + }, + { + address: '0x08711D3B02C8758F2FB3ab4e80228418a7F8e39c', + symbol: 'EDG', + decimal: 0 + }, + { + address: '0xB802b24E0637c2B87D2E8b7784C055BBE921011a', + symbol: 'EMV', + decimal: 2 + }, + { + address: '0x190e569bE071F40c704e15825F285481CB74B6cC', + symbol: 'FAM', + decimal: 12 + }, + { + address: '0xBbB1BD2D741F05E144E6C4517676a15554fD4B8D', + symbol: 'FUN', + decimal: 8 + }, + { + address: '0x6810e776880C02933D47DB1b9fc05908e5386b96', + symbol: 'GNO', + decimal: 18 + }, + { + address: '0xa74476443119A942dE498590Fe1f2454d7D4aC0d', + symbol: 'GNT', + decimal: 18 + }, + { + address: '0xf7B098298f7C69Fc14610bf71d5e02c60792894C', + symbol: 'GUP', + decimal: 3 + }, + { + address: '0x1D921EeD55a6a9ccaA9C79B1A4f7B25556e44365', + symbol: 'GT', + decimal: 0 + }, + { + address: '0x14F37B574242D366558dB61f3335289a5035c506', + symbol: 'HKG', + decimal: 3 + }, + { + address: '0xcbCC0F036ED4788F63FC0fEE32873d6A7487b908', + symbol: 'HMQ', + decimal: 8 + }, + { + address: '0x888666CA69E0f178DED6D75b5726Cee99A87D698', + symbol: 'ICN', + decimal: 18 + }, + { + address: '0xc1E6C6C681B286Fb503B36a9dD6c1dbFF85E73CF', + symbol: 'JET', + decimal: 18 + }, + { + address: '0x773450335eD4ec3DB45aF74f34F2c85348645D39', + symbol: 'JetCoins', + decimal: 18 + }, + { + address: '0xfa05A73FfE78ef8f1a739473e462c54bae6567D9', + symbol: 'LUN', + decimal: 18 + }, + { + address: '0x93E682107d1E9defB0b5ee701C71707a4B2E46Bc', + symbol: 'MCAP', + decimal: 8 + }, + { + address: '0xB63B606Ac810a52cCa15e44bB630fd42D8d1d83d', + symbol: 'MCO', + decimal: 8 + }, + { + address: '0x40395044ac3c0c57051906da938b54bd6557f212', + symbol: 'MGO', + decimal: 8 + }, + { + address: '0xd0b171Eb0b0F2CbD35cCD97cDC5EDC3ffe4871aa', + symbol: 'MDA', + decimal: 18 + }, + { + address: '0xe23cd160761f63FC3a1cF78Aa034b6cdF97d3E0C', + symbol: 'MIT', + decimal: 18 + }, + { + address: '0xC66eA802717bFb9833400264Dd12c2bCeAa34a6d', + symbol: 'MKR', + decimal: 18 + }, + { + address: '0xBEB9eF514a379B997e0798FDcC901Ee474B6D9A1', + symbol: 'MLN', + decimal: 18 + }, + { + address: '0x1a95B271B0535D15fa49932Daba31BA612b52946', + symbol: 'MNE', + decimal: 8 + }, + { + address: '0x68AA3F232dA9bdC2343465545794ef3eEa5209BD', + symbol: 'MSP', + decimal: 18 + }, + { + address: '0xf433089366899d83a9f26a773d59ec7ecf30355e', + symbol: 'MTL', + decimal: 8 + }, + { + address: '0xa645264C5603E96c3b0B078cdab68733794B0A71', + symbol: 'MYST', + decimal: 8 + }, + { + address: '0xcfb98637bcae43C13323EAa1731cED2B716962fD', + symbol: 'NET', + decimal: 18 + }, + { + address: '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671', + symbol: 'NMR', + decimal: 18 + }, + { + address: '0x45e42D659D9f9466cD5DF622506033145a9b89Bc', + symbol: 'NxC', + decimal: 3 + }, + { + address: '0x701C244b988a513c945973dEFA05de933b23Fe1D', + symbol: 'OAX', + decimal: 18 + }, + { + address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', + symbol: 'OMG', + decimal: 18 + }, + { + address: '0xB97048628DB6B661D4C2aA833e95Dbe1A905B280', + symbol: 'PAY', + decimal: 18 + }, + { + address: '0x8Ae4BF2C33a8e667de34B54938B0ccD03Eb8CC06', + symbol: 'PTOY', + decimal: 8 + }, + { + address: '0xD8912C10681D8B21Fd3742244f44658dBA12264E', + symbol: 'PLU', + decimal: 18 + }, + { + address: '0x671AbBe5CE652491985342e85428EB1b07bC6c64', + symbol: 'QAU', + decimal: 8 + }, + { + address: '0x697beac28B09E122C4332D163985e8a73121b97F', + symbol: 'QRL', + decimal: 8 + }, + { + address: '0x48c80F1f4D53D5951e5D5438B54Cba84f29F32a5', + symbol: 'REP', + decimal: 18 + }, + { + address: '0x607F4C5BB672230e8672085532f7e901544a7375', + symbol: 'RLC', + decimal: 9 + }, + { + address: '0xcCeD5B8288086BE8c38E23567e684C3740be4D48', + symbol: 'RLT', + decimal: 10 + }, + { + address: '0x4993CB95c7443bdC06155c5f5688Be9D8f6999a5', + symbol: 'ROUND', + decimal: 18 + }, + + { + address: '0xa1ccc166faf0e998b3e33225a1a0301b1c86119d', + symbol: 'SGEL', + decimal: 18 + }, + + { + address: '0xd248B0D48E44aaF9c49aea0312be7E13a6dc1468', + symbol: 'SGT', + decimal: 1 + }, + { + address: '0xef2e9966eb61bb494e5375d5df8d67b7db8a780d', + symbol: 'SHIT', + decimal: 0 + }, + { + address: '0x2bDC0D42996017fCe214b21607a515DA41A9E0C5', + symbol: 'SKIN', + decimal: 6 + }, + { + address: '0x4994e81897a920c0FEA235eb8CEdEEd3c6fFF697', + symbol: 'SKO1', + decimal: 18 + }, + { + address: '0xaeC2E87E0A235266D9C5ADc9DEb4b2E29b54D009', + symbol: 'SNGLS', + decimal: 0 + }, + { + address: '0x983f6d60db79ea8ca4eb9968c6aff8cfa04b3c63', + symbol: 'SNM', + decimal: 18 + }, + { + address: '0x744d70fdbe2ba4cf95131626614a1763df805b9e', + symbol: 'SNT', + decimal: 18 + }, + { + address: '0x1dCE4Fa03639B7F0C38ee5bB6065045EdCf9819a', + symbol: 'SRC', + decimal: 8 + }, + { + address: '0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac', + symbol: 'STORJ', + decimal: 8 + }, + { + address: '0xB9e7F8568e08d5659f5D29C4997173d84CdF2607', + symbol: 'SWT', + decimal: 18 + }, + { + address: '0xf4134146af2d511dd5ea8cdb1c4ac88c57d60404', + symbol: 'SNC', + decimal: 18 + }, + { + address: '0xE7775A6e9Bcf904eb39DA2b68c5efb4F9360e08C', + symbol: 'TaaS', + decimal: 6 + }, + { + address: '0xa7f976C360ebBeD4465c2855684D1AAE5271eFa9', + symbol: 'TFL', + decimal: 8 + }, + { + address: '0x6531f133e6DeeBe7F2dcE5A0441aA7ef330B4e53', + symbol: 'TIME', + decimal: 8 + }, + { + address: '0xaAAf91D9b90dF800Df4F55c205fd6989c977E73a', + symbol: 'TKN', + decimal: 8 + }, + { + address: '0xCb94be6f13A1182E4A4B6140cb7bf2025d28e41B', + symbol: 'TRST', + decimal: 6 + }, + { + address: '0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7', + symbol: 'Unicorn 🦄 ', + decimal: 0 + }, + { + address: '0x5c543e7AE0A1104f78406C340E9C64FD9fCE5170', + symbol: 'VSL', + decimal: 18 + }, + { + address: '0x82665764ea0b58157E1e5E9bab32F68c76Ec0CdF', + symbol: 'VSM', + decimal: 0 + }, + { + address: '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374', + symbol: 'VERI', + decimal: 18 + }, + { + address: '0xeDBaF3c5100302dCddA53269322f3730b1F0416d', + symbol: 'VRS', + decimal: 5 + }, + { + address: '0x667088b212ce3d06a1b553a7221E1fD19000d9aF', + symbol: 'WINGS', + decimal: 18 + }, + { + address: '0x4DF812F6064def1e5e029f1ca858777CC98D2D81', + symbol: 'XAUR', + decimal: 8 + }, + { + address: '0xb110ec7b1dcb8fab8dedbf28f53bc63ea5bedd84', + symbol: 'XID', + decimal: 8 + } +]; diff --git a/common/containers/Tabs/SendTransaction/index.jsx b/common/containers/Tabs/SendTransaction/index.jsx index ccdfc370..f23ad108 100644 --- a/common/containers/Tabs/SendTransaction/index.jsx +++ b/common/containers/Tabs/SendTransaction/index.jsx @@ -12,6 +12,7 @@ import { AmountField, AddressField } from './components'; +import { BalanceSidebar } from 'components'; import pickBy from 'lodash/pickBy'; import type { State as AppState } from 'reducers'; import { connect } from 'react-redux'; @@ -19,6 +20,7 @@ import BaseWallet from 'libs/wallet/base'; // import type { Transaction } from './types'; import customMessages from './messages'; import { donationAddressMap } from 'config/data'; +import Big from 'big.js'; type State = { hasQueryString: boolean, @@ -45,16 +47,14 @@ function getParam(query: { [string]: string }, key: string) { // TODO how to handle DATA? export class SendTransaction extends React.Component { - static propTypes = { - location: PropTypes.object.isRequired - }; props: { location: { query: { [string]: string } }, - wallet: BaseWallet + wallet: BaseWallet, + balance: Big }; state: State = { hasQueryString: false, @@ -80,15 +80,7 @@ export class SendTransaction extends React.Component { const unitReadable = 'UNITREADABLE'; const nodeUnit = 'NODEUNIT'; const hasEnoughBalance = false; - const { - to, - value, - unit, - gasLimit, - data, - readOnly, - hasQueryString - } = this.state; + const { to, value, unit, gasLimit, data, readOnly, hasQueryString } = this.state; const customMessage = customMessages.find(m => m.to === to); // tokens @@ -112,7 +104,7 @@ export class SendTransaction extends React.Component { {'' /* */}
- {'' /* */} +
@@ -124,9 +116,8 @@ export class SendTransaction extends React.Component {
- Warning! You do not have enough funds to - complete this swap. - {' '} + Warning! You do not have enough funds to complete this swap. +
Please add more funds or access a different wallet.
@@ -147,23 +138,14 @@ export class SendTransaction extends React.Component { unit={unit} onChange={readOnly ? void 0 : this.onAmountChange} /> - + {unit === 'ether' && - } + }
@@ -171,7 +153,9 @@ export class SendTransaction extends React.Component {
- + @@ -267,6 +251,10 @@ export class SendTransaction extends React.Component { }; onAmountChange = (value: string, unit: string) => { + // TODO: tokens + if (value === 'everything') { + value = this.props.balance.toString(); + } this.setState({ value, unit @@ -276,7 +264,8 @@ export class SendTransaction extends React.Component { function mapStateToProps(state: AppState) { return { - wallet: state.wallet.inst + wallet: state.wallet.inst, + balance: state.wallet.balance }; } diff --git a/common/libs/nodes/base.js b/common/libs/nodes/base.js index 5f8572ee..bb566cce 100644 --- a/common/libs/nodes/base.js +++ b/common/libs/nodes/base.js @@ -1,8 +1,8 @@ // @flow +import Big from 'big.js'; export default class BaseNode { - // FIXME bignumber? - queryBalance(address: string): Promise { - throw 'Implement me'; + async getBalance(address: string): Promise { + throw new Error('Implement me'); } } diff --git a/common/libs/nodes/rpc.js b/common/libs/nodes/rpc.js index 2f8c3c3e..75716fdc 100644 --- a/common/libs/nodes/rpc.js +++ b/common/libs/nodes/rpc.js @@ -1,5 +1,27 @@ // @flow import BaseNode from './base'; +import { randomBytes } from 'crypto'; +import Big from 'big.js'; + +type JsonRpcSuccess = {| + id: string, + result: string +|}; + +type JsonRpcError = {| + error: { + code: string, + message: string, + data?: any + } +|}; + +type JsonRpcResponse = JsonRpcSuccess | JsonRpcError; + +// FIXME +type EthCall = any; + +function isError(response) {} export default class RPCNode extends BaseNode { endpoint: string; @@ -7,4 +29,54 @@ export default class RPCNode extends BaseNode { super(); this.endpoint = endpoint; } + + async getBalance(address: string): Promise { + return this.post('eth_getBalance', [address, 'pending']).then(response => { + if (response.error) { + throw new Error(response.error.message); + } + // FIXME is this safe? + return new Big(Number(response.result)); + }); + } + + // FIXME extract batching + async ethCall(calls: EthCall[]) { + return this.batchPost( + calls.map(params => { + return { + id: randomBytes(16).toString('hex'), + jsonrpc: '2.0', + method: 'eth_call', + params: [params, 'pending'] + }; + }) + ); + } + + async post(method: string, params: string[]): Promise { + return fetch(this.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id: randomBytes(16).toString('hex'), + jsonrpc: '2.0', + method, + params + }) + }).then(r => r.json()); + } + + // FIXME + async batchPost(requests: any[]): Promise { + return fetch(this.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requests) + }).then(r => r.json()); + } } diff --git a/common/libs/units.js b/common/libs/units.js new file mode 100644 index 00000000..0a2eaa58 --- /dev/null +++ b/common/libs/units.js @@ -0,0 +1,44 @@ +// @flow + +import Big from 'big.js'; + +const UNITS = { + wei: '1', + kwei: '1000', + ada: '1000', + femtoether: '1000', + mwei: '1000000', + babbage: '1000000', + picoether: '1000000', + gwei: '1000000000', + shannon: '1000000000', + nanoether: '1000000000', + nano: '1000000000', + szabo: '1000000000000', + microether: '1000000000000', + micro: '1000000000000', + finney: '1000000000000000', + milliether: '1000000000000000', + milli: '1000000000000000', + ether: '1000000000000000000', + kether: '1000000000000000000000', + grand: '1000000000000000000000', + einstein: '1000000000000000000000', + mether: '1000000000000000000000000', + gether: '1000000000000000000000000000', + tether: '1000000000000000000000000000000' +}; + +type UNIT = $Keys; + +function getValueOfUnit(unit: UNIT) { + return new Big(UNITS[unit]); +} + +export function toEther(number: Big, unit: UNIT) { + return toWei(number, unit).div(getValueOfUnit('ether')); +} + +export function toWei(number: Big, unit: UNIT): Big { + return number.times(getValueOfUnit(unit)); +} diff --git a/common/libs/validators.js b/common/libs/validators.js index 1e637354..97ac5df3 100644 --- a/common/libs/validators.js +++ b/common/libs/validators.js @@ -20,9 +20,7 @@ export function isValidHex(str: string): boolean { return false; } if (str === '') return true; - str = str.substring(0, 2) == '0x' - ? str.substring(2).toUpperCase() - : str.toUpperCase(); + str = str.substring(0, 2) == '0x' ? str.substring(2).toUpperCase() : str.toUpperCase(); var re = /^[0-9A-F]+$/g; return re.test(str); } @@ -33,9 +31,7 @@ export function isValidENSorEtherAddress(address: string): boolean { export function isValidENSName(str: string) { try { - return ( - str.length > 6 && normalise(str) != '' && str.substring(0, 2) != '0x' - ); + return str.length > 6 && normalise(str) != '' && str.substring(0, 2) != '0x'; } catch (e) { return false; } @@ -65,14 +61,17 @@ function isChecksumAddress(address: string): boolean { function validateEtherAddress(address: string): boolean { if (address.substring(0, 2) != '0x') return false; else if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) return false; - else if ( - /^(0x)?[0-9a-f]{40}$/.test(address) || - /^(0x)?[0-9A-F]{40}$/.test(address) - ) - return true; + else if (/^(0x)?[0-9a-f]{40}$/.test(address) || /^(0x)?[0-9A-F]{40}$/.test(address)) return true; else return isChecksumAddress(address); } export function isValidPrivKey(length: number): boolean { return length === 64 || length === 128 || length === 132; } + +export function isPositiveIntegerOrZero(number: number): boolean { + if (isNaN(number) || !isFinite(number)) { + return false; + } + return number >= 0 && parseInt(number) === number; +} diff --git a/common/libs/wallet/base.js b/common/libs/wallet/base.js index f3463ccb..b3e20fa5 100644 --- a/common/libs/wallet/base.js +++ b/common/libs/wallet/base.js @@ -4,4 +4,8 @@ export default class BaseWallet { getAddress(): string { throw 'Implement me'; } + + getNakedAddress(): string { + return this.getAddress().replace('0x', '').toLowerCase(); + } } diff --git a/common/libs/wallet/index.js b/common/libs/wallet/index.js new file mode 100644 index 00000000..bbfba5d1 --- /dev/null +++ b/common/libs/wallet/index.js @@ -0,0 +1,4 @@ +// @flow + +export { default as BaseWallet } from './base'; +export { default as PrivKeyWallet } from './privkey'; diff --git a/common/reducers/customTokens.js b/common/reducers/customTokens.js new file mode 100644 index 00000000..6927dc55 --- /dev/null +++ b/common/reducers/customTokens.js @@ -0,0 +1,33 @@ +// @flow +import type { + CustomTokenAction, + AddCustomTokenAction, + RemoveCustomTokenAction +} from 'actions/customTokens'; +import type { Token } from 'config/data'; + +export type State = Token[]; + +const initialState: State = []; + +function addCustomToken(state: State, action: AddCustomTokenAction): State { + if (state.find(token => token.symbol === action.payload.symbol)) { + return state; + } + return [...state, action.payload]; +} + +function removeCustomToken(state: State, action: RemoveCustomTokenAction): State { + return state.filter(token => token.symbol !== action.payload); +} + +export function customTokens(state: State = initialState, action: CustomTokenAction): State { + switch (action.type) { + case 'CUSTOM_TOKEN_ADD': + return addCustomToken(state, action); + case 'CUSTOM_TOKEN_REMOVE': + return removeCustomToken(state, action); + default: + return state; + } +} diff --git a/common/reducers/index.js b/common/reducers/index.js index c5d65d70..2766625e 100644 --- a/common/reducers/index.js +++ b/common/reducers/index.js @@ -16,6 +16,12 @@ import type { State as EnsState } from './ens'; import * as wallet from './wallet'; import type { State as WalletState } from './wallet'; +import * as customTokens from './customTokens'; +import type { State as CustomTokensState } from './customTokens'; + +import * as rates from './rates'; +import type { State as RatesState } from './rates'; + import { reducer as formReducer } from 'redux-form'; import { combineReducers } from 'redux'; import { routerReducer } from 'react-router-redux'; @@ -25,7 +31,9 @@ export type State = { config: ConfigState, notifications: NotificationsState, ens: EnsState, - wallet: WalletState + wallet: WalletState, + customTokens: CustomTokensState, + rates: RatesState }; export default combineReducers({ @@ -35,6 +43,8 @@ export default combineReducers({ ...notifications, ...ens, ...wallet, + ...customTokens, + ...rates, form: formReducer, routing: routerReducer }); diff --git a/common/reducers/rates.js b/common/reducers/rates.js new file mode 100644 index 00000000..1f52986c --- /dev/null +++ b/common/reducers/rates.js @@ -0,0 +1,22 @@ +// @flow +import type { SetRatesAction, RatesAction } from 'actions/rates'; + +// SYMBOL -> PRICE TO BUY 1 ETH +export type State = { + [key: string]: number +}; + +const initialState: State = {}; + +function setRates(state: State, action: SetRatesAction): State { + return action.payload; +} + +export function rates(state: State = initialState, action: RatesAction): State { + switch (action.type) { + case 'RATES_SET': + return setRates(state, action); + default: + return state; + } +} diff --git a/common/reducers/wallet.js b/common/reducers/wallet.js index 7d08914f..d24dceb5 100644 --- a/common/reducers/wallet.js +++ b/common/reducers/wallet.js @@ -1,39 +1,50 @@ // @flow import type { WalletAction, - SaveWalletAction - // InitWalletAction + SetWalletAction, + SetBalanceAction, + SetTokenBalancesAction } from 'actions/wallet'; -import BaseWallet from 'libs/wallet/base'; +import { BaseWallet } from 'libs/wallet'; +import { toEther } from 'libs/units'; +import Big from 'big.js'; export type State = { inst: ?BaseWallet, - balance: number, + // in ETH + balance: Big, tokens: { - [string]: number + [string]: Big } }; const initialState: State = { inst: null, - balance: 0, + balance: new Big(0), tokens: {} }; -function saveWallet(state: State, action: SaveWalletAction): State { - return { ...state, inst: action.payload }; +function setWallet(state: State, action: SetWalletAction): State { + return { ...state, inst: action.payload, balance: new Big(0), tokens: {} }; } -function initWallet(state: State): State { - return { ...state, balance: 0, tokens: {} }; +function setBalance(state: State, action: SetBalanceAction): State { + const ethBalance = toEther(action.payload, 'wei'); + return { ...state, balance: ethBalance }; +} + +function setTokenBalances(state: State, action: SetTokenBalancesAction): State { + return { ...state, tokens: { ...state.tokens, ...action.payload } }; } export function wallet(state: State = initialState, action: WalletAction): State { switch (action.type) { - case 'WALLET_SAVE': - return saveWallet(state, action); - case 'WALLET_INIT': - return initWallet(state); + case 'WALLET_SET': + return setWallet(state, action); + case 'WALLET_SET_BALANCE': + return setBalance(state, action); + case 'WALLET_SET_TOKEN_BALANCES': + return setTokenBalances(state, action); default: return state; } diff --git a/common/sagas/rates.js b/common/sagas/rates.js new file mode 100644 index 00000000..95f2cc85 --- /dev/null +++ b/common/sagas/rates.js @@ -0,0 +1,19 @@ +// @flow +import { put, call } from 'redux-saga/effects'; +import type { Effect } from 'redux-saga/effects'; +import { setRates } from 'actions/rates'; + +const symbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP']; + +function fetchRates(symbols) { + return fetch( + `https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=${symbols.join( + ',' + )}` + ).then(r => r.json()); +} + +export default function* ratesSaga(): Generator { + const rates = yield call(fetchRates, symbols); + yield put(setRates(rates)); +} diff --git a/common/sagas/wallet.js b/common/sagas/wallet.js index de19c5be..d27300ae 100644 --- a/common/sagas/wallet.js +++ b/common/sagas/wallet.js @@ -1,35 +1,89 @@ // @flow -import { takeEvery, call, put, select } from 'redux-saga/effects'; +import { takeEvery, call, apply, put, select, fork } from 'redux-saga/effects'; import type { Effect } from 'redux-saga/effects'; -import { delay } from 'redux-saga'; -import { saveWallet, initWallet } from 'actions/wallet'; +import { setWallet, setBalance, setTokenBalances } from 'actions/wallet'; import type { UnlockPrivateKeyAction } from 'actions/wallet'; import { showNotification } from 'actions/notifications'; -import PrivKeyWallet from 'libs/wallet/privkey'; import translate from 'translations'; +import { PrivKeyWallet, BaseWallet } from 'libs/wallet'; +import { BaseNode } from 'libs/nodes'; +import { getNodeLib } from 'selectors/config'; +import { getWalletInst, getTokens } from 'selectors/wallet'; +import Big from 'big.js'; -function* init() { - yield put(initWallet()); - // const node = select(getNode); - // yield call(); - // fetch balance, - // fetch tokens - yield delay(100); +// FIXME MOVE ME +function padLeft(n: string, width: number, z: string = '0'): string { + return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; +} + +function getEthCallData(to: string, method: string, args: string[]) { + return { + to, + data: method + args.map(a => padLeft(a, 64)).join() + }; +} + +function* updateAccountBalance() { + const node: BaseNode = yield select(getNodeLib); + const wallet: ?BaseWallet = yield select(getWalletInst); + if (!wallet) { + return; + } + let balance = yield apply(node, node.getBalance, [wallet.getAddress()]); + yield put(setBalance(balance)); +} + +function* updateTokenBalances() { + const node = yield select(getNodeLib); + const wallet: ?BaseWallet = yield select(getWalletInst); + const tokens = yield select(getTokens); + if (!wallet) { + return; + } + const requests = tokens.map(token => + getEthCallData(token.address, '0x70a08231', [wallet.getNakedAddress()]) + ); + // FIXME handle errors + const tokenBalances = yield apply(node, node.ethCall, [requests]); + yield put( + setTokenBalances( + tokens.reduce((acc, t, i) => { + // FIXME + if (tokenBalances[i].error || tokenBalances[i].result === '0x') { + return acc; + } + let balance = Big(Number(tokenBalances[i].result)).div(Big(10).pow(t.decimal)); // definitely not safe + acc[t.symbol] = balance; + return acc; + }, {}) + ) + ); +} + +function* updateBalances() { + yield fork(updateAccountBalance); + yield fork(updateTokenBalances); } export function* unlockPrivateKey(action?: UnlockPrivateKeyAction): Generator { if (!action) return; let wallet = null; + try { wallet = new PrivKeyWallet(action.payload); } catch (e) { yield put(showNotification('danger', translate('INVALID_PKEY'))); return; } - yield put(saveWallet(wallet)); - yield call(init); + yield put(setWallet(wallet)); + yield call(updateBalances); } -export default function* notificationsSaga(): Generator { - yield takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey); +export default function* walletSaga(): Generator { + // useful for development + yield call(updateBalances); + yield [ + takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey), + takeEvery('CUSTOM_TOKEN_ADD', updateTokenBalances) + ]; } diff --git a/common/selectors/config.js b/common/selectors/config.js index 0f0e896d..26d19639 100644 --- a/common/selectors/config.js +++ b/common/selectors/config.js @@ -1,8 +1,13 @@ // @flow import type { State } from 'reducers'; import { BaseNode } from 'libs/nodes'; -import { NODES } from 'config/data'; +import { NODES, NETWORKS } from 'config/data'; +import type { NetworkConfig } from 'config/data'; export function getNodeLib(state: State): BaseNode { return NODES[state.config.nodeSelection].lib; } + +export function getNetworkConfig(state: State): NetworkConfig { + return NETWORKS[NODES[state.config.nodeSelection].network]; +} diff --git a/common/selectors/wallet.js b/common/selectors/wallet.js new file mode 100644 index 00000000..082558d1 --- /dev/null +++ b/common/selectors/wallet.js @@ -0,0 +1,37 @@ +// @flow +import type { State } from 'reducers'; +import { BaseWallet } from 'libs/wallet'; +import { getNetworkConfig } from 'selectors/config'; +import Big from 'big.js'; +import type { Token } from 'config/data'; + +export function getWalletInst(state: State): ?BaseWallet { + return state.wallet.inst; +} + +export type TokenBalance = { + symbol: string, + balance: Big, + custom: boolean +}; + +type MergedToken = Token & { + custom: boolean +}; + +export function getTokens(state: State): MergedToken[] { + const tokens: MergedToken[] = (getNetworkConfig(state).tokens: any); + return tokens.concat(state.customTokens.map(token => ({ ...token, custom: true }))); +} + +export function getTokenBalances(state: State): TokenBalance[] { + const tokens = getTokens(state); + if (!tokens) { + return []; + } + return tokens.map(t => ({ + symbol: t.symbol, + balance: state.wallet.tokens[t.symbol] ? state.wallet.tokens[t.symbol] : new Big(0), + custom: t.custom + })); +} diff --git a/common/store.js b/common/store.js index 86978944..928fbf19 100644 --- a/common/store.js +++ b/common/store.js @@ -1,11 +1,13 @@ -import { saveState, loadStatePropertyOrEmptyObject } from 'utils/localStorage'; +import { saveState, loadState, loadStatePropertyOrEmptyObject } from 'utils/localStorage'; import { createLogger } from 'redux-logger'; import createSagaMiddleware from 'redux-saga'; import notificationsSaga from './sagas/notifications'; import ensSaga from './sagas/ens'; import walletSaga from './sagas/wallet'; import bitySaga from './sagas/bity'; +import ratesSaga from './sagas/rates'; import { initialState as configInitialState } from 'reducers/config'; +import { initialState as customTokensInitialState } from 'reducers/customTokens'; import throttle from 'lodash/throttle'; import { composeWithDevTools } from 'redux-devtools-extension'; import Perf from 'react-addons-perf'; @@ -30,29 +32,28 @@ const configureStore = () => { middleware = applyMiddleware(sagaMiddleware, routerMiddleware(history)); } - const persistedConfigInitialState = { + const persistedInitialState = { config: { ...configInitialState, ...loadStatePropertyOrEmptyObject('config') - } + }, + customTokens: (loadState() || {}).customTokens || customTokensInitialState }; - const completePersistedInitialState = { - ...persistedConfigInitialState - }; - - store = createStore(RootReducer, completePersistedInitialState, middleware); + store = createStore(RootReducer, persistedInitialState, middleware); sagaMiddleware.run(notificationsSaga); sagaMiddleware.run(ensSaga); sagaMiddleware.run(walletSaga); sagaMiddleware.run(bitySaga); + sagaMiddleware.run(ratesSaga); store.subscribe( throttle(() => { saveState({ config: { languageSelection: store.getState().config.languageSelection - } + }, + customTokens: store.getState().customTokens }); }), 1000 diff --git a/common/utils/formatters.js b/common/utils/formatters.js index 8cc26eb2..d0db12c3 100644 --- a/common/utils/formatters.js +++ b/common/utils/formatters.js @@ -1,5 +1,18 @@ -//flow +// @flow +import Big from 'big.js'; export function toFixedIfLarger(number: number, fixedSize: number = 6): string { return parseFloat(number.toFixed(fixedSize)).toString(); } + +// Use in place of angular number filter +export function formatNumber(number: Big, digits: number = 3): string { + let parts = number.toFixed(digits).split('.'); + parts[1] = parts[1].replace(/0+/, ''); + if (!parts[1]) { + parts.pop(); + } + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); + + return parts.join('.'); +} diff --git a/common/utils/localStorage.js b/common/utils/localStorage.js index e15caf05..9f56a00d 100644 --- a/common/utils/localStorage.js +++ b/common/utils/localStorage.js @@ -7,7 +7,7 @@ export const loadState = () => { if (serializedState === null) { return undefined; } - return JSON.parse(serializedState); + return JSON.parse(serializedState || ''); } catch (err) { console.warn(' Warning: corrupted local storage'); } diff --git a/flow-typed/npm/big.js_v3.x.x.js b/flow-typed/npm/big.js_v3.x.x.js new file mode 100644 index 00000000..ce256f5f --- /dev/null +++ b/flow-typed/npm/big.js_v3.x.x.js @@ -0,0 +1,55 @@ +// flow-typed signature: 159b86cb4ea39f490d67f7c50f39dade +// flow-typed version: 94e9f7e0a4/big.js_v3.x.x/flow_>=v0.17.x + +declare module "big.js" { + + declare type $npm$big$number$object = number | string | Big; + declare type $npm$cmp$result = -1 | 0 | 1; + declare type DIGIT = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; + declare type ROUND_DOWN = 0; + declare type ROUND_HALF_UP = 1; + declare type ROUND_HALF_EVEN = 2; + declare type ROUND_UP = 3; + declare type RM = ROUND_DOWN | ROUND_HALF_UP | ROUND_HALF_EVEN | ROUND_UP; + + declare class Big { + // Properties + static DP: number; + static RM: RM; + static E_NEG: number; + static E_POS: number; + + c: Array; + e: number; + s: -1 | 1; + + // Constructors + static(value: $npm$big$number$object): Big; + constructor(value: $npm$big$number$object): Big; + + // Methods + abs() : Big; + cmp(n: $npm$big$number$object): $npm$cmp$result; + div(n: $npm$big$number$object): Big; + eq(n: $npm$big$number$object): boolean; + gt(n: $npm$big$number$object): boolean; + gte(n: $npm$big$number$object): boolean; + lt(n: $npm$big$number$object): boolean; + lte(n: $npm$big$number$object): boolean; + minus(n: $npm$big$number$object): Big; + mod(n: $npm$big$number$object): Big; + plus(n: $npm$big$number$object): Big; + pow(exp: number): Big; + round(dp: ?number, rm: ?RM): Big; + sqrt(): Big; + times(n: $npm$big$number$object): Big; + toExponential(dp: ?number): string; + toFixed(dp: ?number): string; + toPrecision(sd: ?number): string; + toString(): string; + valueOf(): string; + toJSON(): string; + } + + declare var exports: typeof Big; +} diff --git a/package.json b/package.json index 284a2132..cfb1cb5b 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "babel-cli": "^6.24.1", "babel-core": "^6.23.1", "babel-eslint": "^7.1.1", - "babel-loader": "^6.3.2", + "babel-loader": "^7.1.1", "babel-plugin-transform-react-constant-elements": "^6.23.0", "babel-plugin-transform-react-inline-elements": "^6.22.0", "babel-plugin-transform-react-jsx": "^6.23.0", @@ -47,6 +47,7 @@ "eslint": "^3.16.1", "eslint-loader": "^1.7.1", "eslint-plugin-react": "^6.10.0", + "express": "^4.15.3", "extract-text-webpack-plugin": "^2.0.0", "file-loader": "^0.11.0", "flow-bin": "^0.43.1", @@ -55,7 +56,6 @@ "html-webpack-plugin": "^2.28.0", "isomorphic-style-loader": "^1.1.0", "jest": "^19.0.2", - "json-server": "^0.9.5", "less": "^2.7.2", "less-loader": "^4.0.3", "minimist": "^1.2.0", @@ -70,9 +70,9 @@ "sass-loader": "^6.0.2", "style-loader": "^0.16.1", "url-loader": "^0.5.8", - "webpack": "2.3.3", - "webpack-dev-middleware": "^1.10.1", - "webpack-hot-middleware": "^2.17.1" + "webpack": "3.2.0", + "webpack-dev-middleware": "^1.11.0", + "webpack-hot-middleware": "^2.18.2" }, "scripts": { "db": "nodemon ./db", diff --git a/spec/sagas/wallet.spec.js b/spec/sagas/wallet.spec.js index a26c5f59..163e590d 100644 --- a/spec/sagas/wallet.spec.js +++ b/spec/sagas/wallet.spec.js @@ -1,3 +1,5 @@ +// break dep cycle, we have to fix it for good somehow +import translate from 'translations'; import { unlockPrivateKey } from 'sagas/wallet'; describe('Wallet saga', () => {