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_FORCE_OFFLINE = 'CONFIG_FORCE_OFFLINE',
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 {
[key: string]: BigNumber;
export interface ITokenData {
value: TokenValue;
decimal: number;
}
export interface ITokenValues {
[key: string]: ITokenData | null;
}
export interface DeterministicWalletData {
index: number;
address: string;
value?: BigNumber;
tokenValues: TokenValues;
value?: TokenValue;
tokenValues: ITokenValues;
}
/*** Get determinstic wallets ***/
@ -39,8 +44,8 @@ export interface SetDesiredTokenAction {
/*** Set wallet values ***/
export interface UpdateDeterministicWalletArgs {
address: string;
value?: BigNumber;
tokenValues?: TokenValues;
value?: Wei;
tokenValues?: ITokenValues;
index?: any;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -54,9 +54,9 @@ export default class KeystoreDecrypt extends Component {
<div className={file.length && passReq ? '' : 'hidden'}>
<p>{translate('ADD_Label_3')}</p>
<input
className={`form-control ${password.length > 0
? 'is-valid'
: 'is-invalid'}`}
className={`form-control ${
password.length > 0 ? 'is-valid' : 'is-invalid'
}`}
value={password}
onChange={this.onPasswordChange}
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 QRCode } from './QRCode';
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 {
generateCompleteTransaction as makeAndSignTx,
@ -12,7 +13,6 @@ import {
} from 'containers/Tabs/Contracts/components/TxModal';
import {
TxCompare,
Props as TCProps,
TTxCompare
} from 'containers/Tabs/Contracts/components/TxCompare';
import { withTx } from 'containers/Tabs/Contracts/components//withTx';
@ -81,25 +81,13 @@ export const deployHOC = PassedComponent => {
}
private displayCompareTx = (): React.ReactElement<TTxCompare> => {
const { nonce, gasLimit, data, value, signedTx, to } = this.state;
const { gasPrice, chainId } = this.props;
const { signedTx, nonce } = this.state;
if (!nonce || !signedTx) {
throw Error('Can not display raw tx, nonce empty or no signed tx');
}
const props: TCProps = {
nonce,
gasPrice,
chainId,
data,
gasLimit,
to,
value,
signedTx
};
return <TxCompare {...props} />;
return <TxCompare signedTx={signedTx} />;
};
private displayDeployModal = (): React.ReactElement<TTxModal> => {
@ -143,7 +131,7 @@ export const deployHOC = PassedComponent => {
props.wallet,
props.nodeLib,
props.gasPrice,
new Big(gasLimit),
Wei(gasLimit),
props.chainId,
transactionInput,
true
@ -154,7 +142,7 @@ export const deployHOC = PassedComponent => {
const address = await this.props.wallet.getAddressString();
const nonce = await this.props.nodeLib
.getTransactionCount(address)
.then(n => new Big(n).toString());
.then(n => new BN(n).toString());
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 { RPCNode } from 'libs/nodes';
import { NodeConfig, NetworkConfig } from 'config/data';
@ -7,7 +7,7 @@ import { TShowNotification } from 'actions/notifications';
export interface Props {
wallet: IWallet;
balance: Ether;
balance: Wei;
node: NodeConfig;
nodeLib: RPCNode;
chainId: NetworkConfig['chainId'];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ import {
} from 'selectors/config';
import { getTokens, getTxFromState, MergedToken } from 'selectors/wallet';
import translate, { translateRaw } from 'translations';
import { UnitDisplay } from 'components/ui';
import './ConfirmationModal.scss';
interface Props {
@ -27,6 +28,7 @@ interface Props {
network: NetworkConfig;
lang: string;
broadCastTxStatus: BroadcastTransactionStatus;
decimal: number;
onConfirm(signedTx: string): void;
onClose(): void;
}
@ -72,7 +74,8 @@ class ConfirmationModal extends React.Component<Props, State> {
network,
onClose,
broadCastTxStatus,
transaction
transaction,
decimal
} = this.props;
const { timeToRead } = this.state;
const { toAddress, value, gasPrice, data, from, nonce } = decodeTransaction(
@ -109,77 +112,88 @@ class ConfirmationModal extends React.Component<Props, State> {
disableButtons={isBroadcasting}
isOpen={true}
>
{
<div className="ConfModal">
{isBroadcasting ? (
<div className="ConfModal-loading">
<Spinner size="5x" />
</div>
) : (
<div>
<div className="ConfModal-summary">
<div className="ConfModal-summary-icon ConfModal-summary-icon--from">
<Identicon size="100%" address={from} />
</div>
<div className="ConfModal-summary-amount">
<div className="ConfModal-summary-amount-arrow" />
<div className="ConfModal-summary-amount-currency">
{value} {symbol}
</div>
</div>
<div className="ConfModal-summary-icon ConfModal-summary-icon--to">
<Identicon size="100%" address={toAddress} />
<div className="ConfModal">
{isBroadcasting ? (
<div className="ConfModal-loading">
<Spinner size="5x" />
</div>
) : (
<div>
<div className="ConfModal-summary">
<div className="ConfModal-summary-icon ConfModal-summary-icon--from">
<Identicon size="100%" address={from} />
</div>
<div className="ConfModal-summary-amount">
<div className="ConfModal-summary-amount-arrow" />
<div className="ConfModal-summary-amount-currency">
<UnitDisplay
decimal={decimal}
value={value}
symbol={symbol}
/>
</div>
</div>
<ul className="ConfModal-details">
<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 className="ConfModal-summary-icon ConfModal-summary-icon--to">
<Identicon size="100%" address={toAddress} />
</div>
</div>
)}
</div>
}
<ul className="ConfModal-details">
<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>
</div>
);

View File

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

View File

@ -18,7 +18,9 @@ function promiseFromChildProcess(command): Promise<any> {
}
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 newlineStrippedDockerOutput = dockerOutput.replace(
/(\r\n|\n|\r)/gm,

View File

@ -1,6 +1,6 @@
import abi from 'ethereumjs-abi';
import { toChecksumAddress } from 'ethereumjs-util';
import Big, { BigNumber } from 'bignumber.js';
import BN from 'bn.js';
import { INode } from 'libs/nodes/INode';
import { FuncParams, FunctionOutputMappings, Output, Input } from './types';
import {
@ -12,7 +12,7 @@ import { ISetConfigForTx } from './index';
export interface IUserSendParams {
input;
to: string;
gasLimit: BigNumber;
gasLimit: BN;
value: string;
}
export type ISendParams = IUserSendParams & ISetConfigForTx;
@ -153,18 +153,11 @@ EncodedCall:${data}`);
return valueMapping[type]
? valueMapping[type](value)
: this.isBigNumber(value) ? value.toString() : value;
: BN.isBN(value) ? value.toString() : value;
};
private parsePreEncodedValue = (_: string, value: any) =>
this.isBigNumber(value) ? value.toString() : value;
private isBigNumber = (object: object) =>
object instanceof Big ||
(object &&
object.constructor &&
(object.constructor.name === 'BigNumber' ||
object.constructor.name === 'BN'));
BN.isBN(value) ? value.toString() : value;
private makeFuncParams = () =>
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 Contract, { ABI } from 'libs/contract';
interface Transfer {
to: string;
value: string;
value: BN;
}
const erc20Abi: ABI = [
@ -59,7 +59,7 @@ class ERC20 extends Contract {
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()]);
}
@ -67,7 +67,7 @@ class ERC20 extends Contract {
const decodedArgs = this.decodeArgs(this.getMethodAbi('transfer'), data);
return {
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 { TransactionWithoutGas } from 'libs/messages';
import { Wei } from 'libs/units';
import { Wei, TokenValue } from 'libs/units';
export interface TxObj {
to: string;
@ -9,9 +8,9 @@ export interface TxObj {
}
export interface INode {
getBalance(address: string): Promise<Wei>;
getTokenBalance(address: string, token: Token): Promise<BigNumber>;
getTokenBalances(address: string, tokens: Token[]): Promise<BigNumber[]>;
estimateGas(tx: TransactionWithoutGas): Promise<BigNumber>;
getTokenBalance(address: string, token: Token): Promise<TokenValue>;
getTokenBalances(address: string, tokens: Token[]): Promise<TokenValue[]>;
estimateGas(tx: TransactionWithoutGas): Promise<Wei>;
getTransactionCount(address: string): Promise<string>;
sendRawTx(tx: string): 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 { TransactionWithoutGas } from 'libs/messages';
import { Wei } from 'libs/units';
import { Wei, TokenValue } from 'libs/units';
import { INode, TxObj } from '../INode';
import RPCClient from './client';
import RPCRequests from './requests';
@ -30,50 +29,46 @@ export default class RpcNode implements INode {
if (response.error) {
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
.call(this.requests.estimateGas(transaction))
.then(response => {
if (response.error) {
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
.call(this.requests.getTokenBalance(address, token))
.then(response => {
if (response.error) {
// TODO - Error handling
return new Big(0);
return TokenValue('0');
}
return new Big(String(response.result)).div(
new Big(10).pow(token.decimal)
);
return TokenValue(response.result);
});
}
public getTokenBalances(
address: string,
tokens: Token[]
): Promise<BigNumber[]> {
): Promise<TokenValue[]> {
return this.client
.batch(tokens.map(t => this.requests.getTokenBalance(address, t)))
.then(response => {
return response.map((item, idx) => {
return response.map(item => {
// FIXME wrap in maybe-like
if (item.error) {
return new Big(0);
return TokenValue('0');
}
return new Big(String(item.result)).div(
new Big(10).pow(tokens[idx].decimal)
);
return TokenValue(item.result);
});
});
// TODO - Error handling

View File

@ -1,10 +1,10 @@
// Ref: https://github.com/ethereum/wiki/wiki/JSON-RPC
import { BigNumber } from 'bignumber.js';
import BN from 'bn.js';
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").
export function hexEncodeQuantity(value: BigNumber): string {
export function hexEncodeQuantity(value: BN): string {
return '0x' + (value.toString(16) || '0');
}

View File

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

View File

@ -5,19 +5,11 @@ import ERC20 from 'libs/erc20';
import { TransactionWithoutGas } from 'libs/messages';
import { RPCNode } from 'libs/nodes';
import { INode } from 'libs/nodes/INode';
import {
Ether,
toTokenUnit,
UnitKey,
Wei,
toTokenDisplay,
toUnit
} from 'libs/units';
import { UnitKey, Wei, TokenValue, toTokenBase } from 'libs/units';
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 { translateRaw } from 'translations';
import Big, { BigNumber } from 'bignumber.js';
export interface TransactionInput {
token?: Token | null;
@ -37,7 +29,7 @@ export interface BaseTransaction {
to: string;
value: string;
data: string;
gasLimit: BigNumber | string;
gasLimit: Wei | string;
gasPrice: Wei | string;
chainId: number;
}
@ -82,12 +74,12 @@ export function getTransactionFields(tx: EthTx) {
function getValue(
token: Token | null | undefined,
tx: ExtendedRawTransaction
): BigNumber {
): Wei {
let value;
if (token) {
value = new Big(ERC20.$transfer(tx.data).value);
value = Wei(ERC20.$transfer(tx.data).value);
} else {
value = new Big(tx.value);
value = Wei(tx.value);
}
return value;
}
@ -99,11 +91,14 @@ async function getBalance(
) {
const { from } = tx;
const ETHBalance = await node.getBalance(from);
let balance;
let balance: Wei;
if (token) {
balance = toTokenUnit(await node.getTokenBalance(tx.from, token), token);
balance = toTokenBase(
await node.getTokenBalance(tx.from, token).toString(),
token.decimal
);
} else {
balance = ETHBalance.amount;
balance = ETHBalance;
}
return {
balance,
@ -115,7 +110,7 @@ async function balanceCheck(
node: INode,
tx: ExtendedRawTransaction,
token: Token | null | undefined,
value: BigNumber,
value: Wei,
gasCost: Wei
) {
// 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
// TODO check that eth balance is not lesser than txAmount + gasCost
if (gasCost.amount.gt(ETHBalance.amount)) {
if (gasCost.gt(ETHBalance)) {
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,
token: Token | null | undefined,
data: string,
gasLimit: BigNumber | string,
gasLimit: Wei | string,
gasPrice: Wei | string,
skipEthAddressValidation: boolean
) {
@ -154,16 +149,16 @@ function generateTxValidation(
// Reject gas limit under 21000 (Minimum for transaction)
// Reject if limit over 5000000
// 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');
}
// Reject gasLimit over 5000000gwei
if (gasLimit.greaterThan(5000000)) {
if (gasLimit.gtn(5000000)) {
throw new Error(translateRaw('GETH_GasLimit'));
}
// Reject gasPrice over 1000gwei (1000000000000)
const gwei = new Big('1000000000000');
if (gasPrice.amount.greaterThan(gwei)) {
const gwei = Wei('1000000000000');
if (gasPrice.gt(gwei)) {
throw new Error(
'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');
}
// 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)
const value = getValue(token, tx);
// if not offline, ensure that balance exceeds costs
@ -225,14 +220,14 @@ export async function formatTxInput(
return {
to,
from: await wallet.getAddressString(),
value: valueToHex(new Ether(value)),
value: toHexWei(value), //turn users ether to wei
data
};
} else {
if (!token) {
throw new Error('No matching token');
}
const bigAmount = new Big(value);
const bigAmount = TokenValue(value);
const ERC20Data = ERC20.transfer(to, bigAmount);
return {
to: token.address,
@ -247,7 +242,7 @@ export async function confirmAndSendWeb3Transaction(
wallet: Web3Wallet,
nodeLib: RPCNode,
gasPrice: Wei,
gasLimit: BigNumber,
gasLimit: Wei,
chainId: number,
transactionInput: TransactionInput
): Promise<string> {
@ -273,7 +268,7 @@ export async function generateCompleteTransaction(
wallet: IWallet,
nodeLib: RPCNode,
gasPrice: Wei,
gasLimit: BigNumber,
gasLimit: Wei,
chainId: number,
transactionInput: TransactionInput,
skipValidation: boolean,
@ -307,34 +302,34 @@ export async function generateCompleteTransaction(
// TODO determine best place for helper function
export function getBalanceMinusGasCosts(
gasLimit: BigNumber,
gasLimit: Wei,
gasPrice: Wei,
balance: Wei
): Ether {
const weiGasCosts = gasPrice.amount.times(gasLimit);
const weiBalanceMinusGasCosts = balance.amount.minus(weiGasCosts);
return new Ether(weiBalanceMinusGasCosts);
): 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;
let fixedValue: TokenValue;
let toAddress;
if (token) {
const tokenData = ERC20.$transfer(data);
fixedValue = toTokenDisplay(new Big(tokenData.value), token).toString();
fixedValue = tokenData.value;
toAddress = tokenData.to;
} else {
fixedValue = toUnit(new Big(value, 16), 'wei', 'ether').toString();
fixedValue = Wei(value);
toAddress = to;
}
return {
value: fixedValue,
gasPrice: toUnit(new Big(gasPrice, 16), 'wei', 'gwei').toString(),
gasPrice: Wei(gasPrice),
data,
toAddress,
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 { Token } from 'config/data';
import BN from 'bn.js';
import { stripHexPrefix } from 'libs/values';
type UnitKey = keyof typeof Units;
type Wei = BN;
type TokenValue = BN;
const Units = {
wei: '1',
@ -27,79 +31,74 @@ const Units = {
gether: '1000000000000000000000000000',
tether: '1000000000000000000000000000000'
};
export type TUnit = typeof Units;
export type UnitKey = keyof TUnit;
class Unit {
public unit: UnitKey;
public amount: BigNumber;
constructor(amount: BigNumber, unit: UnitKey) {
this.unit = unit;
this.amount = amount;
const handleValues = (input: string | BN) => {
if (typeof input === 'string') {
return input.startsWith('0x')
? new BN(stripHexPrefix(input), 16)
: new BN(input);
}
public toString(base?: number) {
return this.amount.toString(base);
if (typeof input === 'number') {
return new BN(input);
}
public toPrecision(precision?: number) {
return this.amount.toPrecision(precision);
if (BN.isBN(input)) {
return input;
}
throw Error('unsupported value conversion');
};
public toWei(): Wei {
return new Wei(toWei(this.amount, this.unit));
const Wei = (input: string | BN): Wei => handleValues(input);
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 {
return new GWei(toUnit(this.amount, this.unit, 'gwei'));
const convertedToBaseUnit = (value: string, decimal: number) => {
if (decimal === 0) {
return value;
}
const [integerPart, fractionPart = ''] = value.split('.');
const paddedFraction = fractionPart.padEnd(decimal, '0');
return `${integerPart}${paddedFraction}`;
};
public toEther(): Ether {
return new Ether(toUnit(this.amount, this.unit, 'ether'));
}
}
const fromWei = (wei: Wei, unit: UnitKey) => {
const decimal = getDecimal(unit);
return baseToConvertedUnit(wei.toString(), decimal);
};
// tslint:disable:max-classes-per-file
export class Ether extends Unit {
constructor(amount: BigNumber | number | string) {
super(new Big(amount), 'ether');
}
}
const toWei = (value: string, decimal: number): Wei => {
const wei = convertedToBaseUnit(value, decimal);
return Wei(wei);
};
export class Wei extends Unit {
constructor(amount: BigNumber | number | string) {
super(new Big(amount), 'wei');
}
}
const fromTokenBase = (value: TokenValue, decimal: number) =>
baseToConvertedUnit(value.toString(), decimal);
export class GWei extends Unit {
constructor(amount: BigNumber | number | string) {
super(new Big(amount), 'gwei');
}
}
const toTokenBase = (value: string, decimal: number) =>
TokenValue(convertedToBaseUnit(value, decimal));
function getValueOfUnit(unit: UnitKey) {
return new Big(Units[unit]);
}
export function toWei(num: BigNumber, unit: UnitKey): BigNumber {
return num.times(getValueOfUnit(unit));
}
export function toUnit(
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));
}
export {
TokenValue,
fromWei,
toWei,
toTokenBase,
fromTokenBase,
Wei,
getDecimal,
UnitKey
};

View File

@ -1,5 +1,4 @@
import { Ether } from 'libs/units';
import { Wei } from 'libs/units';
export function stripHexPrefix(value: string) {
return value.replace('0x', '');
}
@ -8,11 +7,8 @@ export function stripHexPrefixAndLower(value: string): string {
return stripHexPrefix(value).toLowerCase();
}
export function valueToHex(value: Ether): string {
// Values are in ether, so convert to wei for RPC calls
const wei = value.toWei();
// Finally, hex it up!
return `0x${wei.toString(16)}`;
export function toHexWei(weiString: string): string {
return `0x${Wei(weiString).toString(16)}`;
}
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 { addHexPrefix } from 'ethereumjs-util';
import { RawTransaction } from 'libs/transaction';
@ -6,6 +6,7 @@ import { stripHexPrefixAndLower } from 'libs/values';
import TrezorConnect from 'vendor/trezor-connect';
import { DeterministicWallet } from './deterministic';
import { IWallet } from '../IWallet';
export class TrezorWallet extends DeterministicWallet implements IWallet {
public signRawTransaction(tx: RawTransaction): Promise<string> {
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
const txToSerialize = {
...tx,
v: addHexPrefix(new Big(result.v).toString(16)),
v: addHexPrefix(new BN(result.v).toString(16)),
r: addHexPrefix(result.r),
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 => {
if (w.address === newWallet.address) {
return {

View File

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

View File

@ -20,7 +20,10 @@ import {
changeNode,
changeNodeIntent
} 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;
@ -43,7 +46,6 @@ function* handlePollOfflineStatus(): SagaIterator {
yield cancel(pollOfflineStatusTask);
}
// @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.
function* reload(): SagaIterator {

View File

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

View File

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

View File

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

View File

@ -369,7 +369,7 @@ declare module 'bn.js' {
* @description reduct
*/
modn(b: number): BN;
modn(b: number): number; //API consistency https://github.com/indutny/bn.js/pull/130
/**
* @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 {
return parseFloat(num.toFixed(fixedSize)).toString();
@ -8,9 +8,53 @@ export function combineAndUpper(...args: string[]) {
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
export function formatNumber(num: BigNumber, digits: number = 3): string {
const parts = num.toFixed(digits).split('.');
export function formatNumber(num: string, digits?: number): string {
const parts = toFixed(num, digits).split('.');
// Remove trailing zeroes on decimal (If there is a decimal)
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.
export function formatGasLimit(
limit: BigNumber,
transactionUnit: string = 'ether'
) {
export function formatGasLimit(limit: Wei, transactionUnit: string = 'ether') {
let limitStr = limit.toString();
// 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: Figure out if this is only true for ether. Do other currencies have
// this limit?
if (limit.gte(4000000)) {
if (limit.gten(4000000)) {
limitStr = '-1';
}

View File

@ -8,7 +8,6 @@
"npm": ">= 5.0.0"
},
"dependencies": {
"bignumber.js": "4.0.2",
"bip39": "2.4.0",
"bn.js": "4.11.8",
"bootstrap-sass": "3.3.7",
@ -45,7 +44,6 @@
"whatwg-fetch": "2.0.3"
},
"devDependencies": {
"@types/bignumber.js": "4.0.2",
"@types/classnames": "2.2.3",
"@types/history": "4.6.1",
"@types/jest": "21.1.5",
@ -118,6 +116,7 @@
"build:demo": "BUILD_GH_PAGES=true webpack --config webpack_config/webpack.prod.js",
"prebuild:demo": "check-node-version --package",
"test": "jest --config=jest_config/jest.config.json --coverage",
"updateSnapshot": "jest --config=jest_config/jest.config.json --updateSnapshot",
"pretest": "check-node-version --package",
"dev": "node webpack_config/server.js",
"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';
const MEW_ADDRESS = '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8';
@ -15,7 +15,8 @@ describe('ERC20', () => {
describe('transfer', () => {
it('should generate the correct data for a transfer', () => {
// 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);
expect(data).toBe(
'0xa9059cbb0000000000000000000000007cb57b5a97eabe94205c07890be4c1ad31e486a80000000000000000000000000000000000000000000000000de0b6b3a7640000'
@ -31,9 +32,7 @@ describe('ERC20', () => {
);
expect(tx.to).toBe(MEW_ADDRESS);
expect(tx.value).toBe(
new Big('0.001').times(new Big(10).pow(18)).toString()
);
expect(tx.value.toString()).toBe(toWei('0.001', 18).toString());
});
});
});

View File

@ -1,6 +1,6 @@
// Ref: https://github.com/ethereum/wiki/wiki/JSON-RPC
import { hexEncodeQuantity, hexEncodeData } from 'libs/nodes/rpc/utils';
import Big from 'bignumber.js';
import BN from 'bn.js';
// 0x41 (65 in decimal)
// 0x400 (1024 in decimal)
@ -9,13 +9,13 @@ import Big from 'bignumber.js';
// WRONG: ff (must be prefixed 0x)
describe('hexEncodeQuantity', () => {
it('convert dec to hex', () => {
expect(hexEncodeQuantity(new Big(65))).toEqual('0x41');
expect(hexEncodeQuantity(new BN(65))).toEqual('0x41');
});
it('should strip leading zeroes', () => {
expect(hexEncodeQuantity(new Big(1024))).toEqual('0x400');
expect(hexEncodeQuantity(new BN(1024))).toEqual('0x400');
});
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 { toWei, toUnit, UnitKey } from '../../common/libs/units';
import {
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('toWei', () => {
const conversions = [
{
value: '0.001371',
unit: 'ether' as UnitKey,
wei: '1371000000000000'
},
{
value: '9',
unit: 'gwei' as UnitKey,
wei: '9000000000'
}
];
describe('ethereum units', () => {
describe('ether', () => {
const wei = Wei(Units.ether);
const ether = fromWei(wei, 'ether');
it('should equal one ether', () => {
expect(ether).toEqual('1');
});
conversions.forEach(c => {
it(`should return '${c.wei}' given ${c.value} ${c.unit}`, () => {
const big = new Big(c.value);
expect(toWei(big, c.unit).toString()).toEqual(c.wei);
it('should equal 1 * 10^18 wei', () => {
const converted = toWei(ether, getDecimal('ether'));
expect(converted.toString()).toEqual(Units.ether);
});
});
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('toUnit', () => {
const conversions = [
{
value: '.41849',
fromUnit: 'ether' as UnitKey,
toUnit: 'gwei' as UnitKey,
output: '418490000'
},
{
value: '4924.71',
fromUnit: 'nanoether' as UnitKey,
toUnit: 'szabo' as UnitKey,
output: '4.92471'
}
];
conversions.forEach(c => {
it(`should return '${c.output}' when converting ${c.value} ${c.fromUnit} to ${c.toUnit}`, () => {
const big = new Big(c.value);
expect(toUnit(big, c.fromUnit, c.toUnit).toString()).toEqual(c.output);
describe('token units', () => {
describe('token 1', () => {
const tokens = '732156.34592016';
const decimal = 18;
const tokenBase = toTokenBase(tokens, decimal);
it('should equal 732156345920160000000000', () => {
expect(tokenBase.toString()).toEqual('732156345920160000000000');
});
it('should equal 732156.34592016', () => {
expect(fromTokenBase(tokenBase, decimal)).toEqual(tokens);
});
});
describe('token 2', () => {
const tokens = '8000';
const decimal = 8;
const converted = fromTokenBase(TokenValue(tokens), decimal);
it('should equal 0.00008', () => {
expect(converted).toEqual('0.00008');
});
it('should equal 8000', () => {
expect(toTokenBase(converted, decimal));
});
});
});

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import Big from 'bignumber.js';
import { Wei } from 'libs/units';
import {
toFixedIfLarger,
formatNumber,
@ -24,35 +24,57 @@ describe('toFixedIfLarger', () => {
describe('formatNumber', () => {
const pairs = [
{
input: new Big('0.0127491'),
input: '0.0127491',
output: '0.013',
digits: undefined
},
{
input: new Big('21.87468421'),
input: '21.87468421',
output: '21.875',
digits: undefined
},
{
input: new Big(0),
input: '0',
output: '0',
digits: undefined
},
{
input: new Big('354.4728173'),
input: '354.4728173',
output: '354.4728',
digits: 4
},
{
input: new Big('100.48391'),
input: '100.48391',
output: '100',
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 => {
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);
});
});
@ -60,14 +82,14 @@ describe('formatNumber', () => {
describe('formatGasLimit', () => {
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', () => {
expect(formatGasLimit(new Big(999999999999999), 'ether')).toEqual('-1');
expect(formatGasLimit(Wei('999999999999999'), 'ether')).toEqual('-1');
});
it('should not alter a valid gas limit', () => {
expect(formatGasLimit(new Big(1234))).toEqual('1234');
expect(formatGasLimit(Wei('1234'))).toEqual('1234');
});
});