Replace bignumber.js with bn.js (#319)

* Add definition file for bn.js

* Remove types-bn

* make isBN a static property

* progress commit -- swap out bignumber.js for bn.js

* Swap out bignumber for bn in vendor

* Change modn to number return

* Start to strip out units lib for a string manipulation based lib

* Convert codebase to only base units

* Get rid of useless component

* Handle only wei in values

* Use unit conversion in sidebar

* Automatically strip hex prefix, and  handle decimal edge case

* Handle base 16 wei in transactions

* Make a render callback component for dealing with unit conversion

* Switch contracts to use bn.js, and get transaction values from signedTx instead of state

* Get send transaction  working with bn.js

* Remove redundant hex stripping,  return base value of tokens

* Cleanup unit file

* Re-implement toFixed for strings

* Use formatNumber in codebase

* Cleanup code

* Undo package test changes

* Update snapshot and remove console logs

* Use TokenValue / Wei more consistently where applicable

* Add typing to deterministicWallets, fix confirmation modal, make UnitDisplay more flexible

* Clean up prop handling in UnitDisplay

* Change instanceof to typeof check, change boolean of displayBalance

* Fix tsc errors

* Fix token row displaying wrong decimals

* Fix deterministic modal token display

* Handle hex and non hex strings automatically in BN conversion

* Fix handling of strings and numbers for BN

* add web3 fixes & comments

* Display short balances on deterministic modals

* add more tests, fix rounding

* Add spacer to balance sidebar network name

* Fix tsc error
This commit is contained in:
HenryNguyen5 2017-11-12 14:45:52 -05:00 committed by Daniel Ternyak
parent c9c147db52
commit 8fe664c171
52 changed files with 1129 additions and 564 deletions

View File

@ -6,5 +6,5 @@ export enum TypeKeys {
CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE', CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE',
CONFIG_FORCE_OFFLINE = 'CONFIG_FORCE_OFFLINE', CONFIG_FORCE_OFFLINE = 'CONFIG_FORCE_OFFLINE',
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS', CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS',
CONFIG_NODE_WEB3_UNSET = 'CONFIG_NODE_WEB3_UNSET', CONFIG_NODE_WEB3_UNSET = 'CONFIG_NODE_WEB3_UNSET'
} }

View File

