balance fetching (#41)

* balance fetching

* fix tests

* bump deps

* validate custom token form

* equiv values

* fix rates saga naming

* address review comments
This commit is contained in:
crptm 2017-07-14 01:02:39 +04:00 committed by Daniel Ternyak
parent 52cbdfbd0f
commit 780f3ba94f
34 changed files with 1426 additions and 116 deletions

View File

@ -33,6 +33,8 @@
"globals": {
"SyntheticInputEvent": false,
"SyntheticKeyboardEvent": false,
"Generator": false
"Generator": false,
"$Keys": false,
"SyntheticMouseEvent": false
}
}

View File

@ -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
};
}

15
common/actions/rates.js Normal file
View File

@ -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
};
}

View File

@ -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
};
}

View File

@ -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 (
<div className="custom-token-fields">
<label>
{translate('TOKEN_Addr')}
</label>
<input
className={
'form-control input-sm ' +
(isValidETHAddress(this.state.address) ? 'is-valid' : 'is-invalid')
}
type="text"
name="address"
value={this.state.address}
onChange={this.onFieldChange}
/>
<label>
{translate('TOKEN_Symbol')}
</label>
<input
className={
'form-control input-sm ' + (this.state.symbol !== '' ? 'is-valid' : 'is-invalid')
}
type="text"
name="symbol"
value={this.state.symbol}
onChange={this.onFieldChange}
/>
<label>
{translate('TOKEN_Dec')}
</label>
<input
className={
'form-control input-sm ' +
(isPositiveIntegerOrZero(parseInt(this.state.decimal)) ? 'is-valid' : 'is-invalid')
}
type="text"
name="decimal"
value={this.state.decimal}
onChange={this.onFieldChange}
/>
<div
className={`btn btn-primary btn-sm ${this.isValid() ? '' : 'disabled'}`}
onClick={this.onSave}
>
{translate('x_Save')}
</div>
</div>
);
}
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) });
};
}

View File

@ -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 (
<aside>
<h5>
{translate('sidebar_AccountAddr')}
</h5>
<ul className="account-info">
<Identicon address={wallet.getAddress()} />
<span className="mono wrap">
{wallet.getAddress()}
</span>
</ul>
<hr />
<h5>
{translate('sidebar_AccountBal')}
</h5>
<ul
className="account-info point"
onDoubleClick={this.toggleShowLongBalance}
title={`${balance.toString()} (Double-Click)`}
>
<li>
<span className="mono wrap">
{this.state.showLongBalance ? balance.toString() : formatNumber(balance)}
</span>
{` ${network.name}`}
</li>
</ul>
<TokenBalances
tokens={tokenBalances}
onAddCustomToken={this.props.addCustomToken}
onRemoveCustomToken={this.props.removeCustomToken}
/>
<hr />
{(!!blockExplorer || !!tokenExplorer) &&
<div>
<h5>
{translate('sidebar_TransHistory')}
</h5>
<ul className="account-info">
{!!blockExplorer &&
<li>
<a
href={blockExplorer.address.replace('[[address]]', wallet.getAddress())}
target="_blank"
>
{`${network.name} (${blockExplorer.name})`}
</a>
</li>}
{!!tokenExplorer &&
<li>
<a
href={tokenExplorer.address.replace('[[address]]', wallet.getAddress())}
target="_blank"
>
{`Tokens (${tokenExplorer.name})`}
</a>
</li>}
</ul>
</div>}
<hr />
{!!Object.keys(rates).length &&
<section>
<h5>
{translate('sidebar_Equiv')}
</h5>
<ul className="account-info">
{rates['BTC'] &&
<li>
<span className="mono wrap">{formatNumber(balance.times(rates['BTC']))}</span> BTC
</li>}
{rates['REP'] &&
<li>
<span className="mono wrap">{formatNumber(balance.times(rates['REP']))}</span> REP
</li>}
{rates['EUR'] &&
<li>
<span className="mono wrap">
{formatNumber(balance.times(rates['EUR']))}
</span>
{' EUR'}
</li>}
{rates['USD'] &&
<li>
<span className="mono wrap">
${formatNumber(balance.times(rates['USD']))}
</span>
{' USD'}
</li>}
{rates['GBP'] &&
<li>
<span className="mono wrap">
£{formatNumber(balance.times(rates['GBP']))}
</span>
{' GBP'}
</li>}
{rates['CHF'] &&
<li>
<span className="mono wrap">{formatNumber(balance.times(rates['CHF']))}</span> CHF
</li>}
</ul>
<Link to={'swap'} className="btn btn-primary btn-sm">
Swap via bity
</Link>
</section>}
</aside>
);
}
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);

View File

@ -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 (
<section className="token-balances">
<h5>
{translate('sidebar_TokenBal')}
</h5>
<table className="account-info">
<tbody>
{tokens
.filter(token => !token.balance.eq(0) || token.custom || this.state.showAllTokens)
.map(token =>
<TokenRow
key={token.symbol}
balance={token.balance}
symbol={token.symbol}
custom={token.custom}
onRemove={this.props.onRemoveCustomToken}
/>
)}
</tbody>
</table>
<a className="btn btn-default btn-sm" onClick={this.toggleShowAllTokens}>
{!this.state.showAllTokens ? 'Show All Tokens' : 'Hide Tokens'}{' '}
</a>
<a className="btn btn-default btn-sm" onClick={this.toggleShowCustomTokenForm}>
<span>
{translate('SEND_custom')}
</span>
</a>
{this.state.showCustomTokenForm && <AddCustomTokenForm onSave={this.addCustomToken} />}
</section>
);
}
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 });
};
}

