Refactor Send (#164)

* refactor SendTransaction component and transaction helper lib

* move transaction generation out of component; refactor tx types and simply tx generation

* remove commented out try/catch

* address todo; rename function/types

* fix imports and address comments
This commit is contained in:
Daniel Ternyak 2017-09-05 16:28:19 -05:00 committed by GitHub
parent 0989424d73
commit e3d3b2c8e8
9 changed files with 235 additions and 180 deletions

View File

@ -17,7 +17,7 @@ import type { Token, NetworkConfig } from 'config/data';
import Modal from 'components/ui/Modal';
import Identicon from 'components/ui/Identicon';
import Spinner from 'components/ui/Spinner';
import type { BroadcastStatusTransaction } from 'libs/transaction';
import type { BroadcastTransactionStatus } from 'libs/transaction';
type Props = {
signedTx: string,
@ -29,7 +29,7 @@ type Props = {
onConfirm: (string, EthTx) => void,
onClose: () => void,
lang: string,
broadCastStatusTx: BroadcastStatusTransaction
broadCastTxStatus: BroadcastTransactionStatus
};
type State = {
@ -62,7 +62,7 @@ class ConfirmationModal extends React.Component {
componentDidUpdate() {
if (
this.state.hasBroadCasted &&
!this.props.broadCastStatusTx.isBroadcasting
!this.props.broadCastTxStatus.isBroadcasting
) {
this.props.onClose();
}
@ -124,7 +124,7 @@ class ConfirmationModal extends React.Component {
};
render() {
const { node, token, network, onClose, broadCastStatusTx } = this.props;
const { node, token, network, onClose, broadCastTxStatus } = this.props;
const { fromAddress, timeToRead } = this.state;
const { toAddress, value, gasPrice, data } = this._decodeTransaction();
@ -146,7 +146,7 @@ class ConfirmationModal extends React.Component {
const symbol = token ? token.symbol : network.unit;
const isBroadcasting =
broadCastStatusTx && broadCastStatusTx.isBroadcasting;
broadCastTxStatus && broadCastTxStatus.isBroadcasting;
return (
<Modal
@ -233,7 +233,7 @@ function mapStateToProps(state, props) {
const lang = getLanguageSelection(state);
const broadCastStatusTx = getTxFromState(state, props.signedTx);
const broadCastTxStatus = getTxFromState(state, props.signedTx);
// Determine if we're sending to a token from the transaction to address
const { to, data } = getTransactionFields(transaction);
@ -241,7 +241,7 @@ function mapStateToProps(state, props) {
const token = data && tokens.find(t => t.address === to);
return {
broadCastStatusTx,
broadCastTxStatus,
transaction,
token,
network,

View File

@ -1,7 +1,23 @@
// @flow
import React from 'react';
// UTILS
import { formatGasLimit } from 'utils/formatters';
import translate from 'translations';
import pickBy from 'lodash/pickBy';
// SELECTORS
import { getNodeConfig } from 'selectors/config';
import {
getNodeLib,
getNetworkConfig,
getGasPriceGwei
} from 'selectors/config';
import {
getTokenBalances,
getTxFromBroadcastTransactionStatus
} from 'selectors/wallet';
import { getTokens } from 'selectors/wallet';
import type { TokenBalance } from 'selectors/wallet';
// COMPONENTS
import { UnlockHeader } from 'components/ui';
import {
Donate,
@ -13,45 +29,37 @@ import {
ConfirmationModal
} from './components';
import { BalanceSidebar } from 'components';
import pickBy from 'lodash/pickBy';
import type { State as AppState } from 'reducers';
import { connect } from 'react-redux';
import BaseWallet from 'libs/wallet/base';
// import type { Transaction } from './types';
import customMessages from './messages';
import { donationAddressMap } from 'config/data';
import { isValidETHAddress } from 'libs/validators';
import {
getNodeLib,
getNetworkConfig,
getGasPriceGwei
} from 'selectors/config';
import { getTokens } from 'selectors/wallet';
// CONFIG
import type { NodeConfig } from 'config/data';
import type { Token, NetworkConfig } 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,
getTxFromBroadcastStatusTransactions
} from 'selectors/wallet';
import type { RPCNode } from 'libs/nodes';
import { donationAddressMap } from 'config/data';
// REDUX
import { connect } from 'react-redux';
import type { State as AppState } from 'reducers';
import { broadcastTx } from 'actions/wallet';
import type { BroadcastTxRequestedAction } from 'actions/wallet';
import type { BroadcastStatusTransaction } from 'libs/transaction';
import type {
TransactionWithoutGas,
BroadcastTransaction
} from 'libs/transaction';
import type { UNIT } from 'libs/units';
import { toWei, toTokenUnit } from 'libs/units';
import { formatGasLimit } from 'utils/formatters';
import { showNotification } from 'actions/notifications';
import type { ShowNotificationAction } from 'actions/notifications';
import type { NodeConfig } from 'config/data';
import { getNodeConfig } from 'selectors/config';
import { generateTransaction, getBalanceMinusGasCosts } from 'libs/transaction';
// LIBS
import BaseWallet from 'libs/wallet/base';
import { isValidETHAddress } from 'libs/validators';
import type { RPCNode } from 'libs/nodes';
import type {
BroadcastTransactionStatus,
TransactionInput,
CompleteTransaction
} from 'libs/transaction';
import type { TransactionWithoutGas } from 'libs/messages';
import type { UNIT } from 'libs/units';
import { toWei } from 'libs/units';
import {
generateCompleteTransaction,
getBalanceMinusGasCosts,
formatTxInput
} from 'libs/transaction';
// MISC
import customMessages from './messages';
import Big from 'bignumber.js';
type State = {
hasQueryString: boolean,
@ -65,7 +73,7 @@ type State = {
gasLimit: string,
data: string,
gasChanged: boolean,
transaction: ?BroadcastTransaction,
transaction: ?CompleteTransaction,
showTxConfirm: boolean,
generateDisabled: boolean
};
@ -76,13 +84,9 @@ function getParam(query: { [string]: string }, key: string) {
if (index === -1) {
return null;
}
return query[keys[index]];
}
// TODO query string
// TODO how to handle DATA?
type Props = {
location: {
query: {
@ -96,14 +100,14 @@ type Props = {
network: NetworkConfig,
tokens: Token[],
tokenBalances: TokenBalance[],
gasPrice: number,
gasPrice: string,
broadcastTx: (signedTx: string) => BroadcastTxRequestedAction,
showNotification: (
level: string,
msg: string,
duration?: number
) => ShowNotificationAction,
transactions: Array<BroadcastStatusTransaction>
transactions: Array<BroadcastTransactionStatus>
};
const initialState = {
@ -150,22 +154,21 @@ export class SendTransaction extends React.Component {
this.estimateGas();
}
}
if (this.state.generateDisabled !== !this.isValid()) {
if (this.state.generateDisabled === this.isValid()) {
this.setState({ generateDisabled: !this.isValid() });
}
const componentStateTransaction = this.state.transaction;
if (componentStateTransaction) {
// lives in redux state
const currentTxAsBroadcastTransaction = getTxFromBroadcastStatusTransactions(
const currentTxAsSignedTransaction = getTxFromBroadcastTransactionStatus(
this.props.transactions,
componentStateTransaction.signedTx
);
// if there is a matching tx in redux state
if (currentTxAsBroadcastTransaction) {
if (currentTxAsSignedTransaction) {
// if the broad-casted transaction attempt is successful, clear the form
if (currentTxAsBroadcastTransaction.successfullyBroadcast) {
this.resetTransaction();
if (currentTxAsSignedTransaction.successfullyBroadcast) {
this.resetTx();
}
}
}
@ -232,7 +235,7 @@ export class SendTransaction extends React.Component {
<button
disabled={this.state.generateDisabled}
className="btn btn-info btn-block"
onClick={this.generateTx}
onClick={this.generateTxFromState}
>
{translate('SEND_generate')}
</button>
@ -269,6 +272,7 @@ export class SendTransaction extends React.Component {
<div className="form-group">
<button
className="btn btn-primary btn-block col-sm-11"
disabled={!this.state.transaction}
onClick={this.openTxModal}
>
{translate('SEND_trans')}
@ -332,49 +336,37 @@ export class SendTransaction extends React.Component {
);
}
async getTransactionInfoFromState(): Promise<TransactionWithoutGas> {
async getFormattedTxFromState(): Promise<TransactionWithoutGas> {
const { wallet } = this.props;
const { token, unit, value, to, data } = this.state;
if (unit === 'ether') {
return {
to,
from: await wallet.getAddress(),
value: valueToHex(value),
data
};
} else {
if (!token) {
throw new Error('No matching token');
}
const bigAmount = new Big(value);
return {
to: token.address,
from: await wallet.getAddress(),
value: '0x0',
data: ERC20.transfer(to, toTokenUnit(bigAmount, token))
};
}
const transactionInput: TransactionInput = {
token,
unit,
value,
to,
data
};
return await formatTxInput(wallet, transactionInput);
}
async estimateGas() {
if (!isNaN(parseInt(this.state.value))) {
try {
const transaction = await this.getTransactionInfoFromState();
// 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;
const gasLimit = await this.props.nodeLib.estimateGas(transaction);
if (this.state === state) {
this.setState({ gasLimit: formatGasLimit(gasLimit, state.unit) });
} else {
this.estimateGas();
}
} catch (error) {
this.props.showNotification('danger', error.message, 5000);
if (isNaN(parseInt(this.state.value))) {
return;
}
try {
const cachedFormattedTx = await this.getFormattedTxFromState();
// 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;
const gasLimit = await this.props.nodeLib.estimateGas(cachedFormattedTx);
if (this.state === state) {
this.setState({ gasLimit: formatGasLimit(gasLimit, state.unit) });
} else {
// state has changed, so try again from the start (with the hope that state won't change by the next time)
this.estimateGas();
}
} catch (error) {
this.props.showNotification('danger', error.message, 5000);
}
}
@ -403,13 +395,9 @@ export class SendTransaction extends React.Component {
};
onDataChange = (value: string) => {
if (this.state.unit !== 'ether') {
return;
if (this.state.unit === 'ether') {
this.setState({ data: value });
}
this.setState({
...this.state,
data: value
});
};
onGasChange = (value: string) => {
@ -437,9 +425,7 @@ export class SendTransaction extends React.Component {
value = tokenBalance.balance.toString();
}
}
let token = this.props.tokens.find(x => x.symbol === unit);
this.setState({
value,
unit,
@ -447,40 +433,41 @@ export class SendTransaction extends React.Component {
});
};
generateTx = async () => {
const { nodeLib, wallet } = this.props;
const { token } = this.state;
const stateTxInfo = await this.getTransactionInfoFromState();
generateTxFromState = async () => {
const { nodeLib, wallet, gasPrice, network } = this.props;
const { token, unit, value, to, data, gasLimit } = this.state;
const chainId = network.chainId;
const transactionInput = {
token,
unit,
value,
to,
data
};
try {
const transaction = await generateTransaction(
nodeLib,
{
...stateTxInfo,
gasLimit: this.state.gasLimit,
gasPrice: this.props.gasPrice,
chainId: this.props.network.chainId
},
const signedTx = await generateCompleteTransaction(
wallet,
token
nodeLib,
gasPrice,
gasLimit,
chainId,
transactionInput
);
this.setState({ transaction });
this.setState({ transaction: signedTx });
} catch (err) {
this.props.showNotification('danger', err.message, 5000);
}
};
openTxModal = () => {
if (this.state.transaction) {
this.setState({ showTxConfirm: true });
}
this.setState({ showTxConfirm: true });
};
hideConfirmTx = () => {
this.setState({ showTxConfirm: false });
};
resetTransaction = () => {
resetTx = () => {
this.setState({
to: '',
value: '',
@ -502,7 +489,7 @@ function mapStateToProps(state: AppState) {
nodeLib: getNodeLib(state),
network: getNetworkConfig(state),
tokens: getTokens(state),
gasPrice: toWei(new Big(getGasPriceGwei(state)), 'gwei'),
gasPrice: toWei(new Big(getGasPriceGwei(state)), 'gwei').toString(),
transactions: state.wallet.transactions
};
}

7
common/libs/messages.js Normal file
View File

@ -0,0 +1,7 @@
// TODO - move this out of transaction; it's only for estimating gas costs
export type TransactionWithoutGas = {|
to: string,
value: string | number,
data: string,
from: string
|};

View File

@ -1,6 +1,6 @@
// @flow
import Big from 'bignumber.js';
import type { TransactionWithoutGas } from 'libs/transaction';
import type { TransactionWithoutGas } from 'libs/messages';
import type { Token } from 'config/data';
export interface INode {

View File

@ -1,7 +1,7 @@
// @flow
import Big from 'bignumber.js';
import type { INode } from '../INode';
import type { TransactionWithoutGas } from 'libs/transaction';
import type { TransactionWithoutGas } from 'libs/messages';
import RPCClient, {
getBalance,
estimateGas,
@ -39,7 +39,7 @@ export default class RpcNode implements INode {
return this.client.call(getTokenBalance(address, token)).then(response => {
if (response.error) {
// TODO - Error handling
return Big(0);
return new Big(0);
}
return new Big(String(response.result)).div(
new Big(10).pow(token.decimal)

View File

@ -1,5 +1,5 @@
// don't use flow temporarily
import type { TransactionWithoutGas } from 'libs/transaction';
import type { TransactionWithoutGas } from 'libs/messages';
type DATA = string;
type QUANTITY = string;

View File

@ -11,39 +11,46 @@ import type { BaseWallet } from 'libs/wallet';
import type { Token } from 'config/data';
import type EthTx from 'ethereumjs-tx';
import { toUnit } from 'libs/units';
import { valueToHex } from 'libs/values';
import type { UNIT } from 'libs/units';
import { RPCNode } from 'libs/nodes';
import { TransactionWithoutGas } from 'libs/messages';
export type BroadcastStatusTransaction = {
export type TransactionInput = {
token: ?Token,
unit: UNIT,
value: string,
to: string,
data: string
};
export type BroadcastTransactionStatus = {
isBroadcasting: boolean,
signedTx: string,
successfullyBroadcast: boolean
};
// TODO: Enforce more bigs, or find better way to avoid ether vs wei for value
export type TransactionWithoutGas = {|
from: string,
to: string,
gasLimit?: string | number,
value: string | number,
data?: string,
chainId?: number
|};
export type Transaction = {|
...TransactionWithoutGas,
gasPrice: string | number
|};
export type RawTransaction = {|
nonce: string,
gasPrice: string,
gasLimit: string,
export type BaseTransaction = {|
to: string,
value: string,
data: string,
gasLimit: string,
gasPrice: string,
chainId: number
|};
export type BroadcastTransaction = {|
export type RawTransaction = {|
...BaseTransaction,
nonce: string
|};
export type ExtendedRawTransaction = {|
...RawTransaction,
// non-standard, legacy
from: string
|};
export type CompleteTransaction = {|
...RawTransaction,
rawTx: string,
signedTx: string
@ -71,12 +78,12 @@ export function getTransactionFields(tx: EthTx) {
};
}
export async function generateTransaction(
export async function generateCompleteTransactionFromRawTransaction(
node: INode,
tx: Transaction,
tx: ExtendedRawTransaction,
wallet: BaseWallet,
token: ?Token
): Promise<BroadcastTransaction> {
): Promise<CompleteTransaction> {
// Reject bad addresses
if (!isValidETHAddress(tx.to)) {
throw new Error(translate('ERROR_5'));
@ -115,7 +122,6 @@ export async function generateTransaction(
let balance;
if (token) {
// $FlowFixMe - We reject above if tx has no data for token
value = new Big(ERC20.$transfer(tx.data).value);
balance = toTokenUnit(await node.getTokenBalance(tx.from, token), token);
} else {
@ -131,10 +137,8 @@ export async function generateTransaction(
// prefix'd hex value.
const cleanHex = hex => addHexPrefix(padToEven(stripHex(hex)));
// Generate the raw transaction
const txCount = await node.getTransactionCount(tx.from);
const rawTx = {
nonce: cleanHex(txCount),
const cleanedRawTx = {
nonce: cleanHex(tx.nonce),
gasPrice: cleanHex(new Big(tx.gasPrice).toString(16)),
gasLimit: cleanHex(new Big(tx.gasLimit).toString(16)),
to: cleanHex(tx.to),
@ -144,24 +148,82 @@ export async function generateTransaction(
};
// Sign the transaction
const rawTxJson = JSON.stringify(rawTx);
const signedTx = await wallet.signRawTransaction(rawTx);
const rawTxJson = JSON.stringify(cleanedRawTx);
const signedTx = await wallet.signRawTransaction(cleanedRawTx);
// 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,
nonce: cleanedRawTx.nonce,
gasPrice: cleanedRawTx.gasPrice,
gasLimit: cleanedRawTx.gasLimit,
to: cleanedRawTx.to,
value: cleanedRawTx.value,
data: cleanedRawTx.data,
chainId: cleanedRawTx.chainId,
rawTx: rawTxJson,
signedTx: signedTx
};
}
export async function formatTxInput(
wallet: BaseWallet,
{ token, unit, value, to, data }: TransactionInput
): Promise<TransactionWithoutGas> {
if (unit === 'ether') {
return {
to,
from: await wallet.getAddress(),
value: valueToHex(value),
data
};
} else {
if (!token) {
throw new Error('No matching token');
}
const bigAmount = new Big(value);
return {
to: token.address,
from: await wallet.getAddress(),
value: '0x0',
data: ERC20.transfer(to, toTokenUnit(bigAmount, token))
};
}
}
export async function generateCompleteTransaction(
wallet: BaseWallet,
nodeLib: RPCNode,
gasPrice: string,
gasLimit: string,
chainId: number,
transactionInput: TransactionInput
): Promise<CompleteTransaction> {
const { token } = transactionInput;
const formattedTx = await formatTxInput(wallet, transactionInput);
const from = await wallet.getAddress();
const transaction: ExtendedRawTransaction = {
nonce: await nodeLib.getTransactionCount(from),
from,
to: formattedTx.to,
gasLimit,
value: formattedTx.value,
data: formattedTx.data,
chainId,
gasPrice
};
return await generateCompleteTransactionFromRawTransaction(
nodeLib,
transaction,
wallet,
token
);
}
// TODO determine best place for helper function
export function getBalanceMinusGasCosts(
weiGasLimit: Big,

View File

@ -8,8 +8,8 @@ import type {
import { BaseWallet } from 'libs/wallet';
import { toUnit } from 'libs/units';
import Big from 'bignumber.js';
import { getTxFromBroadcastStatusTransactions } from 'selectors/wallet';
import type { BroadcastStatusTransaction } from 'libs/transaction';
import { getTxFromBroadcastTransactionStatus } from 'selectors/wallet';
import type { BroadcastTransactionStatus } from 'libs/transaction';
export type State = {
inst: ?BaseWallet,
// in ETH
@ -17,7 +17,7 @@ export type State = {
tokens: {
[string]: Big
},
transactions: Array<BroadcastStatusTransaction>
transactions: Array<BroadcastTransactionStatus>
};
export const INITIAL_STATE: State = {
@ -42,11 +42,11 @@ function setTokenBalances(state: State, action: SetTokenBalancesAction): State {
}
function handleUpdateTxArray(
transactions: Array<BroadcastStatusTransaction>,
broadcastStatusTx: BroadcastStatusTransaction,
transactions: Array<BroadcastTransactionStatus>,
broadcastStatusTx: BroadcastTransactionStatus,
isBroadcasting: boolean,
successfullyBroadcast: boolean
): Array<BroadcastStatusTransaction> {
): Array<BroadcastTransactionStatus> {
return transactions.map(item => {
if (item === broadcastStatusTx) {
return { ...item, isBroadcasting, successfullyBroadcast };
@ -60,9 +60,8 @@ function handleTxBroadcastCompleted(
state: State,
signedTx: string,
successfullyBroadcast: boolean
// TODO How to handle null case for existing Tx?. Should use Array<BroadcastStatusTransaction> but can't.
): Array<any> {
const existingTx = getTxFromBroadcastStatusTransactions(
): Array<BroadcastTransactionStatus> {
const existingTx = getTxFromBroadcastTransactionStatus(
state.transactions,
signedTx
);
@ -75,12 +74,12 @@ function handleTxBroadcastCompleted(
successfullyBroadcast
);
} else {
return [];
return state.transactions;
}
}
function handleBroadcastTxRequested(state: State, signedTx: string) {
const existingTx = getTxFromBroadcastStatusTransactions(
const existingTx = getTxFromBroadcastTransactionStatus(
state.transactions,
signedTx
);

View File

@ -4,7 +4,7 @@ import { BaseWallet } from 'libs/wallet';
import { getNetworkConfig } from 'selectors/config';
import Big from 'bignumber.js';
import type { Token } from 'config/data';
import type { BroadcastStatusTransaction } from 'libs/transaction';
import type { BroadcastTransactionStatus } from 'libs/transaction';
export function getWalletInst(state: State): ?BaseWallet {
return state.wallet.inst;
@ -44,15 +44,15 @@ export function getTokenBalances(state: State): TokenBalance[] {
export function getTxFromState(
state: State,
signedTx: string
): ?BroadcastStatusTransaction {
): ?BroadcastTransactionStatus {
const transactions = state.wallet.transactions;
return getTxFromBroadcastStatusTransactions(transactions, signedTx);
return getTxFromBroadcastTransactionStatus(transactions, signedTx);
}
export function getTxFromBroadcastStatusTransactions(
transactions: Array<BroadcastStatusTransaction>,
export function getTxFromBroadcastTransactionStatus(
transactions: Array<BroadcastTransactionStatus>,
signedTx: string
): ?BroadcastStatusTransaction {
): ?BroadcastTransactionStatus {
return transactions.find(transaction => {
return transaction.signedTx === signedTx;
});