Estimate gas (WIP) (#102)

* relayout rpc code, start contract helper

* Dont ask for estimate if theres no value

* Split out conversion of ether to wei hex into lib function.

* big.js -> bignumber.js
This commit is contained in:
William O'Beirne 2017-08-07 23:45:08 -04:00 committed by Daniel Ternyak
parent 3ef2b51a68
commit 7541d6f486
26 changed files with 637 additions and 134 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

89
common/libs/contract.js Normal file
View File

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

63
common/libs/erc20.js Normal file
View File

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

View File

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

View File

@ -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<Big> {
throw new Error('Implement me');
}
async getTokenBalances(_address: string, _tokens: Token[]): Promise<Big[]> {
throw new Error('Implement me');
}
async estimateGas(_tx: TransactionWithoutGas): Promise<Big> {
throw new Error('Implement me');
}
}

View File

@ -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<Big> {
return this.post('eth_getBalance', [address, 'pending']).then(response => {
if (response.error) {
throw new Error(response.error.message);
}
// FIXME is this safe?
return new Big(Number(response.result));
});
}
// FIXME extract batching
async ethCall(calls: EthCall[]) {
return this.batchPost(
calls.map(params => {
return {
id: randomBytes(16).toString('hex'),
jsonrpc: '2.0',
method: 'eth_call',
params: [params, 'pending']
};
})
);
}
async post(method: string, params: string[]): Promise<JsonRpcResponse> {
return fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: randomBytes(16).toString('hex'),
jsonrpc: '2.0',
method,
params
})
}).then(r => r.json());
}
// FIXME
async batchPost(requests: any[]): Promise<JsonRpcResponse[]> {
return fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requests)
}).then(r => r.json());
}
}

View File

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

View File

@ -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<Big> {
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<Big> {
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<Big[]> {
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));
});
});
}
}

View File

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

View File

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

View File

@ -0,0 +1,14 @@
// @flow
export type TransactionWithoutGas = {|
from?: string,
to: string,
gasPrice?: string,
value?: string,
data?: string
|};
export type Transaction = {|
...TransactionWithoutGas,
gas: string
|};

View File

@ -1,6 +1,6 @@
// @flow
import Big from 'big.js';
import Big from 'bignumber.js';
const UNITS = {
wei: '1',

16
common/libs/values.js Normal file
View File

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

View File

@ -1,4 +1,5 @@
// @flow
import { stripHex } from 'libs/values';
export default class BaseWallet {
getAddress(): Promise<any> {
@ -8,7 +9,7 @@ export default class BaseWallet {
getNakedAddress(): Promise<any> {
return new Promise(resolve => {
this.getAddress.then(address => {
resolve(address.replace('0x', '').toLowerCase());
resolve(stripHex(address));
});
});
}

View File

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

View File

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

View File

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

View File

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

60
package-lock.json generated
View File

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

View File

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

View File

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

View File

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