View File

@ -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 (
<tr>
<td
className="mono wrap point"
title={`${balance.toString()} (Double-Click)`}
onDoubleClick={this.toggleShowLongBalance}
>
{!!custom &&
<img
src="images/icon-remove.svg"
className="token-remove"
title="Remove Token"
onClick={this.onRemove}
/>}
<span>
{this.state.showLongBalance ? balance.toString() : formatNumber(balance)}
</span>
</td>
<td>
{symbol}
</td>
</tr>
);
}
toggleShowLongBalance = (e: SyntheticInputEvent) => {
e.preventDefault();
this.setState(state => {
return {
showLongBalance: !state.showLongBalance
};
});
};
onRemove = () => {
this.props.onRemove(this.props.symbol);
};
}

View File

@ -0,0 +1,3 @@
// @flow
export { default } from './BalanceSidebar';

View File

@ -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';

View File

@ -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
};
}

View File

@ -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
}
};

439
common/config/tokens/eth.js Normal file
View File

@ -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
}
];

View File

@ -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 {
{'' /* <!-- Sidebar --> */}
<section className="col-sm-4">
<div style={{ maxWidth: 350 }}>
{'' /* <wallet-balance-drtv /> */}
<BalanceSidebar />
<hr />
<Donate onDonate={this.onNewTx} />
</div>
@ -124,9 +116,8 @@ export class SendTransaction extends React.Component {
<div className="row form-group">
<div className="alert alert-danger col-xs-12 clearfix">
<strong>
Warning! You do not have enough funds to
complete this swap.
</strong>{' '}
Warning! You do not have enough funds to complete this swap.
</strong>
<br />
Please add more funds or access a different wallet.
</div>
@ -147,23 +138,14 @@ export class SendTransaction extends React.Component {
unit={unit}
onChange={readOnly ? void 0 : this.onAmountChange}
/>
<GasField
value={gasLimit}
onChange={readOnly ? void 0 : this.onGasChange}
/>
<GasField value={gasLimit} onChange={readOnly ? void 0 : this.onGasChange} />
{unit === 'ether' &&
<DataField
value={data}
onChange={readOnly ? void 0 : this.onDataChange}
/>}
<DataField value={data} onChange={readOnly ? void 0 : this.onDataChange} />}
<CustomMessage message={customMessage} />
<div className="row form-group">
<div className="col-xs-12 clearfix">
<a
className="btn btn-info btn-block"
onClick={this.generateTx}
>
<a className="btn btn-info btn-block" onClick={this.generateTx}>
{translate('SEND_generate')}
</a>
</div>
@ -171,7 +153,9 @@ export class SendTransaction extends React.Component {
<div className="row form-group">
<div className="col-sm-6">
<label> {translate('SEND_raw')} </label>
<label>
{translate('SEND_raw')}
</label>
<textarea className="form-control" rows="4" readOnly>
{'' /*rawTx*/}
</textarea>
@ -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
};
}

View File

@ -1,8 +1,8 @@
// @flow
import Big from 'big.js';
export default class BaseNode {
// FIXME bignumber?
queryBalance(address: string): Promise<number> {
throw 'Implement me';
async getBalance(address: string): Promise<Big> {
throw new Error('Implement me');
}
}

View File

@ -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<Big> {
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<JsonRpcResponse> {
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<JsonRpcResponse[]> {
return fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requests)
}).then(r => r.json());
}
}

44
common/libs/units.js Normal file
View File

@ -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<typeof UNITS>;
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));
}

View File

@ -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;
}

View File

@ -4,4 +4,8 @@ export default class BaseWallet {
getAddress(): string {
throw 'Implement me';
}
getNakedAddress(): string {
return this.getAddress().replace('0x', '').toLowerCase();
}
}

View File

@ -0,0 +1,4 @@
// @flow
export { default as BaseWallet } from './base';
export { default as PrivKeyWallet } from './privkey';

View File

@ -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;
}
}

View File

@ -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
});

22
common/reducers/rates.js Normal file
View File

@ -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;
}
}

View File

@ -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;
}

19
common/sagas/rates.js Normal file
View File

@ -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<Effect, void, any> {
const rates = yield call(fetchRates, symbols);
yield put(setRates(rates));
}

View File

@ -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<Effect, void, any> {
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<Effect, void, any> {
yield takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey);
export default function* walletSaga(): Generator<Effect | Effect[], void, any> {
// useful for development
yield call(updateBalances);
yield [
takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey),
takeEvery('CUSTOM_TOKEN_ADD', updateTokenBalances)
];
}

View File

@ -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];
}

View File

@ -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
}));
}

View File

@ -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

View File

@ -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('.');
}

View File

@ -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');
}

55
flow-typed/npm/big.js_v3.x.x.js vendored Normal file
View File

@ -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<DIGIT>;
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;
}

View File

@ -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",

View File

@ -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', () => {