mirror of
https://github.com/status-im/MyCrypto.git
synced 2025-02-16 21:16:35 +00:00
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:
parent
3ef2b51a68
commit
7541d6f486
@ -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 = {
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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
89
common/libs/contract.js
Normal 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
63
common/libs/erc20.js
Normal 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();
|
@ -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('');
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
69
common/libs/nodes/rpc/client.js
Normal file
69
common/libs/nodes/rpc/client.js
Normal 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());
|
||||
}
|
||||
}
|
55
common/libs/nodes/rpc/index.js
Normal file
55
common/libs/nodes/rpc/index.js
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
62
common/libs/nodes/rpc/types.js
Normal file
62
common/libs/nodes/rpc/types.js
Normal 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;
|
15
common/libs/nodes/rpc/utils.js
Normal file
15
common/libs/nodes/rpc/utils.js
Normal 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');
|
||||
}
|
14
common/libs/transaction.js
Normal file
14
common/libs/transaction.js
Normal 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
|
||||
|};
|
@ -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
16
common/libs/values.js
Normal 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)}`;
|
||||
}
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}, {})
|
||||
)
|
||||
|
@ -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 {
|
||||
|
@ -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
60
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
41
spec/libs/nodes/rpc/rpc.spec.js
Normal file
41
spec/libs/nodes/rpc/rpc.spec.js
Normal 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');
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user