diff --git a/common/actions/wallet.js b/common/actions/wallet.js index b79ba0b3..ff1e6419 100644 --- a/common/actions/wallet.js +++ b/common/actions/wallet.js @@ -1,6 +1,6 @@ // @flow import BaseWallet from 'libs/wallet/base'; -import Big from 'big.js'; +import Big from 'bignumber.js'; /*** Unlock Private Key ***/ export type PrivateKeyUnlockParams = { diff --git a/common/components/BalanceSidebar/BalanceSidebar.jsx b/common/components/BalanceSidebar/BalanceSidebar.jsx index c5f00cba..740377e0 100644 --- a/common/components/BalanceSidebar/BalanceSidebar.jsx +++ b/common/components/BalanceSidebar/BalanceSidebar.jsx @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import Big from 'big.js'; +import Big from 'bignumber.js'; import { BaseWallet } from 'libs/wallet'; import type { NetworkConfig } from 'config/data'; import type { State } from 'reducers'; diff --git a/common/components/BalanceSidebar/TokenRow.jsx b/common/components/BalanceSidebar/TokenRow.jsx index adafe693..fc630c74 100644 --- a/common/components/BalanceSidebar/TokenRow.jsx +++ b/common/components/BalanceSidebar/TokenRow.jsx @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import Big from 'big.js'; +import Big from 'bignumber.js'; import { formatNumber } from 'utils/formatters'; import removeIcon from 'assets/images/icon-remove.svg'; diff --git a/common/config/data.js b/common/config/data.js index 50c2b64a..cbafa630 100644 --- a/common/config/data.js +++ b/common/config/data.js @@ -125,7 +125,7 @@ export type NetworkContract = { export type NetworkConfig = { name: string, - // unit: string, + unit: string, blockExplorer?: { name: string, tx: string, @@ -143,7 +143,7 @@ export type NetworkConfig = { export const NETWORKS: { [key: string]: NetworkConfig } = { ETH: { name: 'ETH', - // unit: 'ETH', + unit: 'ETH', chainId: 1, blockExplorer: { name: 'https://etherscan.io', diff --git a/common/containers/Tabs/SendTransaction/index.jsx b/common/containers/Tabs/SendTransaction/index.jsx index 72bf1a09..a92ebb40 100644 --- a/common/containers/Tabs/SendTransaction/index.jsx +++ b/common/containers/Tabs/SendTransaction/index.jsx @@ -20,9 +20,18 @@ import BaseWallet from 'libs/wallet/base'; // import type { Transaction } from './types'; import customMessages from './messages'; import { donationAddressMap } from 'config/data'; -import Big from 'big.js'; +import { isValidETHAddress } from 'libs/validators'; +import { getNodeLib } from 'selectors/config'; +import { getTokens } from 'selectors/wallet'; +import type { BaseNode } from 'libs/nodes'; +import type { Token } from 'config/data'; +import Big from 'bignumber.js'; +import { valueToHex } from 'libs/values'; +import ERC20 from 'libs/erc20'; import type { TokenBalance } from 'selectors/wallet'; import { getTokenBalances } from 'selectors/wallet'; +import type { TransactionWithoutGas } from 'libs/transaction'; +import { formatGasLimit } from 'utils/formatters'; type State = { hasQueryString: boolean, @@ -56,6 +65,8 @@ type Props = { }, wallet: BaseWallet, balance: Big, + node: BaseNode, + tokens: Token[], tokenBalances: TokenBalance[] }; @@ -80,6 +91,25 @@ export class SendTransaction extends React.Component { } } + componentDidUpdate(_prevProps: Props, prevState: State) { + // if gas is not changed + // and we have valid tx + // and relevant fields changed + // estimate gas + // TODO we might want to listen to gas price changes here + // TODO debunce the call + if ( + !this.state.gasChanged && + this.isValid() && + (this.state.to !== prevState.to || + this.state.value !== prevState.value || + this.state.unit !== prevState.unit || + this.state.data !== prevState.data) + ) { + this.estimateGas(); + } + } + render() { const unlocked = !!this.props.wallet; const unitReadable = 'UNITREADABLE'; @@ -238,8 +268,63 @@ export class SendTransaction extends React.Component { return { to, data, value, unit, gasLimit, readOnly }; } + isValid() { + const { to, value } = this.state; + return ( + isValidETHAddress(to) && + value && + Number(value) > 0 && + !isNaN(Number(value)) && + isFinite(Number(value)) + ); + } + + // FIXME MOVE ME + getTransactionFromState(): ?TransactionWithoutGas { + // FIXME add gas price + if (this.state.unit === 'ether') { + return { + to: this.state.to, + from: this.props.wallet.getAddress(), + // gasPrice: `0x${new Number(50 * 1000000000).toString(16)}`, + value: valueToHex(this.state.value) + }; + } + const token = this.props.tokens.find(x => x.symbol === this.state.unit); + if (!token) { + return; + } + + return { + to: token.address, + from: this.props.wallet.getAddress(), + // gasPrice: `0x${new Number(50 * 1000000000).toString(16)}`, + value: '0x0', + data: ERC20.transfer( + this.state.to, + new Big(this.state.value).times(new Big(10).pow(token.decimal)) + ) + }; + } + + estimateGas() { + const trans = this.getTransactionFromState(); + if (!trans) { + return; + } + + // Grab a reference to state. If it has changed by the time the estimateGas + // call comes back, we don't want to replace the gasLimit in state. + const state = this.state; + + this.props.node.estimateGas(trans).then(gasLimit => { + if (this.state === state) { + this.setState({ gasLimit: formatGasLimit(gasLimit, state.unit) }); + } + }); + } + // FIXME use mkTx instead or something that could take care of default gas/data and whatnot, - // FIXME also should it reset gasChanged? onNewTx = ( address: string, amount: string, @@ -302,6 +387,8 @@ function mapStateToProps(state: AppState) { return { wallet: state.wallet.inst, balance: state.wallet.balance, + node: getNodeLib(state), + tokens: getTokens(state), tokenBalances: getTokenBalances(state) }; } diff --git a/common/libs/contract.js b/common/libs/contract.js new file mode 100644 index 00000000..d9425767 --- /dev/null +++ b/common/libs/contract.js @@ -0,0 +1,89 @@ +// @flow +// TODO support events, constructors, fallbacks, array slots, types +import { sha3, setLengthLeft, toBuffer } from 'ethereumjs-util'; +import Big from 'bignumber.js'; + +type ABIType = 'address' | 'uint256' | 'bool'; + +type ABITypedSlot = { + name: string, + type: ABIType +}; + +type ABIMethod = { + name: string, + type: 'function', + constant: boolean, + inputs: ABITypedSlot[], + outputs: ABITypedSlot[], + // default - false + payable?: boolean +}; + +export type ABI = ABIMethod[]; + +function assertString(arg: any) { + if (typeof arg !== 'string') { + throw new Error('Expected string'); + } +} + +// Contract helper, returns data for given call +export default class Contract { + abi: ABI; + constructor(abi: ABI) { + this.abi = abi; + } + + getMethodAbi(name: string): ABIMethod { + const method = this.abi.find(x => x.name === name); + if (!method) { + throw new Error('Unknown method'); + } + if (method.type !== 'function') { + throw new Error('Not a function'); + } + return method; + } + + call(name: string, args: any[]): string { + const method = this.getMethodAbi(name); + const selector = sha3( + `${name}(${method.inputs.map(i => i.type).join(',')})` + ); + + // TODO: Add explanation, why slice the first 8? + return ( + '0x' + + selector.toString('hex').slice(0, 8) + + this.encodeArgs(method, args) + ); + } + + encodeArgs(method: ABIMethod, args: any[]): string { + if (method.inputs.length !== args.length) { + throw new Error('Invalid number of arguments'); + } + + return method.inputs + .map((input, idx) => this.encodeArg(input, args[idx])) + .join(''); + } + + encodeArg(input: ABITypedSlot, arg: any): string { + switch (input.type) { + case 'address': + case 'uint160': + assertString(arg); + return setLengthLeft(toBuffer(arg), 32).toString('hex'); + case 'uint256': + if (arg instanceof Big) { + arg = '0x' + arg.toString(16); + } + assertString(arg); + return setLengthLeft(toBuffer(arg), 32).toString('hex'); + default: + throw new Error(`Dont know how to handle abi type ${input.type}`); + } + } +} diff --git a/common/libs/erc20.js b/common/libs/erc20.js new file mode 100644 index 00000000..ee76cf10 --- /dev/null +++ b/common/libs/erc20.js @@ -0,0 +1,63 @@ +// @flow +import Contract from 'libs/contract'; +import type { ABI } from 'libs/contract'; +import type Big from 'bignumber.js'; + +const erc20Abi: ABI = [ + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + name: 'balance', + type: 'uint256' + } + ], + payable: false, + type: 'function' + }, + { + constant: false, + inputs: [ + { + name: '_to', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + name: 'transfer', + outputs: [ + { + name: 'success', + type: 'bool' + } + ], + payable: false, + type: 'function' + } +]; + +class ERC20 extends Contract { + constructor() { + super(erc20Abi); + } + + balanceOf(address: string) { + return this.call('balanceOf', [address]); + } + + transfer(to: string, value: Big) { + return this.call('transfer', [to, value]); + } +} + +export default new ERC20(); diff --git a/common/libs/keystore.js b/common/libs/keystore.js index 214e7f1d..ea6636c7 100644 --- a/common/libs/keystore.js +++ b/common/libs/keystore.js @@ -65,7 +65,7 @@ export function pkeyToKeystore( }; } -export function getV3Filename(address) { +export function getV3Filename(address: string) { const ts = new Date(); return ['UTC--', ts.toJSON().replace(/:/g, '-'), '--', address].join(''); } diff --git a/common/libs/nodes/base.js b/common/libs/nodes/base.js index d1a43014..2b61766f 100644 --- a/common/libs/nodes/base.js +++ b/common/libs/nodes/base.js @@ -1,8 +1,18 @@ // @flow -import Big from 'big.js'; +import Big from 'bignumber.js'; +import type { TransactionWithoutGas } from 'libs/transaction'; +import type { Token } from 'config/data'; export default class BaseNode { async getBalance(_address: string): Promise { throw new Error('Implement me'); } + + async getTokenBalances(_address: string, _tokens: Token[]): Promise { + throw new Error('Implement me'); + } + + async estimateGas(_tx: TransactionWithoutGas): Promise { + throw new Error('Implement me'); + } } diff --git a/common/libs/nodes/rpc.js b/common/libs/nodes/rpc.js deleted file mode 100644 index a5ba8da0..00000000 --- a/common/libs/nodes/rpc.js +++ /dev/null @@ -1,80 +0,0 @@ -// @flow -import BaseNode from './base'; -import { randomBytes } from 'crypto'; -import Big from 'big.js'; - -type JsonRpcSuccess = {| - id: string, - result: string -|}; - -type JsonRpcError = {| - error: { - code: string, - message: string, - data?: any - } -|}; - -type JsonRpcResponse = JsonRpcSuccess | JsonRpcError; - -// FIXME -type EthCall = any; - -export default class RPCNode extends BaseNode { - endpoint: string; - constructor(endpoint: string) { - super(); - this.endpoint = endpoint; - } - - async getBalance(address: string): Promise { - return this.post('eth_getBalance', [address, 'pending']).then(response => { - if (response.error) { - throw new Error(response.error.message); - } - // FIXME is this safe? - return new Big(Number(response.result)); - }); - } - - // FIXME extract batching - async ethCall(calls: EthCall[]) { - return this.batchPost( - calls.map(params => { - return { - id: randomBytes(16).toString('hex'), - jsonrpc: '2.0', - method: 'eth_call', - params: [params, 'pending'] - }; - }) - ); - } - - async post(method: string, params: string[]): Promise { - return fetch(this.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - id: randomBytes(16).toString('hex'), - jsonrpc: '2.0', - method, - params - }) - }).then(r => r.json()); - } - - // FIXME - async batchPost(requests: any[]): Promise { - return fetch(this.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(requests) - }).then(r => r.json()); - } -} diff --git a/common/libs/nodes/rpc/client.js b/common/libs/nodes/rpc/client.js new file mode 100644 index 00000000..47a13ced --- /dev/null +++ b/common/libs/nodes/rpc/client.js @@ -0,0 +1,69 @@ +// @flow +import { randomBytes } from 'crypto'; +import { hexEncodeData } from './utils'; +import type { + RPCRequest, + JsonRpcResponse, + CallRequest, + GetBalanceRequest, + EstimateGasRequest +} from './types'; + +// FIXME is it safe to generate that much entropy? +function id(): string { + return randomBytes(16).toString('hex'); +} + +export function estimateGas(transaction: T): EstimateGasRequest { + return { + id: id(), + jsonrpc: '2.0', + method: 'eth_estimateGas', + params: [transaction] + }; +} + +export function getBalance(address: string): GetBalanceRequest { + return { + id: id(), + jsonrpc: '2.0', + method: 'eth_getBalance', + params: [hexEncodeData(address), 'pending'] + }; +} + +export function ethCall(transaction: T): CallRequest { + return { + id: id(), + jsonrpc: '2.0', + method: 'eth_call', + params: [transaction, 'pending'] + }; +} + +export default class RPCClient { + endpoint: string; + constructor(endpoint: string) { + this.endpoint = endpoint; + } + + async call(request: RPCRequest): Promise { + return fetch(this.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(r => r.json()); + } + + async batch(requests: RPCRequest[]): Promise { + return fetch(this.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requests) + }).then(r => r.json()); + } +} diff --git a/common/libs/nodes/rpc/index.js b/common/libs/nodes/rpc/index.js new file mode 100644 index 00000000..3f07a9fd --- /dev/null +++ b/common/libs/nodes/rpc/index.js @@ -0,0 +1,55 @@ +// @flow +import Big from 'bignumber.js'; +import BaseNode from '../base'; +import type { TransactionWithoutGas } from 'libs/transaction'; +import RPCClient, { getBalance, estimateGas, ethCall } from './client'; +import type { Token } from 'config/data'; +import ERC20 from 'libs/erc20'; + +export default class RpcNode extends BaseNode { + client: RPCClient; + constructor(endpoint: string) { + super(); + this.client = new RPCClient(endpoint); + } + + async getBalance(address: string): Promise { + return this.client.call(getBalance(address)).then(response => { + if (response.error) { + throw new Error('getBalance error'); + } + return new Big(Number(response.result)); + }); + } + + async estimateGas(transaction: TransactionWithoutGas): Promise { + return this.client.call(estimateGas(transaction)).then(response => { + if (response.error) { + throw new Error('estimateGas error'); + } + return new Big(Number(response.result)); + }); + } + + async getTokenBalances(address: string, tokens: Token[]): Promise { + const data = ERC20.balanceOf(address); + return this.client + .batch( + tokens.map(t => + ethCall({ + to: t.address, + data + }) + ) + ) + .then(response => { + return response.map((item, idx) => { + // FIXME wrap in maybe-like + if (item.error) { + return new Big(0); + } + return new Big(Number(item.result)).div(new Big(10).pow(tokens[idx].decimal)); + }); + }); + } +} diff --git a/common/libs/nodes/rpc/types.js b/common/libs/nodes/rpc/types.js new file mode 100644 index 00000000..e35833ea --- /dev/null +++ b/common/libs/nodes/rpc/types.js @@ -0,0 +1,62 @@ +// @flow + +type DATA = string; +type QUANTITY = string; + +export type DEFAULT_BLOCK = string | 'earliest' | 'latest' | 'pending'; + +type JsonRpcSuccess = {| + id: string, + result: string +|}; + +type JsonRpcError = {| + error: { + code: string, + message: string, + data?: any + } +|}; + +export type JsonRpcResponse = JsonRpcSuccess | JsonRpcError; + +type RPCRequestBase = { + id: string, + jsonrpc: '2.0' +}; + +export type GetBalanceRequest = RPCRequestBase & { + method: 'eth_getBalance', + params: [DATA, DEFAULT_BLOCK] +}; + +export type CallRequest = RPCRequestBase & { + method: 'eth_call', + params: [ + { + from?: DATA, + to: DATA, + gas?: QUANTITY, + gasPrice?: QUANTITY, + value?: QUANTITY, + data?: DATA + }, + DEFAULT_BLOCK + ] +}; + +export type EstimateGasRequest = RPCRequestBase & { + method: 'eth_estimateGas', + params: [ + { + from?: DATA, + to?: DATA, + gas?: QUANTITY, + gasPrice?: QUANTITY, + value?: QUANTITY, + data?: DATA + } + ] +}; + +export type RPCRequest = GetBalanceRequest | CallRequest | EstimateGasRequest; diff --git a/common/libs/nodes/rpc/utils.js b/common/libs/nodes/rpc/utils.js new file mode 100644 index 00000000..611d6733 --- /dev/null +++ b/common/libs/nodes/rpc/utils.js @@ -0,0 +1,15 @@ +// @flow +// Ref: https://github.com/ethereum/wiki/wiki/JSON-RPC + +import type Big from 'bignumber.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: Big): string { + return '0x' + (value.toString(16) || '0'); +} + +// When encoding UNFORMATTED DATA (byte arrays, account addresses, hashes, bytecode arrays): encode as hex, prefix with "0x", two hex digits per byte. +export function hexEncodeData(value: string | Buffer): string { + return '0x' + toBuffer(value).toString('hex'); +} diff --git a/common/libs/transaction.js b/common/libs/transaction.js new file mode 100644 index 00000000..3b5da6bb --- /dev/null +++ b/common/libs/transaction.js @@ -0,0 +1,14 @@ +// @flow + +export type TransactionWithoutGas = {| + from?: string, + to: string, + gasPrice?: string, + value?: string, + data?: string +|}; + +export type Transaction = {| + ...TransactionWithoutGas, + gas: string +|}; diff --git a/common/libs/units.js b/common/libs/units.js index 0a2eaa58..79897519 100644 --- a/common/libs/units.js +++ b/common/libs/units.js @@ -1,6 +1,6 @@ // @flow -import Big from 'big.js'; +import Big from 'bignumber.js'; const UNITS = { wei: '1', diff --git a/common/libs/values.js b/common/libs/values.js new file mode 100644 index 00000000..976c0d17 --- /dev/null +++ b/common/libs/values.js @@ -0,0 +1,16 @@ +// @flow +import Big from 'bignumber.js'; +import { toWei } from 'libs/units'; + +export function stripHex(address: string): string { + return address.replace('0x', '').toLowerCase(); +} + +export function valueToHex(n: Big | number | string): string { + // Convert it to a Big to handle any and all values. + const big = new Big(n); + // Values are in ether, so convert to wei for RPC calls + const wei = toWei(big, 'ether'); + // Finally, hex it up! + return `0x${wei.toString(16)}`; +} diff --git a/common/libs/wallet/base.js b/common/libs/wallet/base.js index 738b0750..4ba2a644 100644 --- a/common/libs/wallet/base.js +++ b/common/libs/wallet/base.js @@ -1,4 +1,5 @@ // @flow +import { stripHex } from 'libs/values'; export default class BaseWallet { getAddress(): Promise { @@ -8,7 +9,7 @@ export default class BaseWallet { getNakedAddress(): Promise { return new Promise(resolve => { this.getAddress.then(address => { - resolve(address.replace('0x', '').toLowerCase()); + resolve(stripHex(address)); }); }); } diff --git a/common/reducers/wallet.js b/common/reducers/wallet.js index a93ff0f3..054149b5 100644 --- a/common/reducers/wallet.js +++ b/common/reducers/wallet.js @@ -7,7 +7,7 @@ import type { } from 'actions/wallet'; import { BaseWallet } from 'libs/wallet'; import { toEther } from 'libs/units'; -import Big from 'big.js'; +import Big from 'bignumber.js'; export type State = { inst: ?BaseWallet, diff --git a/common/sagas/wallet.js b/common/sagas/wallet.js index df189df5..5d0c0842 100644 --- a/common/sagas/wallet.js +++ b/common/sagas/wallet.js @@ -9,19 +9,6 @@ import { PrivKeyWallet, BaseWallet } from 'libs/wallet'; import { BaseNode } from 'libs/nodes'; import { getNodeLib } from 'selectors/config'; import { getWalletInst, getTokens } from 'selectors/wallet'; -import Big from 'big.js'; - -// FIXME MOVE ME -function padLeft(n: string, width: number, z: string = '0'): string { - return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; -} - -function getEthCallData(to: string, method: string, args: string[]) { - return { - to, - data: method + args.map(a => padLeft(a, 64)).join() - }; -} function* updateAccountBalance() { const node: BaseNode = yield select(getNodeLib); @@ -34,28 +21,21 @@ function* updateAccountBalance() { } function* updateTokenBalances() { - const node = yield select(getNodeLib); + const node: BaseNode = yield select(getNodeLib); const wallet: ?BaseWallet = yield select(getWalletInst); const tokens = yield select(getTokens); - if (!wallet) { + if (!wallet || !node) { return; } - const requests = tokens.map(token => - getEthCallData(token.address, '0x70a08231', [wallet.getNakedAddress()]) - ); // FIXME handle errors - const tokenBalances = yield apply(node, node.ethCall, [requests]); + const tokenBalances = yield apply(node, node.getTokenBalances, [ + wallet.getAddress(), + tokens + ]); yield put( setTokenBalances( tokens.reduce((acc, t, i) => { - // FIXME - if (tokenBalances[i].error || tokenBalances[i].result === '0x') { - return acc; - } - let balance = Big(Number(tokenBalances[i].result)).div( - Big(10).pow(t.decimal) - ); // definitely not safe - acc[t.symbol] = balance; + acc[t.symbol] = tokenBalances[i]; return acc; }, {}) ) diff --git a/common/selectors/wallet.js b/common/selectors/wallet.js index 30fd670b..bacc474d 100644 --- a/common/selectors/wallet.js +++ b/common/selectors/wallet.js @@ -2,7 +2,7 @@ import type { State } from 'reducers'; import { BaseWallet } from 'libs/wallet'; import { getNetworkConfig } from 'selectors/config'; -import Big from 'big.js'; +import Big from 'bignumber.js'; import type { Token } from 'config/data'; export function getWalletInst(state: State): ?BaseWallet { diff --git a/common/utils/formatters.js b/common/utils/formatters.js index 13a60557..a2f92296 100644 --- a/common/utils/formatters.js +++ b/common/utils/formatters.js @@ -1,5 +1,5 @@ // @flow -import Big from 'big.js'; +import Big from 'bignumber.js'; export function toFixedIfLarger(number: number, fixedSize: number = 6): string { return parseFloat(number.toFixed(fixedSize)).toString(); @@ -28,3 +28,26 @@ export function formatNumber(number: Big, digits: number = 3): string { return parts.join('.'); } + +// TODO: Comment up this function to make it clear what's happening here. +export function formatGasLimit(limit: Big, transactionUnit: string = 'ether') { + let limitStr = limit.toString(); + + // I'm guessing this is some known off-by-one-error from the node? + // 21k is only the limit for ethereum though, so make sure they're + // sending ether if we're going to fix it for them. + if (limitStr === '21001' && transactionUnit === 'ether') { + limitStr = '21000'; + } + + // If they've exceeded the gas limit per block, make it -1 + // TODO: Explain why not cap at limit? + // 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)) { + limitStr = '-1'; + } + + return limitStr; +} diff --git a/package-lock.json b/package-lock.json index 9bbb601a..f36dcfc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1524,7 +1524,13 @@ "big.js": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.1.3.tgz", - "integrity": "sha1-TK2iGTZS6zyp7I5VyQFWacmAaXg=" + "integrity": "sha1-TK2iGTZS6zyp7I5VyQFWacmAaXg=", + "dev": true + }, + "bignumber.js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-4.0.2.tgz", + "integrity": "sha1-LR3DfuWWiGfs6pC22k0W5oYI0h0=" }, "bin-build": { "version": "2.2.0", @@ -1549,6 +1555,14 @@ "requires": { "os-tmpdir": "1.0.2", "uuid": "2.0.3" + }, + "dependencies": { + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "dev": true + } } } } @@ -1855,6 +1869,12 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", "dev": true + }, + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "dev": true } } }, @@ -2333,6 +2353,14 @@ "uuid": "2.0.3", "write-file-atomic": "1.3.4", "xdg-basedir": "2.0.0" + }, + "dependencies": { + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "dev": true + } } }, "console-browserify": { @@ -3783,6 +3811,11 @@ "rlp": "2.0.0", "secp256k1": "3.3.0" } + }, + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" } } }, @@ -10897,15 +10930,22 @@ "requires": { "scrypt": "6.0.3", "scryptsy": "1.2.1" + }, + "dependencies": { + "scryptsy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/scryptsy/-/scryptsy-1.2.1.tgz", + "integrity": "sha1-oyJfpLJST4AnAHYeKFW987LZIWM=", + "requires": { + "pbkdf2": "3.0.12" + } + } } }, "scryptsy": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/scryptsy/-/scryptsy-1.2.1.tgz", - "integrity": "sha1-oyJfpLJST4AnAHYeKFW987LZIWM=", - "requires": { - "pbkdf2": "3.0.12" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/scryptsy/-/scryptsy-2.0.0.tgz", + "integrity": "sha1-Jiw28CMc+nZU4jY/o5TNLexm83g=" }, "scss-tokenizer": { "version": "0.2.3", @@ -12195,9 +12235,9 @@ "dev": true }, "uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" }, "v8flags": { "version": "2.1.1", diff --git a/package.json b/package.json index bebd61eb..dde66ddc 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "npm": ">= 5.0.0" }, "dependencies": { - "big.js": "^3.1.3", + "bignumber.js": "^4.0.2", "ethereum-blockies": "git+https://github.com/MyEtherWallet/blockies.git", "ethereumjs-tx": "^1.3.3", "ethereumjs-util": "^5.1.2", diff --git a/spec/libs/nodes/rpc/rpc.spec.js b/spec/libs/nodes/rpc/rpc.spec.js new file mode 100644 index 00000000..24ba7675 --- /dev/null +++ b/spec/libs/nodes/rpc/rpc.spec.js @@ -0,0 +1,41 @@ +// Ref: https://github.com/ethereum/wiki/wiki/JSON-RPC +import { hexEncodeQuantity, hexEncodeData } from 'libs/nodes/rpc/utils'; +import Big from 'bignumber.js'; + +// 0x41 (65 in decimal) +// 0x400 (1024 in decimal) +// WRONG: 0x (should always have at least one digit - zero is "0x0") +// WRONG: 0x0400 (no leading zeroes allowed) +// WRONG: ff (must be prefixed 0x) +describe('hexEncodeQuantity', () => { + it('convert dec to hex', () => { + expect(hexEncodeQuantity(new Big(65))).toEqual('0x41'); + }); + it('should strip leading zeroes', () => { + expect(hexEncodeQuantity(new Big(1024))).toEqual('0x400'); + }); + it('should handle zeroes correctly', () => { + expect(hexEncodeQuantity(new Big(0))).toEqual('0x0'); + }); +}); + +// 0x41 (size 1, "A") +// 0x004200 (size 3, "\0B\0") +// 0x (size 0, "") +// WRONG: 0xf0f0f (must be even number of digits) +// WRONG: 004200 (must be prefixed 0x) +describe('hexEncodeData', () => { + it('encode data to hex', () => { + expect(hexEncodeData(Buffer.from('A'))).toEqual('0x41'); + }); + it('should not strip leading zeroes', () => { + expect(hexEncodeData(Buffer.from('\0B\0'))).toEqual('0x004200'); + }); + it('should handle zero length data correctly', () => { + expect(hexEncodeData(Buffer.from(''))).toEqual('0x'); + }); + it('Can take strings as an input', () => { + expect(hexEncodeData('0xFEED')).toEqual('0xfeed'); + expect(hexEncodeData('FEED')).toEqual('0x46454544'); + }); +}); diff --git a/spec/utils/formatters.spec.js b/spec/utils/formatters.spec.js index 0e1de394..824390d5 100644 --- a/spec/utils/formatters.spec.js +++ b/spec/utils/formatters.spec.js @@ -1,5 +1,9 @@ -import Big from 'big.js'; -import { toFixedIfLarger, formatNumber } from '../../common/utils/formatters'; +import Big from 'bignumber.js'; +import { + toFixedIfLarger, + formatNumber, + formatGasLimit +} from '../../common/utils/formatters'; describe('toFixedIfLarger', () => { it('should return same value if decimal isnt longer than default', () => { @@ -50,3 +54,17 @@ describe('formatNumber', () => { }); }); }); + +describe('formatGasLimit', () => { + it('should fix transaction gas limit off-by-one errors', () => { + expect(formatGasLimit(new Big(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'); + }); + + it('should not alter a valid gas limit', () => { + expect(formatGasLimit(new Big(1234))).toEqual('1234'); + }); +});