@ -1,14 +1,19 @@
import { BigNumber } from 'bignumber.js'; import { TokenValue, Wei } from 'libs/units';
export interface TokenValues { export interface ITokenData {
[key: string]: BigNumber; value: TokenValue;
decimal: number;
}
export interface ITokenValues {
[key: string]: ITokenData | null;
} }
export interface DeterministicWalletData { export interface DeterministicWalletData {
index: number; index: number;
address: string; address: string;
value?: BigNumber; value?: TokenValue;
tokenValues: TokenValues; tokenValues: ITokenValues;
} }
/*** Get determinstic wallets ***/ /*** Get determinstic wallets ***/
@ -39,8 +44,8 @@ export interface SetDesiredTokenAction {
/*** Set wallet values ***/ /*** Set wallet values ***/
export interface UpdateDeterministicWalletArgs { export interface UpdateDeterministicWalletArgs {
address: string; address: string;
value?: BigNumber; value?: Wei;
tokenValues?: TokenValues; tokenValues?: ITokenValues;
index?: any; index?: any;
} }

View File

@ -1,5 +1,4 @@
import { BigNumber } from 'bignumber.js'; import { Wei, TokenValue } from 'libs/units';
import { Wei } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet'; import { IWallet } from 'libs/wallet/IWallet';
import * as types from './actionTypes'; import * as types from './actionTypes';
import { TypeKeys } from './constants'; import { TypeKeys } from './constants';
@ -58,7 +57,7 @@ export function setBalance(value: Wei): types.SetBalanceAction {
export type TSetTokenBalances = typeof setTokenBalances; export type TSetTokenBalances = typeof setTokenBalances;
export function setTokenBalances(payload: { export function setTokenBalances(payload: {
[key: string]: BigNumber; [key: string]: TokenValue;
}): types.SetTokenBalancesAction { }): types.SetTokenBalancesAction {
return { return {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES, type: TypeKeys.WALLET_SET_TOKEN_BALANCES,

View File

@ -1,5 +1,4 @@
import { BigNumber } from 'bignumber.js'; import { Wei, TokenValue } from 'libs/units';
import { Wei } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet'; import { IWallet } from 'libs/wallet/IWallet';
import { TypeKeys } from './constants'; import { TypeKeys } from './constants';
@ -43,7 +42,7 @@ export interface SetBalanceAction {
export interface SetTokenBalancesAction { export interface SetTokenBalancesAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES; type: TypeKeys.WALLET_SET_TOKEN_BALANCES;
payload: { payload: {
[key: string]: BigNumber; [key: string]: TokenValue;
}; };
} }

View File

@ -1,15 +1,14 @@
import { TFetchCCRates } from 'actions/rates'; import { TFetchCCRates } from 'actions/rates';
import { Identicon } from 'components/ui'; import { Identicon, UnitDisplay } from 'components/ui';
import { NetworkConfig } from 'config/data'; import { NetworkConfig } from 'config/data';
import { Ether } from 'libs/units';
import { IWallet } from 'libs/wallet'; import { IWallet } from 'libs/wallet';
import { Wei } from 'libs/units';
import React from 'react'; import React from 'react';
import translate from 'translations'; import translate from 'translations';
import { formatNumber } from 'utils/formatters';
import './AccountInfo.scss'; import './AccountInfo.scss';
interface Props { interface Props {
balance: Ether; balance: Wei;
wallet: IWallet; wallet: IWallet;
network: NetworkConfig; network: NetworkConfig;
fetchCCRates: TFetchCCRates; fetchCCRates: TFetchCCRates;
@ -54,7 +53,7 @@ export default class AccountInfo extends React.Component<Props, State> {
public render() { public render() {
const { network, balance } = this.props; const { network, balance } = this.props;
const { blockExplorer, tokenExplorer } = network; const { blockExplorer, tokenExplorer } = network;
const { address } = this.state; const { address, showLongBalance } = this.state;
return ( return (
<div className="AccountInfo"> <div className="AccountInfo">
@ -80,9 +79,11 @@ export default class AccountInfo extends React.Component<Props, State> {
className="AccountInfo-list-item-clickable mono wrap" className="AccountInfo-list-item-clickable mono wrap"
onClick={this.toggleShowLongBalance} onClick={this.toggleShowLongBalance}
> >
{this.state.showLongBalance <UnitDisplay
? balance ? balance.toString() : '???' value={balance}
: balance ? formatNumber(balance.amount) : '???'} unit={'ether'}
displayShortBalance={!showLongBalance}
/>
</span> </span>
{` ${network.name}`} {` ${network.name}`}
</li> </li>
@ -90,28 +91,28 @@ export default class AccountInfo extends React.Component<Props, State> {
</div> </div>
{(!!blockExplorer || !!tokenExplorer) && ( {(!!blockExplorer || !!tokenExplorer) && (
<div className="AccountInfo-section"> <div className="AccountInfo-section">
<h5 className="AccountInfo-section-header"> <h5 className="AccountInfo-section-header">
{translate('sidebar_TransHistory')} {translate('sidebar_TransHistory')}
</h5> </h5>
<ul className="AccountInfo-list"> <ul className="AccountInfo-list">
{!!blockExplorer && ( {!!blockExplorer && (
<li className="AccountInfo-list-item"> <li className="AccountInfo-list-item">
<a href={blockExplorer.address(address)} target="_blank"> <a href={blockExplorer.address(address)} target="_blank">
{`${network.name} (${blockExplorer.name})`} {`${network.name} (${blockExplorer.name})`}
</a> </a>
</li> </li>
)} )}
{!!tokenExplorer && ( {!!tokenExplorer && (
<li className="AccountInfo-list-item"> <li className="AccountInfo-list-item">
<a href={tokenExplorer.address(address)} target="_blank"> <a href={tokenExplorer.address(address)} target="_blank">
{`Tokens (${tokenExplorer.name})`} {`Tokens (${tokenExplorer.name})`}
</a> </a>
</li> </li>
)} )}
</ul> </ul>
</div> </div>
)} )}
</div> </div>
); );
} }

View File

@ -1,13 +1,13 @@
import { Ether } from 'libs/units'; import { Wei } from 'libs/units';
import React from 'react'; import React from 'react';
import translate from 'translations'; import translate from 'translations';
import { formatNumber } from 'utils/formatters';
import './EquivalentValues.scss'; import './EquivalentValues.scss';
import { State } from 'reducers/rates'; import { State } from 'reducers/rates';
import { symbols } from 'actions/rates'; import { symbols } from 'actions/rates';
import { UnitDisplay } from 'components/ui';
interface Props { interface Props {
balance?: Ether; balance?: Wei;
rates?: State['rates']; rates?: State['rates'];
ratesError?: State['ratesError']; ratesError?: State['ratesError'];
} }
@ -33,9 +33,15 @@ export default class EquivalentValues extends React.Component<Props, {}> {
</span> </span>
<span className="EquivalentValues-values-currency-value"> <span className="EquivalentValues-values-currency-value">
{' '} {' '}
{balance {balance ? (
? formatNumber(balance.amount.times(rates[key])) <UnitDisplay
: '???'} unit={'ether'}
value={balance.muln(rates[key])}
displayShortBalance={2}
/>
) : (
'???'
)}
</span> </span>
</li> </li>
); );

View File

@ -1,13 +1,14 @@
import removeIcon from 'assets/images/icon-remove.svg'; import removeIcon from 'assets/images/icon-remove.svg';
import { BigNumber } from 'bignumber.js';
import React from 'react'; import React from 'react';
import { formatNumber } from 'utils/formatters'; import { TokenValue } from 'libs/units';
import { UnitDisplay } from 'components/ui';
import './TokenRow.scss'; import './TokenRow.scss';
interface Props { interface Props {
balance: BigNumber; balance: TokenValue;
symbol: string; symbol: string;
custom?: boolean; custom?: boolean;
decimal: number;
onRemove(symbol: string): void; onRemove(symbol: string): void;
} }
interface State { interface State {
@ -18,9 +19,11 @@ export default class TokenRow extends React.Component<Props, State> {
public state = { public state = {
showLongBalance: false showLongBalance: false
}; };
public render() { public render() {
const { balance, symbol, custom } = this.props; const { balance, symbol, custom, decimal } = this.props;
const { showLongBalance } = this.state; const { showLongBalance } = this.state;
return ( return (
<tr className="TokenRow"> <tr className="TokenRow">
<td <td
@ -28,21 +31,24 @@ export default class TokenRow extends React.Component<Props, State> {
title={`${balance.toString()} (Double-Click)`} title={`${balance.toString()} (Double-Click)`}
onDoubleClick={this.toggleShowLongBalance} onDoubleClick={this.toggleShowLongBalance}
> >
{!!custom && {!!custom && (
<img <img
src={removeIcon} src={removeIcon}
className="TokenRow-balance-remove" className="TokenRow-balance-remove"
title="Remove Token" title="Remove Token"
onClick={this.onRemove} onClick={this.onRemove}
tabIndex={0} tabIndex={0}
/>} />
)}
<span> <span>
{showLongBalance ? balance.toString() : formatNumber(balance)} <UnitDisplay
value={balance}
decimal={decimal}
displayShortBalance={!showLongBalance}
/>
</span> </span>
</td> </td>
<td className="TokenRow-symbol"> <td className="TokenRow-symbol">{symbol}</td>
{symbol}
</td>
</tr> </tr>
); );
} }

View File

@ -25,25 +25,24 @@ export default class TokenBalances extends React.Component<Props, State> {
public render() { public render() {
const { tokens } = this.props; const { tokens } = this.props;
const shownTokens = tokens.filter( const shownTokens = tokens.filter(
token => !token.balance.eq(0) || token.custom || this.state.showAllTokens token => !token.balance.eqn(0) || token.custom || this.state.showAllTokens
); );
return ( return (
<section className="TokenBalances"> <section className="TokenBalances">
<h5 className="TokenBalances-title"> <h5 className="TokenBalances-title">{translate('sidebar_TokenBal')}</h5>
{translate('sidebar_TokenBal')}
</h5>
<table className="TokenBalances-rows"> <table className="TokenBalances-rows">
<tbody> <tbody>
{shownTokens.map(token => {shownTokens.map(token => (
<TokenRow <TokenRow
key={token.symbol} key={token.symbol}
balance={token.balance} balance={token.balance}
symbol={token.symbol} symbol={token.symbol}
custom={token.custom} custom={token.custom}
decimal={token.decimal}
onRemove={this.props.onRemoveCustomToken} onRemove={this.props.onRemoveCustomToken}
/> />
)} ))}
</tbody> </tbody>
</table> </table>
@ -58,16 +57,15 @@ export default class TokenBalances extends React.Component<Props, State> {
className="btn btn-default btn-xs" className="btn btn-default btn-xs"
onClick={this.toggleShowCustomTokenForm} onClick={this.toggleShowCustomTokenForm}
> >
<span> <span>{translate('SEND_custom')}</span>
{translate('SEND_custom')}
</span>
</button> </button>
</div> </div>
{this.state.showCustomTokenForm && {this.state.showCustomTokenForm && (
<div className="TokenBalances-form"> <div className="TokenBalances-form">
<AddCustomTokenForm onSave={this.addCustomToken} /> <AddCustomTokenForm onSave={this.addCustomToken} />
</div>} </div>
)}
</section> </section>
); );
} }

View File

@ -7,7 +7,7 @@ import {
import { showNotification, TShowNotification } from 'actions/notifications'; import { showNotification, TShowNotification } from 'actions/notifications';
import { fetchCCRates as dFetchCCRates, TFetchCCRates } from 'actions/rates'; import { fetchCCRates as dFetchCCRates, TFetchCCRates } from 'actions/rates';
import { NetworkConfig } from 'config/data'; import { NetworkConfig } from 'config/data';
import { Ether } from 'libs/units'; import { Wei } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet'; import { IWallet } from 'libs/wallet/IWallet';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -27,7 +27,7 @@ import OfflineToggle from './OfflineToggle';
interface Props { interface Props {
wallet: IWallet; wallet: IWallet;
balance: Ether; balance: Wei;
network: NetworkConfig; network: NetworkConfig;
tokenBalances: TokenBalance[]; tokenBalances: TokenBalance[];
rates: State['rates']; rates: State['rates'];

View File

@ -91,9 +91,9 @@ export default class Header extends Component<Props, {}> {
<div className="Header-branding-right-dropdown"> <div className="Header-branding-right-dropdown">
<LanguageDropDown <LanguageDropDown
ariaLabel={`change language. current language ${languages[ ariaLabel={`change language. current language ${
selectedLanguage languages[selectedLanguage]
]}`} }`}
options={Object.values(languages)} options={Object.values(languages)}
value={languages[selectedLanguage]} value={languages[selectedLanguage]}
extra={ extra={
@ -111,7 +111,9 @@ export default class Header extends Component<Props, {}> {
<div className="Header-branding-right-dropdown"> <div className="Header-branding-right-dropdown">
<ColorDropdown <ColorDropdown
ariaLabel={`change node. current node ${selectedNode.network} node by ${selectedNode.service}`} ariaLabel={`change node. current node ${
selectedNode.network
} node by ${selectedNode.service}`}
options={nodeOptions} options={nodeOptions}
value={nodeSelection} value={nodeSelection}
extra={ extra={

View File

@ -7,12 +7,14 @@ import {
SetDesiredTokenAction SetDesiredTokenAction
} from 'actions/deterministicWallets'; } from 'actions/deterministicWallets';
import Modal, { IButton } from 'components/ui/Modal'; import Modal, { IButton } from 'components/ui/Modal';
import { AppState } from 'reducers';
import { NetworkConfig } from 'config/data'; import { NetworkConfig } from 'config/data';
import { isValidPath } from 'libs/validators'; import { isValidPath } from 'libs/validators';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getNetworkConfig } from 'selectors/config'; import { getNetworkConfig } from 'selectors/config';
import { getTokens, MergedToken } from 'selectors/wallet'; import { getTokens, MergedToken } from 'selectors/wallet';
import { UnitDisplay } from 'components/ui';
import './DeterministicWalletsModal.scss'; import './DeterministicWalletsModal.scss';
const WALLETS_PER_PAGE = 5; const WALLETS_PER_PAGE = 5;
@ -264,15 +266,12 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
); );
}; };
private renderWalletRow(wallet) { private renderWalletRow(wallet: DeterministicWalletData) {
const { desiredToken, network } = this.props; const { desiredToken, network } = this.props;
const { selectedAddress } = this.state; const { selectedAddress } = this.state;
// Get renderable values, but keep 'em short // Get renderable values, but keep 'em short
const value = wallet.value ? wallet.value.toEther().toPrecision(4) : ''; const token = wallet.tokenValues[desiredToken];
const tokenValue = wallet.tokenValues[desiredToken]
? wallet.tokenValues[desiredToken].toPrecision(4)
: '';
return ( return (
<tr <tr
@ -290,10 +289,24 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
{wallet.address} {wallet.address}
</td> </td>
<td> <td>
{value} {network.unit} <UnitDisplay
unit={'ether'}
value={wallet.value}
symbol={network.unit}
displayShortBalance={true}
/>
</td> </td>
<td> <td>
{tokenValue} {desiredToken} {token ? (
<UnitDisplay
decimal={token.decimal}
value={token.value}
symbol={desiredToken}
displayShortBalance={true}
/>
) : (
'???'
)}
</td> </td>
<td> <td>
<a <a
@ -308,7 +321,7 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
} }
} }
function mapStateToProps(state) { function mapStateToProps(state: AppState) {
return { return {
wallets: state.deterministicWallets.wallets, wallets: state.deterministicWallets.wallets,
desiredToken: state.deterministicWallets.desiredToken, desiredToken: state.deterministicWallets.desiredToken,

View File

@ -54,9 +54,9 @@ export default class KeystoreDecrypt extends Component {
<div className={file.length && passReq ? '' : 'hidden'}> <div className={file.length && passReq ? '' : 'hidden'}>
<p>{translate('ADD_Label_3')}</p> <p>{translate('ADD_Label_3')}</p>
<input <input
className={`form-control ${password.length > 0 className={`form-control ${
? 'is-valid' password.length > 0 ? 'is-valid' : 'is-invalid'
: 'is-invalid'}`} }`}
value={password} value={password}
onChange={this.onPasswordChange} onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}

View File

@ -0,0 +1,60 @@
import { toTokenBase } from 'libs/units';
import React, { Component } from 'react';
interface IChildren {
onUserInput: UnitConverter['onUserInput'];
convertedUnit: string;
}
interface IFakeEvent {
currentTarget: {
value: string;
};
}
export interface Props {
decimal: number;
children({ onUserInput, convertedUnit }: IChildren): React.ReactElement<any>;
onChange(baseUnit: IFakeEvent);
}
interface State {
userInput: string;
}
const initialState = { userInput: '' };
export class UnitConverter extends Component<Props, State> {
public state: State = initialState;
public componentWillReceiveProps(nextProps: Props) {
const { userInput } = this.state;
if (this.props.decimal !== nextProps.decimal) {
this.baseUnitCb(userInput, nextProps.decimal);
}
}
public onUserInput = (e: React.FormEvent<HTMLInputElement>) => {
const { value } = e.currentTarget;
const { decimal } = this.props;
this.baseUnitCb(value, decimal);
this.setState({ userInput: value });
};
public render() {
return this.props.children({
onUserInput: this.onUserInput,
convertedUnit: this.state.userInput
});
}
private baseUnitCb = (value: string, decimal: number) => {
const baseUnit = toTokenBase(value, decimal).toString();
const fakeEvent = {
currentTarget: {
value: baseUnit
}
};
this.props.onChange(fakeEvent);
};
}

View File

@ -0,0 +1 @@
export * from './UnitConverter';

View File

@ -0,0 +1,73 @@
import React from 'react';
import {
fromTokenBase,
getDecimal,
UnitKey,
Wei,
TokenValue
} from 'libs/units';
import { formatNumber as format } from 'utils/formatters';
interface Props {
/**
* @description base value of the token / ether, incase of waiting for API calls, we can return '???'
* @type {TokenValue | Wei}
* @memberof Props
*/
value?: TokenValue | Wei;
/**
* @description Symbol to display to the right of the value, such as 'ETH'
* @type {string}
* @memberof Props
*/
symbol?: string;
/**
* @description display the long balance, if false, trims it to 3 decimal places, if a number is specified then that number is the number of digits to be displayed.
* @type {boolean}
* @memberof Props
*/
displayShortBalance?: boolean | number;
}
interface EthProps extends Props {
unit: UnitKey;
}
interface TokenProps extends Props {
decimal: number;
}
const isEthereumUnit = (param: EthProps | TokenProps): param is EthProps =>
!!(param as EthProps).unit;
const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
const { value, symbol, displayShortBalance } = params;
if (!value) {
return <span>???</span>;
}
const convertedValue = isEthereumUnit(params)
? fromTokenBase(value, getDecimal(params.unit))
: fromTokenBase(value, params.decimal);
let formattedValue;
if (displayShortBalance) {
const digits =
typeof displayShortBalance === 'number' && displayShortBalance;
formattedValue = digits
? format(convertedValue, digits)
: format(convertedValue);
} else {
formattedValue = convertedValue;
}
return (
<span>
{formattedValue}
{symbol ? ` ${symbol}` : ''}
</span>
);
};
export default UnitDisplay;

View File

@ -6,3 +6,4 @@ export { default as Modal } from './Modal';
export { default as UnlockHeader } from './UnlockHeader'; export { default as UnlockHeader } from './UnlockHeader';
export { default as QRCode } from './QRCode'; export { default as QRCode } from './QRCode';
export { default as NewTabLink } from './NewTabLink'; export { default as NewTabLink } from './NewTabLink';
export { default as UnitDisplay } from './UnitDisplay';

View File

@ -1,4 +1,5 @@
import Big from 'bignumber.js'; import BN from 'bn.js';
import { Wei } from 'libs/units';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { import {
generateCompleteTransaction as makeAndSignTx, generateCompleteTransaction as makeAndSignTx,
@ -12,7 +13,6 @@ import {
} from 'containers/Tabs/Contracts/components/TxModal'; } from 'containers/Tabs/Contracts/components/TxModal';
import { import {
TxCompare, TxCompare,
Props as TCProps,
TTxCompare TTxCompare
} from 'containers/Tabs/Contracts/components/TxCompare'; } from 'containers/Tabs/Contracts/components/TxCompare';
import { withTx } from 'containers/Tabs/Contracts/components//withTx'; import { withTx } from 'containers/Tabs/Contracts/components//withTx';
@ -81,25 +81,13 @@ export const deployHOC = PassedComponent => {
} }
private displayCompareTx = (): React.ReactElement<TTxCompare> => { private displayCompareTx = (): React.ReactElement<TTxCompare> => {
const { nonce, gasLimit, data, value, signedTx, to } = this.state; const { signedTx, nonce } = this.state;
const { gasPrice, chainId } = this.props;
if (!nonce || !signedTx) { if (!nonce || !signedTx) {
throw Error('Can not display raw tx, nonce empty or no signed tx'); throw Error('Can not display raw tx, nonce empty or no signed tx');
} }
const props: TCProps = { return <TxCompare signedTx={signedTx} />;
nonce,
gasPrice,
chainId,
data,
gasLimit,
to,
value,
signedTx
};
return <TxCompare {...props} />;
}; };
private displayDeployModal = (): React.ReactElement<TTxModal> => { private displayDeployModal = (): React.ReactElement<TTxModal> => {
@ -143,7 +131,7 @@ export const deployHOC = PassedComponent => {
props.wallet, props.wallet,
props.nodeLib, props.nodeLib,
props.gasPrice, props.gasPrice,
new Big(gasLimit), Wei(gasLimit),
props.chainId, props.chainId,
transactionInput, transactionInput,
true true
@ -154,7 +142,7 @@ export const deployHOC = PassedComponent => {
const address = await this.props.wallet.getAddressString(); const address = await this.props.wallet.getAddressString();
const nonce = await this.props.nodeLib const nonce = await this.props.nodeLib
.getTransactionCount(address) .getTransactionCount(address)
.then(n => new Big(n).toString()); .then(n => new BN(n).toString());
return this.asyncSetState({ nonce, address }); return this.asyncSetState({ nonce, address });
}; };
} }

View File

@ -1,4 +1,4 @@
import { Wei, Ether } from 'libs/units'; import { Wei } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet'; import { IWallet } from 'libs/wallet/IWallet';
import { RPCNode } from 'libs/nodes'; import { RPCNode } from 'libs/nodes';
import { NodeConfig, NetworkConfig } from 'config/data'; import { NodeConfig, NetworkConfig } from 'config/data';
@ -7,7 +7,7 @@ import { TShowNotification } from 'actions/notifications';
export interface Props { export interface Props {
wallet: IWallet; wallet: IWallet;
balance: Ether; balance: Wei;
node: NodeConfig; node: NodeConfig;
nodeLib: RPCNode; nodeLib: RPCNode;
chainId: NetworkConfig['chainId']; chainId: NetworkConfig['chainId'];

View File

@ -8,6 +8,8 @@ import WalletDecrypt from 'components/WalletDecrypt';
import { TShowNotification } from 'actions/notifications'; import { TShowNotification } from 'actions/notifications';
import classnames from 'classnames'; import classnames from 'classnames';
import { isValidGasPrice, isValidValue } from 'libs/validators'; import { isValidGasPrice, isValidValue } from 'libs/validators';
import { UnitConverter } from 'components/renderCbs';
import { getDecimal } from 'libs/units';
export interface Props { export interface Props {
contractFunctions: any; contractFunctions: any;
@ -164,19 +166,26 @@ export default class InteractExplorer extends Component<Props, State> {
</label> </label>
<label className="InteractExplorer-field form-group"> <label className="InteractExplorer-field form-group">
<h4 className="InteractExplorer-field-label">Value</h4> <h4 className="InteractExplorer-field-label">Value</h4>
<input <UnitConverter
name="value" decimal={getDecimal('ether')}
value={value}
onChange={handleInput('value')} onChange={handleInput('value')}
placeholder="0" >
className={classnames( {({ convertedUnit, onUserInput }) => (
'InteractExplorer-field-input', <input
'form-control', name="value"
{ value={convertedUnit}
'is-invalid': !validValue onChange={onUserInput}
} placeholder="0"
className={classnames(
'InteractExplorer-field-input',
'form-control',
{
'is-invalid': !validValue
}
)}
/>
)} )}
/> </UnitConverter>
</label> </label>
<button <button
className="InteractExplorer-func-submit btn btn-primary" className="InteractExplorer-func-submit btn btn-primary"

View File

@ -9,10 +9,9 @@ import {
TTxModal TTxModal
} from 'containers/Tabs/Contracts/components/TxModal'; } from 'containers/Tabs/Contracts/components/TxModal';
import { IUserSendParams } from 'libs/contracts/ABIFunction'; import { IUserSendParams } from 'libs/contracts/ABIFunction';
import Big from 'bignumber.js'; import BN from 'bn.js';
import { import {
TxCompare, TxCompare,
Props as TCProps,
TTxCompare TTxCompare
} from 'containers/Tabs/Contracts/components/TxCompare'; } from 'containers/Tabs/Contracts/components/TxCompare';
@ -111,26 +110,14 @@ class Interact extends Component<IWithTx, State> {
} }
private makeCompareTx = (): React.ReactElement<TTxCompare> => { private makeCompareTx = (): React.ReactElement<TTxCompare> => {
const { nonce, gasLimit, data, value, to, gasPrice } = this.state.rawTx; const { nonce } = this.state.rawTx;
const { signedTx } = this.state; const { signedTx } = this.state;
const { chainId } = this.props;
if (!nonce || !signedTx) { if (!nonce || !signedTx) {
throw Error('Can not display raw tx, nonce empty or no signed tx'); throw Error('Can not display raw tx, nonce empty or no signed tx');
} }
const props: TCProps = { return <TxCompare signedTx={signedTx} />;
nonce,
gasPrice,
chainId,
data,
gasLimit,
to,
value,
signedTx
};
return <TxCompare {...props} />;
}; };
private makeModal = (): React.ReactElement<TTxModal> => { private makeModal = (): React.ReactElement<TTxModal> => {
@ -182,7 +169,7 @@ class Interact extends Component<IWithTx, State> {
const userInputs: IUserSendParams = { const userInputs: IUserSendParams = {
input: parsedInputs, input: parsedInputs,
to: address, to: address,
gasLimit: new Big(gasLimit), gasLimit: new BN(gasLimit),
value value
}; };
@ -198,8 +185,8 @@ class Interact extends Component<IWithTx, State> {
} }
}; };
private handleInput = name => ev => private handleInput = name => (ev: React.FormEvent<any>) =>
this.setState({ [name]: ev.target.value }); this.setState({ [name]: ev.currentTarget.value });
} }
export default withTx(Interact); export default withTx(Interact);

View File

@ -1,15 +1,9 @@
import React from 'react'; import React from 'react';
import { Wei } from 'libs/units';
import translate from 'translations'; import translate from 'translations';
import { decodeTransaction } from 'libs/transaction';
import EthTx from 'ethereumjs-tx';
import Code from 'components/ui/Code'; import Code from 'components/ui/Code';
export interface Props { export interface Props {
nonce: string;
gasPrice: Wei;
gasLimit: string;
to: string;
value: string;
data: string;
chainId: number;
signedTx: string; signedTx: string;
} }
@ -17,23 +11,18 @@ export const TxCompare = (props: Props) => {
if (!props.signedTx) { if (!props.signedTx) {
return null; return null;
} }
const { signedTx, ...rawTx } = props; const rawTx = decodeTransaction(new EthTx(props.signedTx), false);
const Left = () => ( const Left = () => (
<div className="form-group"> <div className="form-group">
<h4>{translate('SEND_raw')}</h4> <h4>{translate('SEND_raw')}</h4>
<Code> <Code>{JSON.stringify(rawTx, null, 2)}</Code>
{JSON.stringify(
{ ...rawTx, gasPrice: rawTx.gasPrice.toString(16) },
null,
2
)}
</Code>
</div> </div>
); );
const Right = () => ( const Right = () => (
<div className="form-group"> <div className="form-group">
<h4> {translate('SEND_signed')} </h4> <h4> {translate('SEND_signed')} </h4>
<Code>{signedTx}</Code> <Code>{props.signedTx}</Code>
</div> </div>
); );
return ( return (

View File

@ -1,6 +1,6 @@
import * as configSelectors from 'selectors/config'; import * as configSelectors from 'selectors/config';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import { GWei, Wei, Ether } from 'libs/units'; import { toWei, Wei, getDecimal } from 'libs/units';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { showNotification, TShowNotification } from 'actions/notifications'; import { showNotification, TShowNotification } from 'actions/notifications';
import { broadcastTx, TBroadcastTx } from 'actions/wallet'; import { broadcastTx, TBroadcastTx } from 'actions/wallet';
@ -10,7 +10,7 @@ import { NodeConfig, NetworkConfig } from 'config/data';
export interface IWithTx { export interface IWithTx {
wallet: IWallet; wallet: IWallet;
balance: Ether; balance: Wei;
node: NodeConfig; node: NodeConfig;
nodeLib: RPCNode; nodeLib: RPCNode;
chainId: NetworkConfig['chainId']; chainId: NetworkConfig['chainId'];
@ -27,7 +27,10 @@ const mapStateToProps = (state: AppState) => ({
nodeLib: configSelectors.getNodeLib(state), nodeLib: configSelectors.getNodeLib(state),
chainId: configSelectors.getNetworkConfig(state).chainId, chainId: configSelectors.getNetworkConfig(state).chainId,
networkName: configSelectors.getNetworkConfig(state).name, networkName: configSelectors.getNetworkConfig(state).name,
gasPrice: new GWei(configSelectors.getGasPriceGwei(state)).toWei() gasPrice: toWei(
`${configSelectors.getGasPriceGwei(state)}`,
getDecimal('gwei')
)
}); });
export const withTx = passedComponent => export const withTx = passedComponent =>

View File

@ -1,45 +1,54 @@
import React from 'react'; import React from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import UnitDropdown from './UnitDropdown'; import UnitDropdown from './UnitDropdown';
import { Ether } from 'libs/units'; import { Wei } from 'libs/units';
import { UnitConverter } from 'components/renderCbs';
interface Props { interface Props {
value: string; decimal: number;
unit: string; unit: string;
tokens: string[]; tokens: string[];
balance: number | null | Ether; balance: number | null | Wei;
onChange?(value: string, unit: string): void; isReadOnly: boolean;
onAmountChange(value: string, unit: string): void;
onUnitChange(unit: string): void;
} }
export default class AmountField extends React.Component { export default class AmountField extends React.Component {
public props: Props; public props: Props;
get active() {
return !this.props.isReadOnly;
}
public render() { public render() {
const { value, unit, onChange, balance } = this.props; const { unit, balance, decimal, isReadOnly } = this.props;
const isReadonly = !onChange;
return ( return (
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-11"> <div className="col-xs-11">
<label>{translate('SEND_amount')}</label> <label>{translate('SEND_amount')}</label>
<div className="input-group"> <div className="input-group">
<input <UnitConverter decimal={decimal} onChange={this.callWithBaseUnit}>
className={`form-control ${isFinite(Number(value)) && {({ onUserInput, convertedUnit }) => (
Number(value) > 0 <input
? 'is-valid' className={`form-control ${isFinite(Number(convertedUnit)) &&
: 'is-invalid'}`} Number(convertedUnit) > 0
type="text" ? 'is-valid'
placeholder={translateRaw('SEND_amount_short')} : 'is-invalid'}`}
value={value} type="text"
disabled={isReadonly} placeholder={translateRaw('SEND_amount_short')}
onChange={isReadonly ? void 0 : this.onValueChange} value={convertedUnit}
/> disabled={isReadOnly}
onChange={onUserInput}
/>
)}
</UnitConverter>
<UnitDropdown <UnitDropdown
value={unit} value={unit}
options={['ether'].concat(this.props.tokens)} options={['ether'].concat(this.props.tokens)}
onChange={isReadonly ? void 0 : this.onUnitChange} onChange={isReadOnly ? void 0 : this.onUnitChange}
/> />
</div> </div>
{!isReadonly && {!isReadOnly &&
balance && ( balance && (
<span className="help-block"> <span className="help-block">
<a onClick={this.onSendEverything}> <a onClick={this.onSendEverything}>
@ -54,24 +63,12 @@ export default class AmountField extends React.Component {
); );
} }
public onUnitChange = (unit: string) => { public onUnitChange = (unit: string) =>
if (this.props.onChange) { this.active && this.props.onUnitChange(unit); // thsi needs to be converted unit
this.props.onChange(this.props.value, unit);
}
};
public onValueChange = (e: React.SyntheticEvent<HTMLInputElement>) => { public callWithBaseUnit = ({ currentTarget: { value } }) =>
if (this.props.onChange) { this.active && this.props.onAmountChange(value, this.props.unit);
this.props.onChange(
(e.target as HTMLInputElement).value,
this.props.unit
);
}
};
public onSendEverything = () => { public onSendEverything = () =>
if (this.props.onChange) { this.active && this.props.onAmountChange('everything', this.props.unit);
this.props.onChange('everything', this.props.unit);
}
};
} }

View File

@ -17,6 +17,7 @@ import {
} from 'selectors/config'; } from 'selectors/config';
import { getTokens, getTxFromState, MergedToken } from 'selectors/wallet'; import { getTokens, getTxFromState, MergedToken } from 'selectors/wallet';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import { UnitDisplay } from 'components/ui';
import './ConfirmationModal.scss'; import './ConfirmationModal.scss';
interface Props { interface Props {
@ -27,6 +28,7 @@ interface Props {
network: NetworkConfig; network: NetworkConfig;
lang: string; lang: string;
broadCastTxStatus: BroadcastTransactionStatus; broadCastTxStatus: BroadcastTransactionStatus;
decimal: number;
onConfirm(signedTx: string): void; onConfirm(signedTx: string): void;
onClose(): void; onClose(): void;
} }
@ -72,7 +74,8 @@ class ConfirmationModal extends React.Component<Props, State> {
network, network,
onClose, onClose,
broadCastTxStatus, broadCastTxStatus,
transaction transaction,
decimal
} = this.props; } = this.props;
const { timeToRead } = this.state; const { timeToRead } = this.state;
const { toAddress, value, gasPrice, data, from, nonce } = decodeTransaction( const { toAddress, value, gasPrice, data, from, nonce } = decodeTransaction(
@ -109,77 +112,88 @@ class ConfirmationModal extends React.Component<Props, State> {
disableButtons={isBroadcasting} disableButtons={isBroadcasting}
isOpen={true} isOpen={true}
> >
{ <div className="ConfModal">
<div className="ConfModal"> {isBroadcasting ? (
{isBroadcasting ? ( <div className="ConfModal-loading">
<div className="ConfModal-loading"> <Spinner size="5x" />
<Spinner size="5x" /> </div>
</div> ) : (
) : ( <div>
<div> <div className="ConfModal-summary">
<div className="ConfModal-summary"> <div className="ConfModal-summary-icon ConfModal-summary-icon--from">
<div className="ConfModal-summary-icon ConfModal-summary-icon--from"> <Identicon size="100%" address={from} />
<Identicon size="100%" address={from} /> </div>
</div> <div className="ConfModal-summary-amount">
<div className="ConfModal-summary-amount"> <div className="ConfModal-summary-amount-arrow" />
<div className="ConfModal-summary-amount-arrow" /> <div className="ConfModal-summary-amount-currency">
<div className="ConfModal-summary-amount-currency"> <UnitDisplay
{value} {symbol} decimal={decimal}
</div> value={value}
</div> symbol={symbol}
<div className="ConfModal-summary-icon ConfModal-summary-icon--to"> />
<Identicon size="100%" address={toAddress} />
</div> </div>
</div> </div>
<div className="ConfModal-summary-icon ConfModal-summary-icon--to">
<ul className="ConfModal-details"> <Identicon size="100%" address={toAddress} />
<li className="ConfModal-details-detail">
You are sending from <code>{from}</code>
</li>
<li className="ConfModal-details-detail">
You are sending to <code>{toAddress}</code>
</li>
<li className="ConfModal-details-detail">
You are sending with a nonce of <code>{nonce}</code>
</li>
<li className="ConfModal-details-detail">
You are sending{' '}
<strong>
{value} {symbol}
</strong>{' '}
with a gas price of <strong>{gasPrice} gwei</strong>
</li>
<li className="ConfModal-details-detail">
You are interacting with the{' '}
<strong>{node.network}</strong> network provided by{' '}
<strong>{node.service}</strong>
</li>
{!token && (
<li className="ConfModal-details-detail">
{data ? (
<span>
You are sending the following data:{' '}
<textarea
className="form-control"
value={data}
rows={3}
disabled={true}
/>
</span>
) : (
'There is no data attached to this transaction'
)}
</li>
)}
</ul>
<div className="ConfModal-confirm">
{translate('SENDModal_Content_3')}
</div> </div>
</div> </div>
)} <ul className="ConfModal-details">
</div> <li className="ConfModal-details-detail">
} You are sending from <code>{from}</code>
</li>
<li className="ConfModal-details-detail">
You are sending to <code>{toAddress}</code>
</li>
<li className="ConfModal-details-detail">
You are sending with a nonce of <code>{nonce}</code>
</li>
<li className="ConfModal-details-detail">
You are sending{' '}
<strong>
<UnitDisplay
decimal={decimal}
value={value}
symbol={symbol}
/>
</strong>{' '}
with a gas price of{' '}
<strong>
<UnitDisplay
unit={'gwei'}
value={gasPrice}
symbol={'gwei'}
/>
</strong>
</li>
<li className="ConfModal-details-detail">
You are interacting with the <strong>{node.network}</strong>{' '}
network provided by <strong>{node.service}</strong>
</li>
{!token && (
<li className="ConfModal-details-detail">
{data ? (
<span>
You are sending the following data:{' '}
<textarea
className="form-control"
value={data}
rows={3}
disabled={true}
/>
</span>
) : (
'There is no data attached to this transaction'
)}
</li>
)}
</ul>
<div className="ConfModal-confirm">
{translate('SENDModal_Content_3')}
</div>
</div>
)}
</div>
</Modal> </Modal>
</div> </div>
); );

View File

@ -1,4 +1,3 @@
import Big from 'bignumber.js';
// COMPONENTS // COMPONENTS
import Spinner from 'components/ui/Spinner'; import Spinner from 'components/ui/Spinner';
import TabSection from 'containers/TabSection'; import TabSection from 'containers/TabSection';
@ -30,7 +29,7 @@ import {
getBalanceMinusGasCosts, getBalanceMinusGasCosts,
TransactionInput TransactionInput
} from 'libs/transaction'; } from 'libs/transaction';
import { Ether, GWei, UnitKey, Wei } from 'libs/units'; import { UnitKey, Wei, getDecimal, toWei } from 'libs/units';
import { isValidETHAddress } from 'libs/validators'; import { isValidETHAddress } from 'libs/validators';
// LIBS // LIBS
import { IWallet, Web3Wallet } from 'libs/wallet'; import { IWallet, Web3Wallet } from 'libs/wallet';
@ -93,7 +92,7 @@ interface State {
interface Props { interface Props {
wallet: IWallet; wallet: IWallet;
balance: Ether; balance: Wei;
nodeLib: RPCNode; nodeLib: RPCNode;
network: NetworkConfig; network: NetworkConfig;
tokens: MergedToken[]; tokens: MergedToken[];
@ -249,7 +248,6 @@ export class SendTransaction extends React.Component<Props, State> {
const unlocked = !!this.props.wallet; const unlocked = !!this.props.wallet;
const { const {
to, to,
value,
unit, unit,
gasLimit, gasLimit,
data, data,
@ -262,6 +260,10 @@ export class SendTransaction extends React.Component<Props, State> {
} = this.state; } = this.state;
const { offline, forceOffline, balance } = this.props; const { offline, forceOffline, balance } = this.props;
const customMessage = customMessages.find(m => m.to === to); const customMessage = customMessages.find(m => m.to === to);
const decimal =
unit === 'ether'
? getDecimal('ether')
: (this.state.token && this.state.token.decimal) || 0;
const isWeb3Wallet = this.props.wallet instanceof Web3Wallet; const isWeb3Wallet = this.props.wallet instanceof Web3Wallet;
return ( return (
<TabSection> <TabSection>
@ -298,28 +300,30 @@ export class SendTransaction extends React.Component<Props, State> {
onChange={readOnly ? null : this.onAddressChange} onChange={readOnly ? null : this.onAddressChange}
/> />
<AmountField <AmountField
value={value}
unit={unit} unit={unit}
decimal={decimal}
balance={balance} balance={balance}
tokens={this.props.tokenBalances tokens={this.props.tokenBalances
.filter(token => !token.balance.eq(0)) .filter(token => !token.balance.eqn(0))
.map(token => token.symbol) .map(token => token.symbol)
.sort()} .sort()}
onChange={readOnly ? void 0 : this.onAmountChange} onAmountChange={this.onAmountChange}
isReadOnly={readOnly}
onUnitChange={this.onUnitChange}
/> />
<GasField <GasField
value={gasLimit} value={gasLimit}
onChange={readOnly ? void 0 : this.onGasChange} onChange={readOnly ? void 0 : this.onGasChange}
/> />
{(offline || forceOffline) && ( {(offline || forceOffline) && (
<div> <div>
<NonceField <NonceField
value={nonce} value={nonce}
onChange={this.onNonceChange} onChange={this.onNonceChange}
placeholder={'0'} placeholder={'0'}
/> />
</div> </div>
)} )}
{unit === 'ether' && ( {unit === 'ether' && (
<DataField <DataField
value={data} value={data}
@ -433,6 +437,7 @@ export class SendTransaction extends React.Component<Props, State> {
{transaction && {transaction &&
showTxConfirm && ( showTxConfirm && (
<ConfirmationModal <ConfirmationModal
decimal={decimal}
fromAddress={this.state.walletAddress} fromAddress={this.state.walletAddress}
signedTx={transaction.signedTx} signedTx={transaction.signedTx}
onClose={this.hideConfirmTx} onClose={this.hideConfirmTx}
@ -565,12 +570,11 @@ export class SendTransaction extends React.Component<Props, State> {
if (unit === 'ether') { if (unit === 'ether') {
const { balance, gasPrice } = this.props; const { balance, gasPrice } = this.props;
const { gasLimit } = this.state; const { gasLimit } = this.state;
const weiBalance = balance.toWei(); const bigGasLimit = Wei(gasLimit);
const bigGasLimit = new Big(gasLimit);
value = getBalanceMinusGasCosts( value = getBalanceMinusGasCosts(
bigGasLimit, bigGasLimit,
gasPrice, gasPrice,
weiBalance balance
).toString(); ).toString();
} else { } else {
const tokenBalance = this.props.tokenBalances.find( const tokenBalance = this.props.tokenBalances.find(
@ -588,23 +592,29 @@ export class SendTransaction extends React.Component<Props, State> {
if (value === 'everything') { if (value === 'everything') {
value = this.handleEverythingAmountChange(value, unit); value = this.handleEverythingAmountChange(value, unit);
} }
let transaction = this.state.transaction;
let generateDisabled = this.state.generateDisabled;
if (unit && unit !== this.state.unit) {
value = '';
transaction = null;
generateDisabled = true;
}
const token = this.props.tokens.find(x => x.symbol === unit);
this.setState({ this.setState({
value, value,
unit, unit
token,
transaction,
generateDisabled
}); });
}; };
public onUnitChange = (unit: UnitKey) => {
const token = this.props.tokens.find(x => x.symbol === unit);
let stateToSet: any = { token };
if (unit !== this.state.unit) {
stateToSet = {
...stateToSet,
transaction: null,
generateDisabled: true,
unit
};
}
this.setState(stateToSet);
};
public resetJustTx = async (): Promise<any> => public resetJustTx = async (): Promise<any> =>
new Promise(resolve => new Promise(resolve =>
this.setState( this.setState(
@ -628,7 +638,7 @@ export class SendTransaction extends React.Component<Props, State> {
to, to,
data data
}; };
const bigGasLimit = new Big(gasLimit); const bigGasLimit = Wei(gasLimit);
if (!(wallet instanceof Web3Wallet)) { if (!(wallet instanceof Web3Wallet)) {
return; return;
@ -673,7 +683,7 @@ export class SendTransaction extends React.Component<Props, State> {
to, to,
data data
}; };
const bigGasLimit = new Big(gasLimit); const bigGasLimit = Wei(gasLimit);
try { try {
const signedTx = await generateCompleteTransaction( const signedTx = await generateCompleteTransaction(
wallet, wallet,
@ -722,7 +732,7 @@ function mapStateToProps(state: AppState) {
nodeLib: getNodeLib(state), nodeLib: getNodeLib(state),
network: getNetworkConfig(state), network: getNetworkConfig(state),
tokens: getTokens(state), tokens: getTokens(state),
gasPrice: new GWei(getGasPriceGwei(state)).toWei(), gasPrice: toWei(`${getGasPriceGwei(state)}`, getDecimal('gwei')),
transactions: state.wallet.transactions, transactions: state.wallet.transactions,
offline: state.config.offline, offline: state.config.offline,
forceOffline: state.config.forceOffline forceOffline: state.config.forceOffline

View File

@ -18,7 +18,9 @@ function promiseFromChildProcess(command): Promise<any> {
} }
async function privToAddrViaDocker(privKeyWallet: IFullWallet) { async function privToAddrViaDocker(privKeyWallet: IFullWallet) {
const command = `docker run -e key=${privKeyWallet.getPrivateKeyString()} ${dockerImage}:${dockerTag}`; const command = `docker run -e key=${privKeyWallet.getPrivateKeyString()} ${
dockerImage
}:${dockerTag}`;
const dockerOutput = await promiseFromChildProcess(command); const dockerOutput = await promiseFromChildProcess(command);
const newlineStrippedDockerOutput = dockerOutput.replace( const newlineStrippedDockerOutput = dockerOutput.replace(
/(\r\n|\n|\r)/gm, /(\r\n|\n|\r)/gm,

View File

@ -1,6 +1,6 @@
import abi from 'ethereumjs-abi'; import abi from 'ethereumjs-abi';
import { toChecksumAddress } from 'ethereumjs-util'; import { toChecksumAddress } from 'ethereumjs-util';
import Big, { BigNumber } from 'bignumber.js'; import BN from 'bn.js';
import { INode } from 'libs/nodes/INode'; import { INode } from 'libs/nodes/INode';
import { FuncParams, FunctionOutputMappings, Output, Input } from './types'; import { FuncParams, FunctionOutputMappings, Output, Input } from './types';
import { import {
@ -12,7 +12,7 @@ import { ISetConfigForTx } from './index';
export interface IUserSendParams { export interface IUserSendParams {
input; input;
to: string; to: string;
gasLimit: BigNumber; gasLimit: BN;
value: string; value: string;
} }
export type ISendParams = IUserSendParams & ISetConfigForTx; export type ISendParams = IUserSendParams & ISetConfigForTx;
@ -153,18 +153,11 @@ EncodedCall:${data}`);
return valueMapping[type] return valueMapping[type]
? valueMapping[type](value) ? valueMapping[type](value)
: this.isBigNumber(value) ? value.toString() : value; : BN.isBN(value) ? value.toString() : value;
}; };
private parsePreEncodedValue = (_: string, value: any) => private parsePreEncodedValue = (_: string, value: any) =>
this.isBigNumber(value) ? value.toString() : value; BN.isBN(value) ? value.toString() : value;
private isBigNumber = (object: object) =>
object instanceof Big ||
(object &&
object.constructor &&
(object.constructor.name === 'BigNumber' ||
object.constructor.name === 'BN'));
private makeFuncParams = () => private makeFuncParams = () =>
this.inputs.reduce((accumulator, currInput) => { this.inputs.reduce((accumulator, currInput) => {

View File

@ -1,10 +1,10 @@
import { BigNumber } from 'bignumber.js'; import BN from 'bn.js';
import { toChecksumAddress } from 'ethereumjs-util'; import { toChecksumAddress } from 'ethereumjs-util';
import Contract, { ABI } from 'libs/contract'; import Contract, { ABI } from 'libs/contract';
interface Transfer { interface Transfer {
to: string; to: string;
value: string; value: BN;
} }
const erc20Abi: ABI = [ const erc20Abi: ABI = [
@ -59,7 +59,7 @@ class ERC20 extends Contract {
return this.call('balanceOf', [address]); return this.call('balanceOf', [address]);
} }
public transfer(to: string, value: BigNumber): string { public transfer(to: string, value: BN): string {
return this.call('transfer', [to, value.toString()]); return this.call('transfer', [to, value.toString()]);
} }
@ -67,7 +67,7 @@ class ERC20 extends Contract {
const decodedArgs = this.decodeArgs(this.getMethodAbi('transfer'), data); const decodedArgs = this.decodeArgs(this.getMethodAbi('transfer'), data);
return { return {
to: toChecksumAddress(`0x${decodedArgs[0].toString(16)}`), to: toChecksumAddress(`0x${decodedArgs[0].toString(16)}`),
value: decodedArgs[1].toString(10) value: decodedArgs[1]
}; };
} }
} }

View File

@ -1,7 +1,6 @@
import { BigNumber } from 'bignumber.js';
import { Token } from 'config/data'; import { Token } from 'config/data';
import { TransactionWithoutGas } from 'libs/messages'; import { TransactionWithoutGas } from 'libs/messages';
import { Wei } from 'libs/units'; import { Wei, TokenValue } from 'libs/units';
export interface TxObj { export interface TxObj {
to: string; to: string;
@ -9,9 +8,9 @@ export interface TxObj {
} }
export interface INode { export interface INode {
getBalance(address: string): Promise<Wei>; getBalance(address: string): Promise<Wei>;
getTokenBalance(address: string, token: Token): Promise<BigNumber>; getTokenBalance(address: string, token: Token): Promise<TokenValue>;
getTokenBalances(address: string, tokens: Token[]): Promise<BigNumber[]>; getTokenBalances(address: string, tokens: Token[]): Promise<TokenValue[]>;
estimateGas(tx: TransactionWithoutGas): Promise<BigNumber>; estimateGas(tx: TransactionWithoutGas): Promise<Wei>;
getTransactionCount(address: string): Promise<string>; getTransactionCount(address: string): Promise<string>;
sendRawTx(tx: string): Promise<string>; sendRawTx(tx: string): Promise<string>;
sendCallRequest(txObj: TxObj): Promise<string>; sendCallRequest(txObj: TxObj): Promise<string>;

View File

@ -1,7 +1,6 @@
import Big, { BigNumber } from 'bignumber.js';
import { Token } from 'config/data'; import { Token } from 'config/data';
import { TransactionWithoutGas } from 'libs/messages'; import { TransactionWithoutGas } from 'libs/messages';
import { Wei } from 'libs/units'; import { Wei, TokenValue } from 'libs/units';
import { INode, TxObj } from '../INode'; import { INode, TxObj } from '../INode';
import RPCClient from './client'; import RPCClient from './client';
import RPCRequests from './requests'; import RPCRequests from './requests';
@ -30,50 +29,46 @@ export default class RpcNode implements INode {
if (response.error) { if (response.error) {
throw new Error(response.error.message); throw new Error(response.error.message);
} }
return new Wei(String(response.result)); return Wei(response.result);
}); });
} }
public estimateGas(transaction: TransactionWithoutGas): Promise<BigNumber> { public estimateGas(transaction: TransactionWithoutGas): Promise<Wei> {
return this.client return this.client
.call(this.requests.estimateGas(transaction)) .call(this.requests.estimateGas(transaction))
.then(response => { .then(response => {
if (response.error) { if (response.error) {
throw new Error(response.error.message); throw new Error(response.error.message);
} }
return new Big(String(response.result)); return Wei(response.result);
}); });
} }
public getTokenBalance(address: string, token: Token): Promise<BigNumber> { public getTokenBalance(address: string, token: Token): Promise<TokenValue> {
return this.client return this.client
.call(this.requests.getTokenBalance(address, token)) .call(this.requests.getTokenBalance(address, token))
.then(response => { .then(response => {
if (response.error) { if (response.error) {
// TODO - Error handling // TODO - Error handling
return new Big(0); return TokenValue('0');
} }
return new Big(String(response.result)).div( return TokenValue(response.result);
new Big(10).pow(token.decimal)
);
}); });
} }
public getTokenBalances( public getTokenBalances(
address: string, address: string,
tokens: Token[] tokens: Token[]
): Promise<BigNumber[]> { ): Promise<TokenValue[]> {
return this.client return this.client
.batch(tokens.map(t => this.requests.getTokenBalance(address, t))) .batch(tokens.map(t => this.requests.getTokenBalance(address, t)))
.then(response => { .then(response => {
return response.map((item, idx) => { return response.map(item => {
// FIXME wrap in maybe-like // FIXME wrap in maybe-like
if (item.error) { if (item.error) {
return new Big(0); return TokenValue('0');
} }
return new Big(String(item.result)).div( return TokenValue(item.result);
new Big(10).pow(tokens[idx].decimal)
);
}); });
}); });
// TODO - Error handling // TODO - Error handling

View File

@ -1,10 +1,10 @@
// Ref: https://github.com/ethereum/wiki/wiki/JSON-RPC // Ref: https://github.com/ethereum/wiki/wiki/JSON-RPC
import { BigNumber } from 'bignumber.js'; import BN from 'bn.js';
import { toBuffer } from 'ethereumjs-util'; import { toBuffer } from 'ethereumjs-util';
// When encoding QUANTITIES (integers, numbers): encode as hex, prefix with "0x", the most compact representation (slight exception: zero should be represented as "0x0"). // When encoding QUANTITIES (integers, numbers): encode as hex, prefix with "0x", the most compact representation (slight exception: zero should be represented as "0x0").
export function hexEncodeQuantity(value: BigNumber): string { export function hexEncodeQuantity(value: BN): string {
return '0x' + (value.toString(16) || '0'); return '0x' + (value.toString(16) || '0');
} }

View File

@ -1,7 +1,6 @@
import Big, { BigNumber } from 'bignumber.js';
import { Token } from 'config/data'; import { Token } from 'config/data';
import { TransactionWithoutGas } from 'libs/messages'; import { TransactionWithoutGas } from 'libs/messages';
import { Wei } from 'libs/units'; import { Wei, TokenValue } from 'libs/units';
import { INode, TxObj } from '../INode'; import { INode, TxObj } from '../INode';
import ERC20 from 'libs/erc20'; import ERC20 from 'libs/erc20';
@ -18,6 +17,7 @@ export default class Web3Node implements INode {
if (err) { if (err) {
return reject(err.message); return reject(err.message);
} }
// web3 return string
resolve(res); resolve(res);
}); });
}); });
@ -29,12 +29,13 @@ export default class Web3Node implements INode {
if (err) { if (err) {
return reject(err); return reject(err);
} }
resolve(new Wei(res.toString())); // web3 returns BigNumber
resolve(Wei(res.toString()));
}); });
}); });
} }
public estimateGas(transaction: TransactionWithoutGas): Promise<BigNumber> { public estimateGas(transaction: TransactionWithoutGas): Promise<Wei> {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
this.web3.eth.estimateGas( this.web3.eth.estimateGas(
{ {
@ -45,13 +46,14 @@ export default class Web3Node implements INode {
if (err) { if (err) {
return reject(err); return reject(err);
} }
resolve(new Big(res.toString())); // web3 returns number
resolve(Wei(res));
} }
) )
); );
} }
public getTokenBalance(address: string, token: Token): Promise<BigNumber> { public getTokenBalance(address: string, token: Token): Promise<TokenValue> {
return new Promise(resolve => { return new Promise(resolve => {
this.web3.eth.call( this.web3.eth.call(
{ {
@ -62,13 +64,10 @@ export default class Web3Node implements INode {
(err, res) => { (err, res) => {
if (err) { if (err) {
// TODO - Error handling // TODO - Error handling
return resolve(new Big(0)); return resolve(TokenValue('0'));
} }
// web3 returns string
const bigResult = new Big(res.toString()); resolve(TokenValue(res));
const bigTokenBase = new Big(10).pow(token.decimal);
resolve(bigResult.div(bigTokenBase));
} }
); );
}); });
@ -77,11 +76,11 @@ export default class Web3Node implements INode {
public getTokenBalances( public getTokenBalances(
address: string, address: string,
tokens: Token[] tokens: Token[]
): Promise<BigNumber[]> { ): Promise<TokenValue[]> {
return new Promise(resolve => { return new Promise(resolve => {
const batch = this.web3.createBatch(); const batch = this.web3.createBatch();
const totalCount = tokens.length; const totalCount = tokens.length;
const returnArr = new Array<BigNumber>(totalCount); const returnArr = new Array<TokenValue>(totalCount);
let finishCount = 0; let finishCount = 0;
tokens.forEach((token, index) => tokens.forEach((token, index) =>
@ -92,20 +91,19 @@ export default class Web3Node implements INode {
data: ERC20.balanceOf(address) data: ERC20.balanceOf(address)
}, },
'pending', 'pending',
(err, res) => finish(token, index, err, res) (err, res) => finish(index, err, res)
) )
) )
); );
batch.execute(); batch.execute();
function finish(token, index, err, res) { function finish(index, err, res) {
if (err) { if (err) {
// TODO - Error handling // TODO - Error handling
returnArr[index] = new Big(0); returnArr[index] = TokenValue('0');
} else { } else {
returnArr[index] = new Big(res.toString()).div( // web3 returns string
new Big(10).pow(token.decimal) returnArr[index] = TokenValue(res);
);
} }
finishCount++; finishCount++;
@ -122,6 +120,7 @@ export default class Web3Node implements INode {
if (err) { if (err) {
return reject(err); return reject(err);
} }
// web3 returns number
resolve(txCount.toString()); resolve(txCount.toString());
}) })
); );
@ -133,6 +132,7 @@ export default class Web3Node implements INode {
if (err) { if (err) {
return reject(err); return reject(err);
} }
// web3 return string
resolve(txHash); resolve(txHash);
}) })
); );

View File

@ -5,19 +5,11 @@ import ERC20 from 'libs/erc20';
import { TransactionWithoutGas } from 'libs/messages'; import { TransactionWithoutGas } from 'libs/messages';
import { RPCNode } from 'libs/nodes'; import { RPCNode } from 'libs/nodes';
import { INode } from 'libs/nodes/INode'; import { INode } from 'libs/nodes/INode';
import { import { UnitKey, Wei, TokenValue, toTokenBase } from 'libs/units';
Ether,
toTokenUnit,
UnitKey,
Wei,
toTokenDisplay,
toUnit
} from 'libs/units';
import { isValidETHAddress } from 'libs/validators'; import { isValidETHAddress } from 'libs/validators';
import { stripHexPrefixAndLower, valueToHex, sanitizeHex } from 'libs/values'; import { stripHexPrefixAndLower, sanitizeHex, toHexWei } from 'libs/values';
import { IWallet, Web3Wallet } from 'libs/wallet'; import { IWallet, Web3Wallet } from 'libs/wallet';
import { translateRaw } from 'translations'; import { translateRaw } from 'translations';
import Big, { BigNumber } from 'bignumber.js';
export interface TransactionInput { export interface TransactionInput {
token?: Token | null; token?: Token | null;
@ -37,7 +29,7 @@ export interface BaseTransaction {
to: string; to: string;
value: string; value: string;
data: string; data: string;
gasLimit: BigNumber | string; gasLimit: Wei | string;
gasPrice: Wei | string; gasPrice: Wei | string;
chainId: number; chainId: number;
} }
@ -82,12 +74,12 @@ export function getTransactionFields(tx: EthTx) {
function getValue( function getValue(
token: Token | null | undefined, token: Token | null | undefined,
tx: ExtendedRawTransaction tx: ExtendedRawTransaction
): BigNumber { ): Wei {
let value; let value;
if (token) { if (token) {
value = new Big(ERC20.$transfer(tx.data).value); value = Wei(ERC20.$transfer(tx.data).value);
} else { } else {
value = new Big(tx.value); value = Wei(tx.value);
} }
return value; return value;
} }
@ -99,11 +91,14 @@ async function getBalance(
) { ) {
const { from } = tx; const { from } = tx;
const ETHBalance = await node.getBalance(from); const ETHBalance = await node.getBalance(from);
let balance; let balance: Wei;
if (token) { if (token) {
balance = toTokenUnit(await node.getTokenBalance(tx.from, token), token); balance = toTokenBase(
await node.getTokenBalance(tx.from, token).toString(),
token.decimal
);
} else { } else {
balance = ETHBalance.amount; balance = ETHBalance;
} }
return { return {
balance, balance,
@ -115,7 +110,7 @@ async function balanceCheck(
node: INode, node: INode,
tx: ExtendedRawTransaction, tx: ExtendedRawTransaction,
token: Token | null | undefined, token: Token | null | undefined,
value: BigNumber, value: Wei,
gasCost: Wei gasCost: Wei
) { ) {
// Ensure their balance exceeds the amount they're sending // Ensure their balance exceeds the amount they're sending
@ -125,9 +120,9 @@ async function balanceCheck(
} }
// ensure gas cost is not greaterThan current eth balance // ensure gas cost is not greaterThan current eth balance
// TODO check that eth balance is not lesser than txAmount + gasCost // TODO check that eth balance is not lesser than txAmount + gasCost
if (gasCost.amount.gt(ETHBalance.amount)) { if (gasCost.gt(ETHBalance)) {
throw new Error( throw new Error(
`gasCost: ${gasCost.amount} greaterThan ETHBalance: ${ETHBalance.amount}` `gasCost: ${gasCost.toString()} greaterThan ETHBalance: ${ETHBalance.toString()}`
); );
} }
} }
@ -136,7 +131,7 @@ function generateTxValidation(
to: string, to: string,
token: Token | null | undefined, token: Token | null | undefined,
data: string, data: string,
gasLimit: BigNumber | string, gasLimit: Wei | string,
gasPrice: Wei | string, gasPrice: Wei | string,
skipEthAddressValidation: boolean skipEthAddressValidation: boolean
) { ) {
@ -154,16 +149,16 @@ function generateTxValidation(
// Reject gas limit under 21000 (Minimum for transaction) // Reject gas limit under 21000 (Minimum for transaction)
// Reject if limit over 5000000 // Reject if limit over 5000000
// TODO: Make this dynamic, the limit shifts // TODO: Make this dynamic, the limit shifts
if (gasLimit.lessThan(21000)) { if (gasLimit.ltn(21000)) {
throw new Error('Gas limit must be at least 21000 for transactions'); throw new Error('Gas limit must be at least 21000 for transactions');
} }
// Reject gasLimit over 5000000gwei // Reject gasLimit over 5000000gwei
if (gasLimit.greaterThan(5000000)) { if (gasLimit.gtn(5000000)) {
throw new Error(translateRaw('GETH_GasLimit')); throw new Error(translateRaw('GETH_GasLimit'));
} }
// Reject gasPrice over 1000gwei (1000000000000) // Reject gasPrice over 1000gwei (1000000000000)
const gwei = new Big('1000000000000'); const gwei = Wei('1000000000000');
if (gasPrice.amount.greaterThan(gwei)) { if (gasPrice.gt(gwei)) {
throw new Error( throw new Error(
'Gas price too high. Please contact support if this was not a mistake.' 'Gas price too high. Please contact support if this was not a mistake.'
); );
@ -186,7 +181,7 @@ export async function generateCompleteTransactionFromRawTransaction(
throw Error('Gas Limit and Gas Price should be of type bignumber'); throw Error('Gas Limit and Gas Price should be of type bignumber');
} }
// computed gas cost (gasprice * gaslimit) // computed gas cost (gasprice * gaslimit)
const gasCost: Wei = new Wei(gasPrice.amount.times(gasLimit)); const gasCost: Wei = Wei(gasPrice.mul(gasLimit));
// get amount value (either in ETH or in Token) // get amount value (either in ETH or in Token)
const value = getValue(token, tx); const value = getValue(token, tx);
// if not offline, ensure that balance exceeds costs // if not offline, ensure that balance exceeds costs
@ -225,14 +220,14 @@ export async function formatTxInput(
return { return {
to, to,
from: await wallet.getAddressString(), from: await wallet.getAddressString(),
value: valueToHex(new Ether(value)), value: toHexWei(value), //turn users ether to wei
data data
}; };
} else { } else {
if (!token) { if (!token) {
throw new Error('No matching token'); throw new Error('No matching token');
} }
const bigAmount = new Big(value); const bigAmount = TokenValue(value);
const ERC20Data = ERC20.transfer(to, bigAmount); const ERC20Data = ERC20.transfer(to, bigAmount);
return { return {
to: token.address, to: token.address,
@ -247,7 +242,7 @@ export async function confirmAndSendWeb3Transaction(
wallet: Web3Wallet, wallet: Web3Wallet,
nodeLib: RPCNode, nodeLib: RPCNode,
gasPrice: Wei, gasPrice: Wei,
gasLimit: BigNumber, gasLimit: Wei,
chainId: number, chainId: number,
transactionInput: TransactionInput transactionInput: TransactionInput
): Promise<string> { ): Promise<string> {
@ -273,7 +268,7 @@ export async function generateCompleteTransaction(
wallet: IWallet, wallet: IWallet,
nodeLib: RPCNode, nodeLib: RPCNode,
gasPrice: Wei, gasPrice: Wei,
gasLimit: BigNumber, gasLimit: Wei,
chainId: number, chainId: number,
transactionInput: TransactionInput, transactionInput: TransactionInput,
skipValidation: boolean, skipValidation: boolean,
@ -307,34 +302,34 @@ export async function generateCompleteTransaction(
// TODO determine best place for helper function // TODO determine best place for helper function
export function getBalanceMinusGasCosts( export function getBalanceMinusGasCosts(
gasLimit: BigNumber, gasLimit: Wei,
gasPrice: Wei, gasPrice: Wei,
balance: Wei balance: Wei
): Ether { ): Wei {
const weiGasCosts = gasPrice.amount.times(gasLimit); const weiGasCosts = gasPrice.mul(gasLimit);
const weiBalanceMinusGasCosts = balance.amount.minus(weiGasCosts); const weiBalanceMinusGasCosts = balance.sub(weiGasCosts);
return new Ether(weiBalanceMinusGasCosts); return Wei(weiBalanceMinusGasCosts);
} }
export function decodeTransaction(transaction: EthTx, token: Token | false) { export function decodeTransaction(transaction: EthTx, token: Token | false) {
const { to, value, data, gasPrice, nonce, from } = getTransactionFields( const { to, value, data, gasPrice, nonce, from } = getTransactionFields(
transaction transaction
); );
let fixedValue; let fixedValue: TokenValue;
let toAddress; let toAddress;
if (token) { if (token) {
const tokenData = ERC20.$transfer(data); const tokenData = ERC20.$transfer(data);
fixedValue = toTokenDisplay(new Big(tokenData.value), token).toString(); fixedValue = tokenData.value;
toAddress = tokenData.to; toAddress = tokenData.to;
} else { } else {
fixedValue = toUnit(new Big(value, 16), 'wei', 'ether').toString(); fixedValue = Wei(value);
toAddress = to; toAddress = to;
} }
return { return {
value: fixedValue, value: fixedValue,
gasPrice: toUnit(new Big(gasPrice, 16), 'wei', 'gwei').toString(), gasPrice: Wei(gasPrice),
data, data,
toAddress, toAddress,
nonce, nonce,

View File

@ -0,0 +1,312 @@
import { Token } from 'config/data';
import EthTx from 'ethereumjs-tx';
import { addHexPrefix, padToEven, toChecksumAddress } from 'ethereumjs-util';
import ERC20 from 'libs/erc20';
import { TransactionWithoutGas } from 'libs/messages';
import { RPCNode } from 'libs/nodes';
import { INode } from 'libs/nodes/INode';
import { UnitKey, Wei, TokenValue, toTokenBase } from 'libs/units';
import { isValidETHAddress } from 'libs/validators';
import { stripHexPrefixAndLower, toHexWei, sanitizeHex } from 'libs/values';
import { IWallet } from 'libs/wallet';
import { translateRaw } from 'translations';
export interface TransactionInput {
token?: Token | null;
unit: UnitKey;
value: string;
to: string;
data: string;
}
export interface BroadcastTransactionStatus {
isBroadcasting: boolean;
signedTx: string;
successfullyBroadcast: boolean;
}
export interface BaseTransaction {
to: string;
value: string;
data: string;
gasLimit: Wei | string;
gasPrice: Wei | string;
chainId: number;
}
export interface RawTransaction extends BaseTransaction {
nonce: string;
}
export interface ExtendedRawTransaction extends RawTransaction {
// non-standard, legacy
from: string;
}
export interface CompleteTransaction extends RawTransaction {
rawTx: string;
signedTx: string;
}
// Get useable fields from an EthTx object.
export function getTransactionFields(tx: EthTx) {
// For some crazy reason, toJSON spits out an array, not keyed values.
const [nonce, gasPrice, gasLimit, to, value, data, v, r, s] = tx.toJSON();
return {
// No value comes back as '0x', but most things expect '0x00'
value: value === '0x' ? '0x00' : value,
// If data is 0x, it might as well not be there
data: data === '0x' ? null : data,
// To address is unchecksummed, which could cause mismatches in comparisons
to: toChecksumAddress(to),
from: sanitizeHex(tx.getSenderAddress().toString('hex')),
// Everything else is as-is
nonce,
gasPrice,
gasLimit,
v,
r,
s
};
}
function getValue(
token: Token | null | undefined,
tx: ExtendedRawTransaction
): Wei {
let value;
if (token) {
value = Wei(ERC20.$transfer(tx.data).value);
} else {
value = Wei(tx.value);
}
return value;
}
async function getBalance(
node: INode,
tx: ExtendedRawTransaction,
token: Token | null | undefined
) {
const { from } = tx;
const ETHBalance = await node.getBalance(from);
let balance: Wei;
if (token) {
balance = toTokenBase(
await node.getTokenBalance(tx.from, token).toString(),
token.decimal
);
} else {
balance = ETHBalance;
}
return {
balance,
ETHBalance
};
}
async function balanceCheck(
node: INode,
tx: ExtendedRawTransaction,
token: Token | null | undefined,
value: Wei,
gasCost: Wei
) {
// Ensure their balance exceeds the amount they're sending
const { balance, ETHBalance } = await getBalance(node, tx, token);
if (value.gt(balance)) {
throw new Error(translateRaw('GETH_Balance'));
}
// ensure gas cost is not greaterThan current eth balance
// TODO check that eth balance is not lesser than txAmount + gasCost
if (gasCost.gt(ETHBalance)) {
throw new Error(
`gasCost: ${gasCost.toString()} greaterThan ETHBalance: ${ETHBalance.toString()}`
);
}
}
function generateTxValidation(
to: string,
token: Token | null | undefined,
data: string,
gasLimit: Wei | string,
gasPrice: Wei | string,
skipEthAddressValidation: boolean
) {
// Reject bad addresses
if (!isValidETHAddress(to) && !skipEthAddressValidation) {
throw new Error(translateRaw('ERROR_5'));
}
// Reject token transactions without data
if (token && !data) {
throw new Error('Tokens must be sent with data');
}
if (typeof gasLimit === 'string' || typeof gasPrice === 'string') {
throw Error('Gas Limit and Gas Price should be of type bignumber');
}
// Reject gas limit under 21000 (Minimum for transaction)
// Reject if limit over 5000000
// TODO: Make this dynamic, the limit shifts
if (gasLimit.ltn(21000)) {
throw new Error('Gas limit must be at least 21000 for transactions');
}
// Reject gasLimit over 5000000gwei
if (gasLimit.gtn(5000000)) {
throw new Error(translateRaw('GETH_GasLimit'));
}
// Reject gasPrice over 1000gwei (1000000000000)
const gwei = Wei('1000000000000');
if (gasPrice.gt(gwei)) {
throw new Error(
'Gas price too high. Please contact support if this was not a mistake.'
);
}
}
export async function generateCompleteTransactionFromRawTransaction(
node: INode,
tx: ExtendedRawTransaction,
wallet: IWallet,
token: Token | null | undefined,
skipValidation: boolean,
offline?: boolean
): Promise<CompleteTransaction> {
const { to, data, gasLimit, gasPrice, chainId, nonce } = tx;
// validation
generateTxValidation(to, token, data, gasLimit, gasPrice, skipValidation);
// duplicated from generateTxValidation -- typescript bug
if (typeof gasLimit === 'string' || typeof gasPrice === 'string') {
throw Error('Gas Limit and Gas Price should be of type bignumber');
}
// computed gas cost (gasprice * gaslimit)
const gasCost: Wei = Wei(gasPrice.mul(gasLimit));
// get amount value (either in ETH or in Token)
const value = getValue(token, tx);
// if not offline, ensure that balance exceeds costs
if (!offline) {
await balanceCheck(node, tx, token, value, gasCost);
}
// Taken from v3's `sanitizeHex`, ensures that the value is a %2 === 0
// prefix'd hex value.
const cleanHex = hex => addHexPrefix(padToEven(stripHexPrefixAndLower(hex)));
const cleanedRawTx = {
nonce: cleanHex(nonce),
gasPrice: cleanHex(gasPrice.toString(16)),
gasLimit: cleanHex(gasLimit.toString(16)),
to: toChecksumAddress(cleanHex(to)),
value: token ? '0x00' : cleanHex(value.toString(16)),
data: data ? cleanHex(data) : '',
chainId: chainId || 1
};
// Sign the transaction
const rawTxJson = JSON.stringify(cleanedRawTx);
const signedTx = await wallet.signRawTransaction(cleanedRawTx);
return {
...cleanedRawTx,
rawTx: rawTxJson,
signedTx
};
}
export async function formatTxInput(
wallet: IWallet,
{ token, unit, value, to, data }: TransactionInput
): Promise<TransactionWithoutGas> {
if (unit === 'ether') {
return {
to,
from: await wallet.getAddressString(),
value: toHexWei(value), //turn users ether to wei
data
};
} else {
if (!token) {
throw new Error('No matching token');
}
const bigAmount = TokenValue(value);
const ERC20Data = ERC20.transfer(to, bigAmount);
return {
to: token.address,
from: await wallet.getAddressString(),
value: '0x0',
data: ERC20Data
};
}
}
export async function generateCompleteTransaction(
wallet: IWallet,
nodeLib: RPCNode,
gasPrice: Wei,
gasLimit: Wei,
chainId: number,
transactionInput: TransactionInput,
skipValidation: boolean,
nonce?: number | null,
offline?: boolean
): Promise<CompleteTransaction> {
const { token } = transactionInput;
const { from, to, value, data } = await formatTxInput(
wallet,
transactionInput
);
const transaction: ExtendedRawTransaction = {
nonce: nonce ? `0x${nonce}` : await nodeLib.getTransactionCount(from),
from,
to,
gasLimit,
value,
data,
chainId,
gasPrice
};
return await generateCompleteTransactionFromRawTransaction(
nodeLib,
transaction,
wallet,
token,
skipValidation,
offline
);
}
// TODO determine best place for helper function
export function getBalanceMinusGasCosts(
gasLimit: Wei,
gasPrice: Wei,
balance: Wei
): Wei {
const weiGasCosts = gasPrice.mul(gasLimit);
const weiBalanceMinusGasCosts = balance.sub(weiGasCosts);
return Wei(weiBalanceMinusGasCosts);
}
export function decodeTransaction(transaction: EthTx, token: Token | false) {
const { to, value, data, gasPrice, nonce, from } = getTransactionFields(
transaction
);
let fixedValue: TokenValue;
let toAddress;
if (token) {
const tokenData = ERC20.$transfer(data);
fixedValue = tokenData.value;
toAddress = tokenData.to;
} else {
fixedValue = Wei(value);
toAddress = to;
}
return {
value: fixedValue,
gasPrice: Wei(gasPrice),
data,
toAddress,
nonce,
from
};
}

View File

@ -1,5 +1,9 @@
import Big, { BigNumber } from 'bignumber.js'; import BN from 'bn.js';
import { Token } from 'config/data'; import { stripHexPrefix } from 'libs/values';
type UnitKey = keyof typeof Units;
type Wei = BN;
type TokenValue = BN;
const Units = { const Units = {
wei: '1', wei: '1',
@ -27,79 +31,74 @@ const Units = {
gether: '1000000000000000000000000000', gether: '1000000000000000000000000000',
tether: '1000000000000000000000000000000' tether: '1000000000000000000000000000000'
}; };
const handleValues = (input: string | BN) => {
export type TUnit = typeof Units; if (typeof input === 'string') {
export type UnitKey = keyof TUnit; return input.startsWith('0x')
? new BN(stripHexPrefix(input), 16)
class Unit { : new BN(input);
public unit: UnitKey;
public amount: BigNumber;
constructor(amount: BigNumber, unit: UnitKey) {
this.unit = unit;
this.amount = amount;
} }
if (typeof input === 'number') {
public toString(base?: number) { return new BN(input);
return this.amount.toString(base);
} }
if (BN.isBN(input)) {
public toPrecision(precision?: number) { return input;
return this.amount.toPrecision(precision);
} }
throw Error('unsupported value conversion');
};
public toWei(): Wei { const Wei = (input: string | BN): Wei => handleValues(input);
return new Wei(toWei(this.amount, this.unit));
const TokenValue = (input: string | BN) => handleValues(input);
const getDecimal = (key: UnitKey) => Units[key].length - 1;
const stripRightZeros = (str: string) => {
const strippedStr = str.replace(/0+$/, '');
return strippedStr === '' ? null : strippedStr;
};
const baseToConvertedUnit = (value: string, decimal: number) => {
if (decimal === 0) {
return value;
} }
const paddedValue = value.padStart(decimal + 1, '0'); //0.1 ==>
const integerPart = paddedValue.slice(0, -decimal);
const fractionPart = stripRightZeros(paddedValue.slice(-decimal));
return fractionPart ? `${integerPart}.${fractionPart}` : `${integerPart}`;
};
public toGWei(): GWei { const convertedToBaseUnit = (value: string, decimal: number) => {
return new GWei(toUnit(this.amount, this.unit, 'gwei')); if (decimal === 0) {
return value;
} }
const [integerPart, fractionPart = ''] = value.split('.');
const paddedFraction = fractionPart.padEnd(decimal, '0');
return `${integerPart}${paddedFraction}`;
};
public toEther(): Ether { const fromWei = (wei: Wei, unit: UnitKey) => {
return new Ether(toUnit(this.amount, this.unit, 'ether')); const decimal = getDecimal(unit);
} return baseToConvertedUnit(wei.toString(), decimal);
} };
// tslint:disable:max-classes-per-file const toWei = (value: string, decimal: number): Wei => {
export class Ether extends Unit { const wei = convertedToBaseUnit(value, decimal);
constructor(amount: BigNumber | number | string) { return Wei(wei);
super(new Big(amount), 'ether'); };
}
}
export class Wei extends Unit { const fromTokenBase = (value: TokenValue, decimal: number) =>
constructor(amount: BigNumber | number | string) { baseToConvertedUnit(value.toString(), decimal);
super(new Big(amount), 'wei');
}
}
export class GWei extends Unit { const toTokenBase = (value: string, decimal: number) =>
constructor(amount: BigNumber | number | string) { TokenValue(convertedToBaseUnit(value, decimal));
super(new Big(amount), 'gwei');
}
}
function getValueOfUnit(unit: UnitKey) { export {
return new Big(Units[unit]); TokenValue,
} fromWei,
toWei,
export function toWei(num: BigNumber, unit: UnitKey): BigNumber { toTokenBase,
return num.times(getValueOfUnit(unit)); fromTokenBase,
} Wei,
getDecimal,
export function toUnit( UnitKey
num: BigNumber, };
fromUnit: UnitKey,
convertToUnit: UnitKey
): BigNumber {
return toWei(num, fromUnit).div(getValueOfUnit(convertToUnit));
}
export function toTokenUnit(num: BigNumber, token: Token): BigNumber {
return num.times(new Big(10).pow(token.decimal));
}
export function toTokenDisplay(num: BigNumber, token: Token): BigNumber {
return num.times(new Big(10).pow(-token.decimal));
}

View File

@ -1,5 +1,4 @@
import { Ether } from 'libs/units'; import { Wei } from 'libs/units';
export function stripHexPrefix(value: string) { export function stripHexPrefix(value: string) {
return value.replace('0x', ''); return value.replace('0x', '');
} }
@ -8,11 +7,8 @@ export function stripHexPrefixAndLower(value: string): string {
return stripHexPrefix(value).toLowerCase(); return stripHexPrefix(value).toLowerCase();
} }
export function valueToHex(value: Ether): string { export function toHexWei(weiString: string): string {
// Values are in ether, so convert to wei for RPC calls return `0x${Wei(weiString).toString(16)}`;
const wei = value.toWei();
// Finally, hex it up!
return `0x${wei.toString(16)}`;
} }
export function padLeftEven(hex: string) { export function padLeftEven(hex: string) {

View File

@ -1,4 +1,4 @@
import Big from 'bignumber.js'; import BN from 'bn.js';
import EthTx from 'ethereumjs-tx'; import EthTx from 'ethereumjs-tx';
import { addHexPrefix } from 'ethereumjs-util'; import { addHexPrefix } from 'ethereumjs-util';
import { RawTransaction } from 'libs/transaction'; import { RawTransaction } from 'libs/transaction';
@ -6,6 +6,7 @@ import { stripHexPrefixAndLower } from 'libs/values';
import TrezorConnect from 'vendor/trezor-connect'; import TrezorConnect from 'vendor/trezor-connect';
import { DeterministicWallet } from './deterministic'; import { DeterministicWallet } from './deterministic';
import { IWallet } from '../IWallet'; import { IWallet } from '../IWallet';
export class TrezorWallet extends DeterministicWallet implements IWallet { export class TrezorWallet extends DeterministicWallet implements IWallet {
public signRawTransaction(tx: RawTransaction): Promise<string> { public signRawTransaction(tx: RawTransaction): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -30,7 +31,7 @@ export class TrezorWallet extends DeterministicWallet implements IWallet {
// https://github.com/kvhnuke/etherwallet/blob/v3.10.2.6/app/scripts/uiFuncs.js#L24 // https://github.com/kvhnuke/etherwallet/blob/v3.10.2.6/app/scripts/uiFuncs.js#L24
const txToSerialize = { const txToSerialize = {
...tx, ...tx,
v: addHexPrefix(new Big(result.v).toString(16)), v: addHexPrefix(new BN(result.v).toString(16)),
r: addHexPrefix(result.r), r: addHexPrefix(result.r),
s: addHexPrefix(result.s) s: addHexPrefix(result.s)
}; };

View File

@ -42,7 +42,10 @@ export function deterministicWallets(
} }
} }
function updateWalletValues(wallets, newWallet) { function updateWalletValues(
wallets: DeterministicWalletData[],
newWallet: Partial<DeterministicWalletData>
) {
return wallets.map(w => { return wallets.map(w => {
if (w.address === newWallet.address) { if (w.address === newWallet.address) {
return { return {

View File

@ -5,18 +5,17 @@ import {
WalletAction, WalletAction,
TypeKeys TypeKeys
} from 'actions/wallet'; } from 'actions/wallet';
import { BigNumber } from 'bignumber.js'; import { Wei, TokenValue } from 'libs/units';
import { BroadcastTransactionStatus } from 'libs/transaction'; import { BroadcastTransactionStatus } from 'libs/transaction';
import { Ether } from 'libs/units';
import { IWallet } from 'libs/wallet'; import { IWallet } from 'libs/wallet';
import { getTxFromBroadcastTransactionStatus } from 'selectors/wallet'; import { getTxFromBroadcastTransactionStatus } from 'selectors/wallet';
export interface State { export interface State {
inst?: IWallet | null; inst?: IWallet | null;
// in ETH // in ETH
balance?: Ether | null; balance?: Wei | null;
tokens: { tokens: {
[key: string]: BigNumber; [key: string]: TokenValue;
}; };
transactions: BroadcastTransactionStatus[]; transactions: BroadcastTransactionStatus[];
} }
@ -33,8 +32,8 @@ function setWallet(state: State, action: SetWalletAction): State {
} }
function setBalance(state: State, action: SetBalanceAction): State { function setBalance(state: State, action: SetBalanceAction): State {
const ethBalance = action.payload.toEther(); const weiBalance = action.payload;
return { ...state, balance: ethBalance }; return { ...state, balance: weiBalance };
} }
function setTokenBalances(state: State, action: SetTokenBalancesAction): State { function setTokenBalances(state: State, action: SetTokenBalancesAction): State {

View File

@ -20,7 +20,10 @@ import {
changeNode, changeNode,
changeNodeIntent changeNodeIntent
} from 'actions/config'; } from 'actions/config';
import { State as ConfigState, INITIAL_STATE as configInitialState } from 'reducers/config'; import {
State as ConfigState,
INITIAL_STATE as configInitialState
} from 'reducers/config';
export const getConfig = (state: AppState): ConfigState => state.config; export const getConfig = (state: AppState): ConfigState => state.config;
@ -43,7 +46,6 @@ function* handlePollOfflineStatus(): SagaIterator {
yield cancel(pollOfflineStatusTask); yield cancel(pollOfflineStatusTask);
} }
// @HACK For now we reload the app when doing a language swap to force non-connected // @HACK For now we reload the app when doing a language swap to force non-connected
// data to reload. Also the use of timeout to avoid using additional actions for now. // data to reload. Also the use of timeout to avoid using additional actions for now.
function* reload(): SagaIterator { function* reload(): SagaIterator {

View File

@ -23,6 +23,7 @@ import { getNodeLib } from 'selectors/config';
import { getDesiredToken, getWallets } from 'selectors/deterministicWallets'; import { getDesiredToken, getWallets } from 'selectors/deterministicWallets';
import { getTokens } from 'selectors/wallet'; import { getTokens } from 'selectors/wallet';
import translate from 'translations'; import translate from 'translations';
import { TokenValue } from 'libs/units';
function* getDeterministicWallets( function* getDeterministicWallets(
action: GetDeterministicWalletsAction action: GetDeterministicWalletsAction
@ -105,7 +106,7 @@ function* updateWalletTokenValues(): SagaIterator {
const calls = wallets.map(w => { const calls = wallets.map(w => {
return apply(node, node.getTokenBalance, [w.address, token]); return apply(node, node.getTokenBalance, [w.address, token]);
}); });
const tokenBalances = yield all(calls); const tokenBalances: TokenValue[] = yield all(calls);
for (let i = 0; i < wallets.length; i++) { for (let i = 0; i < wallets.length; i++) {
yield put( yield put(
@ -113,7 +114,7 @@ function* updateWalletTokenValues(): SagaIterator {
...wallets[i], ...wallets[i],
tokenValues: { tokenValues: {
...wallets[i].tokenValues, ...wallets[i].tokenValues,
[desiredToken]: tokenBalances[i] [desiredToken]: { value: tokenBalances[i], decimal: token.decimal }
} }
}) })
); );

View File

@ -10,10 +10,10 @@ import {
UnlockMnemonicAction, UnlockMnemonicAction,
UnlockPrivateKeyAction UnlockPrivateKeyAction
} from 'actions/wallet'; } from 'actions/wallet';
import { Wei } from 'libs/units';
import { changeNodeIntent } from 'actions/config'; import { changeNodeIntent } from 'actions/config';
import TransactionSucceeded from 'components/ExtendedNotifications/TransactionSucceeded'; import TransactionSucceeded from 'components/ExtendedNotifications/TransactionSucceeded';
import { INode } from 'libs/nodes/INode'; import { INode } from 'libs/nodes/INode';
import { Wei } from 'libs/units';
import { import {
IWallet, IWallet,
MnemonicWallet, MnemonicWallet,

View File

@ -1,4 +1,4 @@
import Big, { BigNumber } from 'bignumber.js'; import { TokenValue } from 'libs/units';
import { Token } from 'config/data'; import { Token } from 'config/data';
import { BroadcastTransactionStatus } from 'libs/transaction'; import { BroadcastTransactionStatus } from 'libs/transaction';
import { IWallet } from 'libs/wallet'; import { IWallet } from 'libs/wallet';
@ -11,8 +11,9 @@ export function getWalletInst(state: AppState): IWallet | null | undefined {
export interface TokenBalance { export interface TokenBalance {
symbol: string; symbol: string;
balance: BigNumber; balance: TokenValue;
custom: boolean; custom: boolean;
decimal: number;
} }
export type MergedToken = Token & { export type MergedToken = Token & {
@ -38,8 +39,9 @@ export function getTokenBalances(state: AppState): TokenBalance[] {
symbol: t.symbol, symbol: t.symbol,
balance: state.wallet.tokens[t.symbol] balance: state.wallet.tokens[t.symbol]
? state.wallet.tokens[t.symbol] ? state.wallet.tokens[t.symbol]
: new Big(0), : TokenValue('0'),
custom: t.custom custom: t.custom,
decimal: t.decimal
})); }));
} }

View File

@ -369,7 +369,7 @@ declare module 'bn.js' {
* @description reduct * @description reduct
*/ */
modn(b: number): BN; modn(b: number): number; //API consistency https://github.com/indutny/bn.js/pull/130
/** /**
* @description rounded division * @description rounded division

View File

@ -1,4 +1,4 @@
import { BigNumber } from 'bignumber.js'; import { Wei } from 'libs/units';
export function toFixedIfLarger(num: number, fixedSize: number = 6): string { export function toFixedIfLarger(num: number, fixedSize: number = 6): string {
return parseFloat(num.toFixed(fixedSize)).toString(); return parseFloat(num.toFixed(fixedSize)).toString();
@ -8,9 +8,53 @@ export function combineAndUpper(...args: string[]) {
return args.reduce((acc, item) => acc.concat(item.toUpperCase()), ''); return args.reduce((acc, item) => acc.concat(item.toUpperCase()), '');
} }
const toFixed = (num: string, digits: number = 3) => {
const [integerPart, fractionPart = ''] = num.split('.');
if (fractionPart.length === digits) {
return num;
}
if (fractionPart.length < digits) {
return `${integerPart}.${fractionPart.padEnd(digits, '0')}`;
}
let decimalPoint = integerPart.length;
const formattedFraction = fractionPart.slice(0, digits);
const integerArr = `${integerPart}${formattedFraction}`
.split('')
.map(str => +str);
let carryOver = Math.floor((+fractionPart[digits] + 5) / 10);
// grade school addition / rounding
for (let i = integerArr.length - 1; i >= 0; i--) {
const currVal = integerArr[i] + carryOver;
const newVal = currVal % 10;
carryOver = Math.floor(currVal / 10);
integerArr[i] = newVal;
if (i === 0 && carryOver > 0) {
integerArr.unshift(0);
decimalPoint++;
i++;
}
}
const strArr = integerArr.map(n => n.toString());
strArr.splice(decimalPoint, 0, '.');
if (strArr[strArr.length - 1] === '.') {
strArr.pop();
}
return strArr.join('');
};
// Use in place of angular number filter // Use in place of angular number filter
export function formatNumber(num: BigNumber, digits: number = 3): string { export function formatNumber(num: string, digits?: number): string {
const parts = num.toFixed(digits).split('.'); const parts = toFixed(num, digits).split('.');
// Remove trailing zeroes on decimal (If there is a decimal) // Remove trailing zeroes on decimal (If there is a decimal)
if (parts[1]) { if (parts[1]) {
@ -29,10 +73,7 @@ export function formatNumber(num: BigNumber, digits: number = 3): string {
} }
// TODO: Comment up this function to make it clear what's happening here. // TODO: Comment up this function to make it clear what's happening here.
export function formatGasLimit( export function formatGasLimit(limit: Wei, transactionUnit: string = 'ether') {
limit: BigNumber,
transactionUnit: string = 'ether'
) {
let limitStr = limit.toString(); let limitStr = limit.toString();
// I'm guessing this is some known off-by-one-error from the node? // I'm guessing this is some known off-by-one-error from the node?
@ -47,7 +88,7 @@ export function formatGasLimit(
// TODO: Make this dynamic, potentially. Would require promisifying this fn. // TODO: Make this dynamic, potentially. Would require promisifying this fn.
// TODO: Figure out if this is only true for ether. Do other currencies have // TODO: Figure out if this is only true for ether. Do other currencies have
// this limit? // this limit?
if (limit.gte(4000000)) { if (limit.gten(4000000)) {
limitStr = '-1'; limitStr = '-1';
} }

View File

@ -8,7 +8,6 @@
"npm": ">= 5.0.0" "npm": ">= 5.0.0"
}, },
"dependencies": { "dependencies": {
"bignumber.js": "4.0.2",
"bip39": "2.4.0", "bip39": "2.4.0",
"bn.js": "4.11.8", "bn.js": "4.11.8",
"bootstrap-sass": "3.3.7", "bootstrap-sass": "3.3.7",
@ -45,7 +44,6 @@
"whatwg-fetch": "2.0.3" "whatwg-fetch": "2.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/bignumber.js": "4.0.2",
"@types/classnames": "2.2.3", "@types/classnames": "2.2.3",
"@types/history": "4.6.1", "@types/history": "4.6.1",
"@types/jest": "21.1.5", "@types/jest": "21.1.5",
@ -118,6 +116,7 @@
"build:demo": "BUILD_GH_PAGES=true webpack --config webpack_config/webpack.prod.js", "build:demo": "BUILD_GH_PAGES=true webpack --config webpack_config/webpack.prod.js",
"prebuild:demo": "check-node-version --package", "prebuild:demo": "check-node-version --package",
"test": "jest --config=jest_config/jest.config.json --coverage", "test": "jest --config=jest_config/jest.config.json --coverage",
"updateSnapshot": "jest --config=jest_config/jest.config.json --updateSnapshot",
"pretest": "check-node-version --package", "pretest": "check-node-version --package",
"dev": "node webpack_config/server.js", "dev": "node webpack_config/server.js",
"predev": "check-node-version --package", "predev": "check-node-version --package",

View File

@ -1,4 +1,4 @@
import Big from 'bignumber.js'; import { toWei } from 'libs/units';
import ERC20 from 'libs/erc20'; import ERC20 from 'libs/erc20';
const MEW_ADDRESS = '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8'; const MEW_ADDRESS = '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8';
@ -15,7 +15,8 @@ describe('ERC20', () => {
describe('transfer', () => { describe('transfer', () => {
it('should generate the correct data for a transfer', () => { it('should generate the correct data for a transfer', () => {
// Test data generated by sending 1 GNT to the MEW address // Test data generated by sending 1 GNT to the MEW address
const value = new Big('1').times(new Big(10).pow(18));
const value = toWei('1', 18);
const data = ERC20.transfer(MEW_ADDRESS, value); const data = ERC20.transfer(MEW_ADDRESS, value);
expect(data).toBe( expect(data).toBe(
'0xa9059cbb0000000000000000000000007cb57b5a97eabe94205c07890be4c1ad31e486a80000000000000000000000000000000000000000000000000de0b6b3a7640000' '0xa9059cbb0000000000000000000000007cb57b5a97eabe94205c07890be4c1ad31e486a80000000000000000000000000000000000000000000000000de0b6b3a7640000'
@ -31,9 +32,7 @@ describe('ERC20', () => {
); );
expect(tx.to).toBe(MEW_ADDRESS); expect(tx.to).toBe(MEW_ADDRESS);
expect(tx.value).toBe( expect(tx.value.toString()).toBe(toWei('0.001', 18).toString());
new Big('0.001').times(new Big(10).pow(18)).toString()
);
}); });
}); });
}); });

View File

@ -1,6 +1,6 @@
// Ref: https://github.com/ethereum/wiki/wiki/JSON-RPC // Ref: https://github.com/ethereum/wiki/wiki/JSON-RPC
import { hexEncodeQuantity, hexEncodeData } from 'libs/nodes/rpc/utils'; import { hexEncodeQuantity, hexEncodeData } from 'libs/nodes/rpc/utils';
import Big from 'bignumber.js'; import BN from 'bn.js';
// 0x41 (65 in decimal) // 0x41 (65 in decimal)
// 0x400 (1024 in decimal) // 0x400 (1024 in decimal)
@ -9,13 +9,13 @@ import Big from 'bignumber.js';
// WRONG: ff (must be prefixed 0x) // WRONG: ff (must be prefixed 0x)
describe('hexEncodeQuantity', () => { describe('hexEncodeQuantity', () => {
it('convert dec to hex', () => { it('convert dec to hex', () => {
expect(hexEncodeQuantity(new Big(65))).toEqual('0x41'); expect(hexEncodeQuantity(new BN(65))).toEqual('0x41');
}); });
it('should strip leading zeroes', () => { it('should strip leading zeroes', () => {
expect(hexEncodeQuantity(new Big(1024))).toEqual('0x400'); expect(hexEncodeQuantity(new BN(1024))).toEqual('0x400');
}); });
it('should handle zeroes correctly', () => { it('should handle zeroes correctly', () => {
expect(hexEncodeQuantity(new Big(0))).toEqual('0x0'); expect(hexEncodeQuantity(new BN(0))).toEqual('0x0');
}); });
}); });

View File

@ -1,49 +1,98 @@
import Big from 'bignumber.js'; import {
import { toWei, toUnit, UnitKey } from '../../common/libs/units'; fromWei,
Wei,
toWei,
toTokenBase,
fromTokenBase,
getDecimal,
TokenValue
} from 'libs/units';
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'
};
describe('Units', () => { describe('Units', () => {
describe('toWei', () => { describe('ethereum units', () => {
const conversions = [ describe('ether', () => {
{ const wei = Wei(Units.ether);
value: '0.001371', const ether = fromWei(wei, 'ether');
unit: 'ether' as UnitKey, it('should equal one ether', () => {
wei: '1371000000000000' expect(ether).toEqual('1');
}, });
{
value: '9',
unit: 'gwei' as UnitKey,
wei: '9000000000'
}
];
conversions.forEach(c => { it('should equal 1 * 10^18 wei', () => {
it(`should return '${c.wei}' given ${c.value} ${c.unit}`, () => { const converted = toWei(ether, getDecimal('ether'));
const big = new Big(c.value); expect(converted.toString()).toEqual(Units.ether);
expect(toWei(big, c.unit).toString()).toEqual(c.wei); });
});
describe('gwei', () => {
const wei = Wei(`2${Units.gwei}`);
const gwei = fromWei(wei, 'gwei');
it('should equal 21 gwei', () => {
expect(gwei).toEqual('21');
});
it('should equal 21 * 10^9 wei', () => {
const converted = toWei(gwei, getDecimal('gwei'));
expect(converted.toString()).toEqual(wei.toString());
});
});
describe('kwei', () => {
const wei = Wei('1623');
const kwei = fromWei(wei, 'kwei');
it('should equal 1.623 kwei', () => {
expect(kwei).toEqual('1.623');
});
it('should equal 1.623 * 10^3 wei', () => {
const converted = toWei(kwei, getDecimal('kwei'));
expect(converted.toString()).toEqual('1623');
}); });
}); });
}); });
describe('token units', () => {
describe('toUnit', () => { describe('token 1', () => {
const conversions = [ const tokens = '732156.34592016';
{ const decimal = 18;
value: '.41849', const tokenBase = toTokenBase(tokens, decimal);
fromUnit: 'ether' as UnitKey, it('should equal 732156345920160000000000', () => {
toUnit: 'gwei' as UnitKey, expect(tokenBase.toString()).toEqual('732156345920160000000000');
output: '418490000' });
}, it('should equal 732156.34592016', () => {
{ expect(fromTokenBase(tokenBase, decimal)).toEqual(tokens);
value: '4924.71', });
fromUnit: 'nanoether' as UnitKey, });
toUnit: 'szabo' as UnitKey, describe('token 2', () => {
output: '4.92471' const tokens = '8000';
} const decimal = 8;
]; const converted = fromTokenBase(TokenValue(tokens), decimal);
it('should equal 0.00008', () => {
conversions.forEach(c => { expect(converted).toEqual('0.00008');
it(`should return '${c.output}' when converting ${c.value} ${c.fromUnit} to ${c.toUnit}`, () => { });
const big = new Big(c.value); it('should equal 8000', () => {
expect(toUnit(big, c.fromUnit, c.toUnit).toString()).toEqual(c.output); expect(toTokenBase(converted, decimal));
}); });
}); });
}); });

View File

@ -4,12 +4,7 @@ exports[`render snapshot 1`] = `
<SendTransaction <SendTransaction
broadcastTx={[Function]} broadcastTx={[Function]}
forceOffline={false} forceOffline={false}
gasPrice={ gasPrice={"4e3b29200"}
Wei {
"amount": "21000000000",
"unit": "wei",
}
}
location={ location={
Object { Object {
"search": "?to=73640ebefe93e4d0d6e9030ee9c1866ad1f3b9f1feeb403e978c4952d8369b39", "search": "?to=73640ebefe93e4d0d6e9030ee9c1866ad1f3b9f1feeb403e978c4952d8369b39",

View File

@ -1,6 +1,6 @@
import { wallet, INITIAL_STATE } from 'reducers/wallet'; import { wallet, INITIAL_STATE } from 'reducers/wallet';
import * as walletActions from 'actions/wallet'; import * as walletActions from 'actions/wallet';
import Big from 'bignumber.js'; import { TokenValue } from 'libs/units';
describe('wallet reducer', () => { describe('wallet reducer', () => {
it('should handle WALLET_SET', () => { it('should handle WALLET_SET', () => {
@ -23,7 +23,7 @@ describe('wallet reducer', () => {
}); });
it('should handle WALLET_SET_TOKEN_BALANCES', () => { it('should handle WALLET_SET_TOKEN_BALANCES', () => {
const tokenBalances = { OMG: new Big(20) }; const tokenBalances = { OMG: TokenValue('20') };
expect( expect(
wallet(undefined, walletActions.setTokenBalances(tokenBalances)) wallet(undefined, walletActions.setTokenBalances(tokenBalances))
).toEqual({ ).toEqual({

View File

@ -1,4 +1,4 @@
import Big from 'bignumber.js'; import { Wei } from 'libs/units';
import { import {
toFixedIfLarger, toFixedIfLarger,
formatNumber, formatNumber,
@ -24,35 +24,57 @@ describe('toFixedIfLarger', () => {
describe('formatNumber', () => { describe('formatNumber', () => {
const pairs = [ const pairs = [
{ {
input: new Big('0.0127491'), input: '0.0127491',
output: '0.013', output: '0.013',
digits: undefined digits: undefined
}, },
{ {
input: new Big('21.87468421'), input: '21.87468421',
output: '21.875', output: '21.875',
digits: undefined digits: undefined
}, },
{ {
input: new Big(0), input: '0',
output: '0', output: '0',
digits: undefined digits: undefined
}, },
{ {
input: new Big('354.4728173'), input: '354.4728173',
output: '354.4728', output: '354.4728',
digits: 4 digits: 4
}, },
{ {
input: new Big('100.48391'), input: '100.48391',
output: '100', output: '100',
digits: 0 digits: 0
},
{
input: '239.999632',
output: '240',
digits: 0
},
{
input: '999.999',
output: '1,000',
digits: 0
},
{
input: '0.9',
output: '1',
digits: 0
},
{
input: '0.09',
output: '0.1',
digits: 1
} }
]; ];
pairs.forEach(pair => { pairs.forEach(pair => {
const digits = pair.digits; const digits = pair.digits;
it(`should convert ${pair.input.toString()} to ${pair.output} when using ${digits} digits`, () => { it(`should convert ${pair.input.toString()} to ${pair.output} when using ${
digits
} digits`, () => {
expect(formatNumber(pair.input, pair.digits)).toEqual(pair.output); expect(formatNumber(pair.input, pair.digits)).toEqual(pair.output);
}); });
}); });
@ -60,14 +82,14 @@ describe('formatNumber', () => {
describe('formatGasLimit', () => { describe('formatGasLimit', () => {
it('should fix transaction gas limit off-by-one errors', () => { it('should fix transaction gas limit off-by-one errors', () => {
expect(formatGasLimit(new Big(21001), 'ether')).toEqual('21000'); expect(formatGasLimit(Wei('21001'), 'ether')).toEqual('21000');
}); });
it('should mark the gas limit `-1` if you exceed the limit per block', () => { it('should mark the gas limit `-1` if you exceed the limit per block', () => {
expect(formatGasLimit(new Big(999999999999999), 'ether')).toEqual('-1'); expect(formatGasLimit(Wei('999999999999999'), 'ether')).toEqual('-1');
}); });
it('should not alter a valid gas limit', () => { it('should not alter a valid gas limit', () => {
expect(formatGasLimit(new Big(1234))).toEqual('1234'); expect(formatGasLimit(Wei('1234'))).toEqual('1234');
}); });
}); });