WIP: Generating transaction on Send tab (Pt 1) (#100)
* Generating transaction ,placing into read only textareas. * Fix async wallet getAddress cases. * Chain id from network * remove leftover console log * Check balance before generating transaction. * Translate error msgs, check for invalid address. * Errors for gas limit and gas price issues.
This commit is contained in:
parent
c594619acd
commit
9b2156ed4f
|
@ -33,7 +33,6 @@ export class BalanceSidebar extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!this.props.wallet) return;
|
|
||||||
this.props.wallet
|
this.props.wallet
|
||||||
.getAddress()
|
.getAddress()
|
||||||
.then(addr => {
|
.then(addr => {
|
||||||
|
@ -48,6 +47,7 @@ export class BalanceSidebar extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { wallet, balance, network, tokenBalances, rates } = this.props;
|
const { wallet, balance, network, tokenBalances, rates } = this.props;
|
||||||
const { blockExplorer, tokenExplorer } = network;
|
const { blockExplorer, tokenExplorer } = network;
|
||||||
|
const { address } = this.state;
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -58,9 +58,9 @@ export class BalanceSidebar extends React.Component {
|
||||||
{translate('sidebar_AccountAddr')}
|
{translate('sidebar_AccountAddr')}
|
||||||
</h5>
|
</h5>
|
||||||
<ul className="account-info">
|
<ul className="account-info">
|
||||||
<Identicon address={this.state.address} />
|
<Identicon address={address} />
|
||||||
<span className="mono wrap">
|
<span className="mono wrap">
|
||||||
{this.state.address}
|
{address}
|
||||||
</span>
|
</span>
|
||||||
</ul>
|
</ul>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -96,10 +96,7 @@ export class BalanceSidebar extends React.Component {
|
||||||
{!!blockExplorer &&
|
{!!blockExplorer &&
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={blockExplorer.address.replace(
|
href={blockExplorer.address.replace('[[address]]', address)}
|
||||||
'[[address]]',
|
|
||||||
this.state.address
|
|
||||||
)}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{`${network.name} (${blockExplorer.name})`}
|
{`${network.name} (${blockExplorer.name})`}
|
||||||
|
@ -108,10 +105,7 @@ export class BalanceSidebar extends React.Component {
|
||||||
{!!tokenExplorer &&
|
{!!tokenExplorer &&
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={tokenExplorer.address.replace(
|
href={tokenExplorer.address.replace('[[address]]', address)}
|
||||||
'[[address]]',
|
|
||||||
this.state.address
|
|
||||||
)}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{`Tokens (${tokenExplorer.name})`}
|
{`Tokens (${tokenExplorer.name})`}
|
||||||
|
@ -187,7 +181,7 @@ export class BalanceSidebar extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: State, props: Props) {
|
function mapStateToProps(state: State) {
|
||||||
return {
|
return {
|
||||||
wallet: getWalletInst(state),
|
wallet: getWalletInst(state),
|
||||||
balance: state.wallet.balance,
|
balance: state.wallet.balance,
|
||||||
|
|
|
@ -75,6 +75,6 @@ export default class GasPriceDropdown extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
updateGasPrice = (e: SyntheticInputEvent) => {
|
updateGasPrice = (e: SyntheticInputEvent) => {
|
||||||
this.props.onChange(e.currentTarget.valueAsNumber);
|
this.props.onChange(parseInt(e.target.value, 10));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,9 @@ export default class UnitDropdown extends React.Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
expanded: false
|
expanded: false
|
||||||
});
|
});
|
||||||
this.props.onChange(value);
|
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
import { UnlockHeader } from 'components/ui';
|
import { UnlockHeader } from 'components/ui';
|
||||||
import {
|
import {
|
||||||
|
@ -21,27 +20,40 @@ import BaseWallet from 'libs/wallet/base';
|
||||||
import customMessages from './messages';
|
import customMessages from './messages';
|
||||||
import { donationAddressMap } from 'config/data';
|
import { donationAddressMap } from 'config/data';
|
||||||
import { isValidETHAddress } from 'libs/validators';
|
import { isValidETHAddress } from 'libs/validators';
|
||||||
import { getNodeLib } from 'selectors/config';
|
import {
|
||||||
|
getNodeLib,
|
||||||
|
getNetworkConfig,
|
||||||
|
getGasPriceGwei
|
||||||
|
} from 'selectors/config';
|
||||||
import { getTokens } from 'selectors/wallet';
|
import { getTokens } from 'selectors/wallet';
|
||||||
import type { BaseNode } from 'libs/nodes';
|
import type { Token, NetworkConfig } from 'config/data';
|
||||||
import type { Token } from 'config/data';
|
|
||||||
import Big from 'bignumber.js';
|
import Big from 'bignumber.js';
|
||||||
import { valueToHex } from 'libs/values';
|
import { valueToHex } from 'libs/values';
|
||||||
import ERC20 from 'libs/erc20';
|
import ERC20 from 'libs/erc20';
|
||||||
import type { TokenBalance } from 'selectors/wallet';
|
import type { TokenBalance } from 'selectors/wallet';
|
||||||
import { getTokenBalances } from 'selectors/wallet';
|
import { getTokenBalances } from 'selectors/wallet';
|
||||||
import type { TransactionWithoutGas } from 'libs/transaction';
|
import type { RPCNode } from 'libs/nodes';
|
||||||
|
import type {
|
||||||
|
TransactionWithoutGas,
|
||||||
|
BroadcastTransaction
|
||||||
|
} from 'libs/transaction';
|
||||||
|
import type { UNIT } from 'libs/units';
|
||||||
|
import { toWei } from 'libs/units';
|
||||||
import { formatGasLimit } from 'utils/formatters';
|
import { formatGasLimit } from 'utils/formatters';
|
||||||
|
import { showNotification } from 'actions/notifications';
|
||||||
|
import type { ShowNotificationAction } from 'actions/notifications';
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
hasQueryString: boolean,
|
hasQueryString: boolean,
|
||||||
readOnly: boolean,
|
readOnly: boolean,
|
||||||
to: string,
|
to: string,
|
||||||
value: string,
|
value: string,
|
||||||
unit: string,
|
// $FlowFixMe - Comes from getParam not validating unit
|
||||||
|
unit: UNIT,
|
||||||
gasLimit: string,
|
gasLimit: string,
|
||||||
data: string,
|
data: string,
|
||||||
gasChanged: boolean
|
gasChanged: boolean,
|
||||||
|
transaction: ?BroadcastTransaction
|
||||||
};
|
};
|
||||||
|
|
||||||
function getParam(query: { [string]: string }, key: string) {
|
function getParam(query: { [string]: string }, key: string) {
|
||||||
|
@ -65,9 +77,16 @@ type Props = {
|
||||||
},
|
},
|
||||||
wallet: BaseWallet,
|
wallet: BaseWallet,
|
||||||
balance: Big,
|
balance: Big,
|
||||||
node: BaseNode,
|
nodeLib: RPCNode,
|
||||||
|
network: NetworkConfig,
|
||||||
tokens: Token[],
|
tokens: Token[],
|
||||||
tokenBalances: TokenBalance[]
|
tokenBalances: TokenBalance[],
|
||||||
|
gasPrice: number,
|
||||||
|
showNotification: (
|
||||||
|
level: string,
|
||||||
|
msg: string,
|
||||||
|
duration?: number
|
||||||
|
) => ShowNotificationAction
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SendTransaction extends React.Component {
|
export class SendTransaction extends React.Component {
|
||||||
|
@ -81,7 +100,8 @@ export class SendTransaction extends React.Component {
|
||||||
unit: 'ether',
|
unit: 'ether',
|
||||||
gasLimit: '21000',
|
gasLimit: '21000',
|
||||||
data: '',
|
data: '',
|
||||||
gasChanged: false
|
gasChanged: false,
|
||||||
|
transaction: null
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -112,8 +132,6 @@ export class SendTransaction extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const unlocked = !!this.props.wallet;
|
const unlocked = !!this.props.wallet;
|
||||||
const unitReadable = 'UNITREADABLE';
|
|
||||||
const nodeUnit = 'NODEUNIT';
|
|
||||||
const hasEnoughBalance = false;
|
const hasEnoughBalance = false;
|
||||||
const {
|
const {
|
||||||
to,
|
to,
|
||||||
|
@ -122,7 +140,8 @@ export class SendTransaction extends React.Component {
|
||||||
gasLimit,
|
gasLimit,
|
||||||
data,
|
data,
|
||||||
readOnly,
|
readOnly,
|
||||||
hasQueryString
|
hasQueryString,
|
||||||
|
transaction
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const customMessage = customMessages.find(m => m.to === to);
|
const customMessage = customMessages.find(m => m.to === to);
|
||||||
|
|
||||||
|
@ -213,17 +232,23 @@ export class SendTransaction extends React.Component {
|
||||||
<label>
|
<label>
|
||||||
{translate('SEND_raw')}
|
{translate('SEND_raw')}
|
||||||
</label>
|
</label>
|
||||||
<textarea className="form-control" rows="4" readOnly>
|
<textarea
|
||||||
{'' /*rawTx*/}
|
className="form-control"
|
||||||
</textarea>
|
value={transaction ? transaction.rawTx : ''}
|
||||||
|
rows="4"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-6">
|
<div className="col-sm-6">
|
||||||
<label>
|
<label>
|
||||||
{translate('SEND_signed')}
|
{translate('SEND_signed')}
|
||||||
</label>
|
</label>
|
||||||
<textarea className="form-control" rows="4" readOnly>
|
<textarea
|
||||||
{'' /*signedTx*/}
|
className="form-control"
|
||||||
</textarea>
|
value={transaction ? transaction.signedTx : ''}
|
||||||
|
rows="4"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -279,26 +304,24 @@ export class SendTransaction extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME MOVE ME
|
async getTransactionFromState(): Promise<TransactionWithoutGas> {
|
||||||
getTransactionFromState(): ?TransactionWithoutGas {
|
const { wallet } = this.props;
|
||||||
// FIXME add gas price
|
|
||||||
if (this.state.unit === 'ether') {
|
if (this.state.unit === 'ether') {
|
||||||
return {
|
return {
|
||||||
to: this.state.to,
|
to: this.state.to,
|
||||||
from: this.props.wallet.getAddress(),
|
from: await wallet.getAddress(),
|
||||||
// gasPrice: `0x${new Number(50 * 1000000000).toString(16)}`,
|
|
||||||
value: valueToHex(this.state.value)
|
value: valueToHex(this.state.value)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const token = this.props.tokens.find(x => x.symbol === this.state.unit);
|
const token = this.props.tokens.find(x => x.symbol === this.state.unit);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
throw new Error('No matching token');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
to: token.address,
|
to: token.address,
|
||||||
from: this.props.wallet.getAddress(),
|
from: await wallet.getAddress(),
|
||||||
// gasPrice: `0x${new Number(50 * 1000000000).toString(16)}`,
|
|
||||||
value: '0x0',
|
value: '0x0',
|
||||||
data: ERC20.transfer(
|
data: ERC20.transfer(
|
||||||
this.state.to,
|
this.state.to,
|
||||||
|
@ -307,8 +330,8 @@ export class SendTransaction extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
estimateGas() {
|
async estimateGas() {
|
||||||
const trans = this.getTransactionFromState();
|
const trans = await this.getTransactionFromState();
|
||||||
if (!trans) {
|
if (!trans) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -317,7 +340,7 @@ export class SendTransaction extends React.Component {
|
||||||
// call comes back, we don't want to replace the gasLimit in state.
|
// call comes back, we don't want to replace the gasLimit in state.
|
||||||
const state = this.state;
|
const state = this.state;
|
||||||
|
|
||||||
this.props.node.estimateGas(trans).then(gasLimit => {
|
this.props.nodeLib.estimateGas(trans).then(gasLimit => {
|
||||||
if (this.state === state) {
|
if (this.state === state) {
|
||||||
this.setState({ gasLimit: formatGasLimit(gasLimit, state.unit) });
|
this.setState({ gasLimit: formatGasLimit(gasLimit, state.unit) });
|
||||||
}
|
}
|
||||||
|
@ -381,16 +404,42 @@ export class SendTransaction extends React.Component {
|
||||||
unit
|
unit
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
generateTx = async () => {
|
||||||
|
const { nodeLib, wallet } = this.props;
|
||||||
|
const address = await wallet.getAddress();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transaction = await nodeLib.generateTransaction(
|
||||||
|
{
|
||||||
|
to: this.state.to,
|
||||||
|
from: address,
|
||||||
|
value: this.state.value,
|
||||||
|
gasLimit: this.state.gasLimit,
|
||||||
|
gasPrice: this.props.gasPrice,
|
||||||
|
data: this.state.data,
|
||||||
|
chainId: this.props.network.chainId
|
||||||
|
},
|
||||||
|
wallet
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setState({ transaction });
|
||||||
|
} catch (err) {
|
||||||
|
this.props.showNotification('danger', err.message, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: AppState) {
|
function mapStateToProps(state: AppState) {
|
||||||
return {
|
return {
|
||||||
wallet: state.wallet.inst,
|
wallet: state.wallet.inst,
|
||||||
balance: state.wallet.balance,
|
balance: state.wallet.balance,
|
||||||
node: getNodeLib(state),
|
tokenBalances: getTokenBalances(state),
|
||||||
|
nodeLib: getNodeLib(state),
|
||||||
|
network: getNetworkConfig(state),
|
||||||
tokens: getTokens(state),
|
tokens: getTokens(state),
|
||||||
tokenBalances: getTokenBalances(state)
|
gasPrice: toWei(new Big(getGasPriceGwei(state)), 'gwei')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(SendTransaction);
|
export default connect(mapStateToProps, { showNotification })(SendTransaction);
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Big from 'bignumber.js';
|
import Big from 'bignumber.js';
|
||||||
import type { TransactionWithoutGas } from 'libs/transaction';
|
import type {
|
||||||
|
TransactionWithoutGas,
|
||||||
|
Transaction,
|
||||||
|
BroadcastTransaction
|
||||||
|
} from 'libs/transaction';
|
||||||
import type { Token } from 'config/data';
|
import type { Token } from 'config/data';
|
||||||
|
import type { BaseWallet } from 'libs/wallet';
|
||||||
|
|
||||||
export default class BaseNode {
|
export default class BaseNode {
|
||||||
async getBalance(_address: string): Promise<Big> {
|
async getBalance(_address: string): Promise<Big> {
|
||||||
|
@ -15,4 +20,11 @@ export default class BaseNode {
|
||||||
async estimateGas(_tx: TransactionWithoutGas): Promise<Big> {
|
async estimateGas(_tx: TransactionWithoutGas): Promise<Big> {
|
||||||
throw new Error('Implement me');
|
throw new Error('Implement me');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateTransaction(
|
||||||
|
_tx: Transaction,
|
||||||
|
_wallet: BaseWallet
|
||||||
|
): Promise<BroadcastTransaction> {
|
||||||
|
throw new Error('Implement me');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ import type {
|
||||||
JsonRpcResponse,
|
JsonRpcResponse,
|
||||||
CallRequest,
|
CallRequest,
|
||||||
GetBalanceRequest,
|
GetBalanceRequest,
|
||||||
EstimateGasRequest
|
EstimateGasRequest,
|
||||||
|
GetTransactionCountRequest
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
// FIXME is it safe to generate that much entropy?
|
// FIXME is it safe to generate that much entropy?
|
||||||
|
@ -41,6 +42,17 @@ export function ethCall<T: *>(transaction: T): CallRequest {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTransactionCount(
|
||||||
|
address: string
|
||||||
|
): GetTransactionCountRequest {
|
||||||
|
return {
|
||||||
|
id: id(),
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method: 'eth_getTransactionCount',
|
||||||
|
params: [address, 'pending']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default class RPCClient {
|
export default class RPCClient {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
constructor(endpoint: string) {
|
constructor(endpoint: string) {
|
||||||
|
|
|
@ -1,10 +1,24 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Big from 'bignumber.js';
|
import Big from 'bignumber.js';
|
||||||
|
import { addHexPrefix } from 'ethereumjs-util';
|
||||||
|
import translate from 'translations';
|
||||||
import BaseNode from '../base';
|
import BaseNode from '../base';
|
||||||
import type { TransactionWithoutGas } from 'libs/transaction';
|
import type {
|
||||||
import RPCClient, { getBalance, estimateGas, ethCall } from './client';
|
TransactionWithoutGas,
|
||||||
|
Transaction,
|
||||||
|
BroadcastTransaction
|
||||||
|
} from 'libs/transaction';
|
||||||
|
import RPCClient, {
|
||||||
|
getBalance,
|
||||||
|
estimateGas,
|
||||||
|
ethCall,
|
||||||
|
getTransactionCount
|
||||||
|
} from './client';
|
||||||
import type { Token } from 'config/data';
|
import type { Token } from 'config/data';
|
||||||
import ERC20 from 'libs/erc20';
|
import ERC20 from 'libs/erc20';
|
||||||
|
import type { BaseWallet } from 'libs/wallet';
|
||||||
|
import { toWei } from 'libs/units';
|
||||||
|
import { isValidETHAddress } from 'libs/validators';
|
||||||
|
|
||||||
export default class RpcNode extends BaseNode {
|
export default class RpcNode extends BaseNode {
|
||||||
client: RPCClient;
|
client: RPCClient;
|
||||||
|
@ -48,8 +62,94 @@ export default class RpcNode extends BaseNode {
|
||||||
if (item.error) {
|
if (item.error) {
|
||||||
return new Big(0);
|
return new Big(0);
|
||||||
}
|
}
|
||||||
return new Big(Number(item.result)).div(new Big(10).pow(tokens[idx].decimal));
|
return new Big(Number(item.result)).div(
|
||||||
|
new Big(10).pow(tokens[idx].decimal)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateTransaction(
|
||||||
|
tx: Transaction,
|
||||||
|
wallet: BaseWallet
|
||||||
|
): Promise<BroadcastTransaction> {
|
||||||
|
// Reject bad addresses
|
||||||
|
if (!isValidETHAddress(tx.to)) {
|
||||||
|
return Promise.reject(new Error(translate('ERROR_5')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject gas limit under 21000 (Minimum for transaction)
|
||||||
|
// Reject if limit over 5000000
|
||||||
|
// TODO: Make this dynamic, the limit shifts
|
||||||
|
const limitBig = new Big(tx.gasLimit);
|
||||||
|
if (limitBig.lessThan(21000)) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
translate('Gas limit must be at least 21000 for transactions')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limitBig.greaterThan(5000000)) {
|
||||||
|
return Promise.reject(new Error(translate('GETH_GasLimit')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject gas over 1000gwei (1000000000000)
|
||||||
|
const priceBig = new Big(tx.gasPrice);
|
||||||
|
if (priceBig.greaterThan(new Big('1000000000000'))) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
'Gas price too high. Please contact support if this was not a mistake.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const calls = [getBalance(tx.from), getTransactionCount(tx.from)];
|
||||||
|
|
||||||
|
return this.client.batch(calls).then(async results => {
|
||||||
|
const [balance, txCount] = results;
|
||||||
|
|
||||||
|
if (balance.error) {
|
||||||
|
throw new Error(`Failed to retrieve balance for ${tx.from}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txCount.error) {
|
||||||
|
throw new Error(`Failed to retrieve transaction count for ${tx.from}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle token values
|
||||||
|
const valueWei = new Big(toWei(new Big(tx.value), 'ether'));
|
||||||
|
const balanceWei = new Big(balance.result);
|
||||||
|
if (valueWei.gte(balanceWei)) {
|
||||||
|
throw new Error(translate('GETH_Balance'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawTx = {
|
||||||
|
nonce: addHexPrefix(txCount.result),
|
||||||
|
gasPrice: addHexPrefix(new Big(tx.gasPrice).toString(16)),
|
||||||
|
gasLimit: addHexPrefix(new Big(tx.gasLimit).toString(16)),
|
||||||
|
to: addHexPrefix(tx.to),
|
||||||
|
value: addHexPrefix(valueWei.toString(16)),
|
||||||
|
data: tx.data ? addHexPrefix(tx.data) : '',
|
||||||
|
chainId: tx.chainId || 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawTxJson = JSON.stringify(rawTx);
|
||||||
|
const signedTx = await wallet.signRawTransaction(rawTx);
|
||||||
|
|
||||||
|
// Repeat all of this shit for Flow typechecking. Sealed objects don't
|
||||||
|
// like spreads, so we have to be explicit.
|
||||||
|
return {
|
||||||
|
nonce: rawTx.nonce,
|
||||||
|
gasPrice: rawTx.gasPrice,
|
||||||
|
gasLimit: rawTx.gasLimit,
|
||||||
|
to: rawTx.to,
|
||||||
|
value: rawTx.value,
|
||||||
|
data: rawTx.data,
|
||||||
|
chainId: rawTx.chainId,
|
||||||
|
rawTx: rawTxJson,
|
||||||
|
signedTx: signedTx
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,4 +59,13 @@ export type EstimateGasRequest = RPCRequestBase & {
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RPCRequest = GetBalanceRequest | CallRequest | EstimateGasRequest;
|
export type GetTransactionCountRequest = RPCRequestBase & {
|
||||||
|
method: 'eth_getTransactionCount',
|
||||||
|
params: [DATA, DEFAULT_BLOCK]
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RPCRequest =
|
||||||
|
| GetBalanceRequest
|
||||||
|
| CallRequest
|
||||||
|
| EstimateGasRequest
|
||||||
|
| GetTransactionCountRequest;
|
||||||
|
|
|
@ -3,9 +3,12 @@
|
||||||
import EthTx from 'ethereumjs-tx';
|
import EthTx from 'ethereumjs-tx';
|
||||||
import { sha3, ecsign } from 'ethereumjs-util';
|
import { sha3, ecsign } from 'ethereumjs-util';
|
||||||
import { isValidRawTx } from 'libs/validators';
|
import { isValidRawTx } from 'libs/validators';
|
||||||
import type { RawTx } from 'libs/validators';
|
import type { RawTransaction } from 'libs/transaction';
|
||||||
|
|
||||||
export function signRawTxWithPrivKey(privKey: Buffer, rawTx: RawTx): string {
|
export function signRawTxWithPrivKey(
|
||||||
|
privKey: Buffer,
|
||||||
|
rawTx: RawTransaction
|
||||||
|
): string {
|
||||||
if (!isValidRawTx(rawTx)) {
|
if (!isValidRawTx(rawTx)) {
|
||||||
throw new Error('Invalid raw transaction');
|
throw new Error('Invalid raw transaction');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,31 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
export type TransactionWithoutGas = {|
|
export type TransactionWithoutGas = {|
|
||||||
from?: string,
|
from: string,
|
||||||
to: string,
|
to: string,
|
||||||
gasPrice?: string,
|
gasLimit?: string | number,
|
||||||
value?: string,
|
value: string | number,
|
||||||
data?: string
|
data?: string,
|
||||||
|
chainId?: number
|
||||||
|};
|
|};
|
||||||
|
|
||||||
export type Transaction = {|
|
export type Transaction = {|
|
||||||
...TransactionWithoutGas,
|
...TransactionWithoutGas,
|
||||||
gas: string
|
gasPrice: string | number
|
||||||
|
|};
|
||||||
|
|
||||||
|
export type RawTransaction = {|
|
||||||
|
nonce: string,
|
||||||
|
gasPrice: string,
|
||||||
|
gasLimit: string,
|
||||||
|
to: string,
|
||||||
|
value: string,
|
||||||
|
data: string,
|
||||||
|
chainId: number
|
||||||
|
|};
|
||||||
|
|
||||||
|
export type BroadcastTransaction = {|
|
||||||
|
...RawTransaction,
|
||||||
|
rawTx: string,
|
||||||
|
signedTx: string
|
||||||
|};
|
|};
|
||||||
|
|
|
@ -29,7 +29,7 @@ const UNITS = {
|
||||||
tether: '1000000000000000000000000000000'
|
tether: '1000000000000000000000000000000'
|
||||||
};
|
};
|
||||||
|
|
||||||
type UNIT = $Keys<typeof UNITS>;
|
export type UNIT = $Keys<typeof UNITS>;
|
||||||
|
|
||||||
function getValueOfUnit(unit: UNIT) {
|
function getValueOfUnit(unit: UNIT) {
|
||||||
return new Big(UNITS[unit]);
|
return new Big(UNITS[unit]);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import WalletAddressValidator from 'wallet-address-validator';
|
import WalletAddressValidator from 'wallet-address-validator';
|
||||||
import { normalise } from './ens';
|
import { normalise } from './ens';
|
||||||
import { toChecksumAddress } from 'ethereumjs-util';
|
import { toChecksumAddress } from 'ethereumjs-util';
|
||||||
|
import type { RawTransaction } from 'libs/transaction';
|
||||||
|
|
||||||
export function isValidETHAddress(address: string): boolean {
|
export function isValidETHAddress(address: string): boolean {
|
||||||
if (!address) {
|
if (!address) {
|
||||||
|
@ -85,17 +86,7 @@ export function isPositiveIntegerOrZero(number: number): boolean {
|
||||||
return number >= 0 && parseInt(number) === number;
|
return number >= 0 && parseInt(number) === number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RawTx = {
|
export function isValidRawTx(rawTx: RawTransaction): boolean {
|
||||||
nonce: string,
|
|
||||||
gasPrice: string,
|
|
||||||
gasLimit: string,
|
|
||||||
to: string,
|
|
||||||
value: string,
|
|
||||||
data: string,
|
|
||||||
chainId: number
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isValidRawTx(rawTx: RawTx): boolean {
|
|
||||||
const propReqs = [
|
const propReqs = [
|
||||||
{ name: 'nonce', type: 'string', lenReq: true },
|
{ name: 'nonce', type: 'string', lenReq: true },
|
||||||
{ name: 'gasPrice', type: 'string', lenReq: true },
|
{ name: 'gasPrice', type: 'string', lenReq: true },
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { stripHex } from 'libs/values';
|
import { stripHex } from 'libs/values';
|
||||||
|
import type { RawTransaction } from 'libs/transaction';
|
||||||
|
|
||||||
export default class BaseWallet {
|
export default class BaseWallet {
|
||||||
getAddress(): Promise<any> {
|
getAddress(): Promise<string> {
|
||||||
return Promise.reject('Implement me');
|
return Promise.reject('Implement me');
|
||||||
}
|
}
|
||||||
|
|
||||||
getNakedAddress(): Promise<any> {
|
getNakedAddress(): Promise<string> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
this.getAddress().then(address => {
|
this.getAddress().then(address => {
|
||||||
resolve(stripHex(address));
|
resolve(stripHex(address));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signRawTransaction(_tx: RawTransaction): Promise<string> {
|
||||||
|
return Promise.reject('Implement me');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { pkeyToKeystore } from 'libs/keystore';
|
import { pkeyToKeystore } from 'libs/keystore';
|
||||||
import { signRawTxWithPrivKey, signMessageWithPrivKey } from 'libs/signing';
|
import { signRawTxWithPrivKey, signMessageWithPrivKey } from 'libs/signing';
|
||||||
import type { RawTx } from 'libs/validators';
|
import type { RawTransaction } from 'libs/transaction';
|
||||||
|
|
||||||
export default class PrivKeyWallet extends BaseWallet {
|
export default class PrivKeyWallet extends BaseWallet {
|
||||||
privKey: Buffer;
|
privKey: Buffer;
|
||||||
|
@ -21,7 +21,7 @@ export default class PrivKeyWallet extends BaseWallet {
|
||||||
this.address = publicToAddress(this.pubKey);
|
this.address = publicToAddress(this.pubKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddress(): Promise<any> {
|
getAddress(): Promise<string> {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
toChecksumAddress(`0x${this.address.toString('hex')}`)
|
toChecksumAddress(`0x${this.address.toString('hex')}`)
|
||||||
);
|
);
|
||||||
|
@ -47,7 +47,7 @@ export default class PrivKeyWallet extends BaseWallet {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
signRawTransaction(rawTx: RawTx): Promise<any> {
|
signRawTransaction(rawTx: RawTransaction): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
resolve(signRawTxWithPrivKey(this.privKey, rawTx));
|
resolve(signRawTxWithPrivKey(this.privKey, rawTx));
|
||||||
|
|
|
@ -19,3 +19,7 @@ export function getNetworkConfig(state: State): NetworkConfig {
|
||||||
export function getNetworkContracts(state: State): ?Array<NetworkContract> {
|
export function getNetworkContracts(state: State): ?Array<NetworkContract> {
|
||||||
return getNetworkConfig(state).contracts;
|
return getNetworkConfig(state).contracts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getGasPriceGwei(state: State): number {
|
||||||
|
return state.config.gasPriceGwei;
|
||||||
|
}
|
||||||
|
|
|
@ -3771,6 +3771,20 @@
|
||||||
"ethereum-blockies": {
|
"ethereum-blockies": {
|
||||||
"version": "git+https://github.com/MyEtherWallet/blockies.git#c0d0b782e77e17228f6fe1c387b956987e45b670"
|
"version": "git+https://github.com/MyEtherWallet/blockies.git#c0d0b782e77e17228f6fe1c387b956987e45b670"
|
||||||
},
|
},
|
||||||
|
"ethereum-common": {
|
||||||
|
"version": "0.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz",
|
||||||
|
"integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8="
|
||||||
|
},
|
||||||
|
"ethereumjs-tx": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.3.tgz",
|
||||||
|
"integrity": "sha1-7OBR0+/b53GtKlGNYWMsoqt17Ls=",
|
||||||
|
"requires": {
|
||||||
|
"ethereum-common": "0.0.18",
|
||||||
|
"ethereumjs-util": "5.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ethereumjs-util": {
|
"ethereumjs-util": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.2.tgz",
|
||||||
|
|
|
@ -2,24 +2,26 @@ import {
|
||||||
isValidBTCAddress,
|
isValidBTCAddress,
|
||||||
isValidETHAddress
|
isValidETHAddress
|
||||||
} from '../../common/libs/validators';
|
} from '../../common/libs/validators';
|
||||||
import { donationAddressMap } from '../../common/config/data';
|
|
||||||
|
const VALID_BTC_ADDRESS = '1MEWT2SGbqtz6mPCgFcnea8XmWV5Z4Wc6';
|
||||||
|
const VALID_ETH_ADDRESS = '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8';
|
||||||
|
|
||||||
describe('Validator', () => {
|
describe('Validator', () => {
|
||||||
it('should validate correct BTC address as true', () => {
|
it('should validate correct BTC address as true', () => {
|
||||||
expect(isValidBTCAddress(donationAddressMap.BTC)).toBeTruthy();
|
expect(isValidBTCAddress(VALID_BTC_ADDRESS)).toBeTruthy();
|
||||||
});
|
});
|
||||||
it('should validate incorrect BTC address as false', () => {
|
it('should validate incorrect BTC address as false', () => {
|
||||||
expect(
|
expect(
|
||||||
isValidBTCAddress('nonsense' + donationAddressMap.BTC + 'nonsense')
|
isValidBTCAddress('nonsense' + VALID_BTC_ADDRESS + 'nonsense')
|
||||||
).toBeFalsy();
|
).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate correct ETH address as true', () => {
|
it('should validate correct ETH address as true', () => {
|
||||||
expect(isValidETHAddress(donationAddressMap.ETH)).toBeTruthy();
|
expect(isValidETHAddress(VALID_ETH_ADDRESS)).toBeTruthy();
|
||||||
});
|
});
|
||||||
it('should validate incorrect ETH address as false', () => {
|
it('should validate incorrect ETH address as false', () => {
|
||||||
expect(
|
expect(
|
||||||
isValidETHAddress('nonsense' + donationAddressMap.ETH + 'nonsense')
|
isValidETHAddress('nonsense' + VALID_ETH_ADDRESS + 'nonsense')
|
||||||
).toBeFalsy();
|
).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue