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:
parent
52cbdfbd0f
commit
780f3ba94f
|
@ -33,6 +33,8 @@
|
|||
"globals": {
|
||||
"SyntheticInputEvent": false,
|
||||
"SyntheticKeyboardEvent": false,
|
||||
"Generator": false
|
||||
"Generator": false,
|
||||
"$Keys": false,
|
||||
"SyntheticMouseEvent": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) });
|
||||
};
|
||||
}
|
|
@ -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);
|
|
@ -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 });
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export { default } from './BalanceSidebar';
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
];
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -4,4 +4,8 @@ export default class BaseWallet {
|
|||
getAddress(): string {
|
||||
throw 'Implement me';
|
||||
}
|
||||
|
||||
getNakedAddress(): string {
|
||||
return this.getAddress().replace('0x', '').toLowerCase();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
// @flow
|
||||
|
||||
export { default as BaseWallet } from './base';
|
||||
export { default as PrivKeyWallet } from './privkey';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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)
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}));
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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('.');
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
10
package.json
10
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",
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Loading…
Reference in New Issue