Broadcast Tx (#304)

* create MVP of broadcast TX component

* add broadcastTx to routes and tab options

* Update BroadcastTx path to /pushTx

* - add sanitizeHex and padLeftEven functions from V3

* - Move decodeTransaction logic out of ConfirmationModal.
- Add from key to getTransactionFields

* Simplify ConfirmationModal:

1. Move business logic out of component (decodeTransaction).
2. Don't pass node props when Component is already smart. Map from state instead.
3. Pass walletAddress instead of the entire wallet object to component as prop.
4. Handle

* - Don't map node state (child component grabs from state)
- implement and call setWalletAddressOnUpdate

* correct tab to path.

* 1. Integrate Confirmation Modal
2. Validate signedTx input and disable Send Transaction button when invalid

* disable tslint error. EthTx expect a Data type object, but a string is passed. However, tx object is created as expected. Need to investigate

* adjust type definition to match allowed string input. Remove tslint disable

* fix tslint errors

* add textarea valid/invalid stlying based on disabled status

* remove unused imports

* cleanup / address PR comments

* Address PR comments
This commit is contained in:
Daniel Ternyak 2017-10-23 13:48:55 -07:00 committed by GitHub
parent b94bede473
commit d72b478c89
9 changed files with 252 additions and 79 deletions

View File

@ -9,6 +9,7 @@ import Help from 'containers/Tabs/Help';
import SendTransaction from 'containers/Tabs/SendTransaction';
import Swap from 'containers/Tabs/Swap';
import ViewWallet from 'containers/Tabs/ViewWallet';
import BroadcastTx from 'containers/Tabs/BroadcastTx';
// TODO: fix this
interface Props {
@ -31,6 +32,7 @@ export default class Root extends Component<Props, {}> {
<Route path="/send-transaction" component={SendTransaction} />
<Route path="/contracts" component={Contracts} />
<Route path="/ens" component={ENS} />
<Route path="/pushTx" component={BroadcastTx} />
</div>
</Router>
</Provider>

View File

@ -28,6 +28,10 @@ const tabs = [
name: 'NAV_ENS',
to: 'ens'
},
{
name: 'Broadcast Transaction',
to: 'pushTx'
},
{
name: 'NAV_Help',
to: 'https://myetherwallet.groovehq.com/help_center',

View File

@ -0,0 +1,7 @@
@import "common/sass/variables";
.BroadcastTx {
&-title {
margin: $space auto $space * 2.5;
}
}

View File

@ -0,0 +1,140 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import TabSection from 'containers/TabSection';
import { translateRaw } from 'translations';
import { broadcastTx as dBroadcastTx, TBroadcastTx } from 'actions/wallet';
import { QRCode } from 'components/ui';
import './index.scss';
import {
BroadcastTransactionStatus,
getTransactionFields
} from 'libs/transaction';
import EthTx from 'ethereumjs-tx';
import { ConfirmationModal } from 'containers/Tabs/SendTransaction/components';
import classnames from 'classnames';
interface Props {
broadcastTx: TBroadcastTx;
transactions: BroadcastTransactionStatus[];
}
interface State {
signedTx: string;
showConfirmationModal: boolean;
disabled: boolean;
}
const initialState: State = {
showConfirmationModal: false,
signedTx: '',
disabled: true
};
class BroadcastTx extends Component<Props, State> {
public state = initialState;
public ensureValidSignedTxInputOnUpdate() {
try {
const tx = new EthTx(this.state.signedTx);
getTransactionFields(tx);
if (this.state.disabled) {
this.setState({ disabled: false });
}
} catch (e) {
if (!this.state.disabled) {
this.setState({ disabled: true });
}
}
}
public componentDidUpdate() {
this.ensureValidSignedTxInputOnUpdate();
}
public render() {
const { signedTx, disabled, showConfirmationModal } = this.state;
const inputClasses = classnames({
'form-control': true,
'is-valid': !disabled,
'is-invalid': disabled
});
return (
<TabSection>
<div className="Tab-content-pane row block text-center">
<div className="col-md-6">
<div className="col-md-12 BroadcastTx-title">
<h2>Broadcast Signed Transaction</h2>
</div>
<p>
Paste a signed transaction and press the "SEND TRANSACTION"
button.
</p>
<label>{translateRaw('SEND_signed')}</label>
<textarea
className={inputClasses}
rows={7}
value={signedTx}
onChange={this.handleChange}
/>
<button
className="btn btn-primary"
disabled={disabled || signedTx === ''}
onClick={this.handleBroadcastTx}
>
{translateRaw('SEND_trans')}
</button>
</div>
<div className="col-md-6" style={{ marginTop: '70px' }}>
<div
className="qr-code text-center"
style={{
maxWidth: '15rem',
margin: '1rem auto',
width: '100%'
}}
>
{signedTx && <QRCode data={signedTx} />}
</div>
</div>
</div>
{showConfirmationModal && (
<ConfirmationModal
signedTx={signedTx}
onClose={this.handleClose}
onConfirm={this.handleConfirm}
/>
)}
</TabSection>
);
}
public handleClose = () => {
this.setState({ showConfirmationModal: false });
};
public handleBroadcastTx = () => {
this.setState({ showConfirmationModal: true });
};
public handleConfirm = () => {
this.props.broadcastTx(this.state.signedTx);
};
protected handleChange = event => {
this.setState({ signedTx: event.target.value });
};
}
function mapStateToProps(state: AppState) {
return {
transactions: state.wallet.transactions
};
}
export default connect(mapStateToProps, { broadcastTx: dBroadcastTx })(
BroadcastTx
);

View File

@ -1,19 +1,20 @@
import Big from 'bignumber.js';
import Identicon from 'components/ui/Identicon';
import Modal, { IButton } from 'components/ui/Modal';
import Spinner from 'components/ui/Spinner';
import { NetworkConfig, NodeConfig } from 'config/data';
import EthTx from 'ethereumjs-tx';
import ERC20 from 'libs/erc20';
import {
BroadcastTransactionStatus,
getTransactionFields
getTransactionFields,
decodeTransaction
} from 'libs/transaction';
import { toTokenDisplay, toUnit } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet';
import React from 'react';
import { connect } from 'react-redux';
import { getLanguageSelection, getNetworkConfig } from 'selectors/config';
import {
getLanguageSelection,
getNetworkConfig,
getNodeConfig
} from 'selectors/config';
import { getTokens, getTxFromState, MergedToken } from 'selectors/wallet';
import translate, { translateRaw } from 'translations';
import './ConfirmationModal.scss';
@ -21,9 +22,8 @@ import './ConfirmationModal.scss';
interface Props {
signedTx: string;
transaction: EthTx;
wallet: IWallet;
node: NodeConfig;
token: MergedToken | undefined;
token: MergedToken;
network: NetworkConfig;
lang: string;
broadCastTxStatus: BroadcastTransactionStatus;
@ -32,30 +32,22 @@ interface Props {
}
interface State {
fromAddress: string;
timeToRead: number;
hasBroadCasted: boolean;
}
class ConfirmationModal extends React.Component<Props, State> {
public state = {
fromAddress: '',
timeToRead: 5,
hasBroadCasted: false
};
private readTimer = 0;
public componentWillReceiveProps(newProps: Props) {
// Reload address if the wallet changes
if (newProps.wallet !== this.props.wallet) {
this.setWalletAddress(this.props.wallet);
}
}
public componentDidUpdate() {
if (
this.state.hasBroadCasted &&
this.props.broadCastTxStatus &&
!this.props.broadCastTxStatus.isBroadcasting
) {
this.props.onClose();
@ -71,20 +63,22 @@ class ConfirmationModal extends React.Component<Props, State> {
window.clearInterval(this.readTimer);
}
}, 1000);
this.setWalletAddress(this.props.wallet);
}
public render() {
const { node, token, network, onClose, broadCastTxStatus } = this.props;
const { fromAddress, timeToRead } = this.state;
const {
toAddress,
value,
gasPrice,
data,
nonce
} = this.decodeTransaction();
node,
token,
network,
onClose,
broadCastTxStatus,
transaction
} = this.props;
const { timeToRead } = this.state;
const { toAddress, value, gasPrice, data, from, nonce } = decodeTransaction(
transaction,
token
);
const buttonPrefix = timeToRead > 0 ? `(${timeToRead}) ` : '';
const buttons: IButton[] = [
@ -124,7 +118,7 @@ class ConfirmationModal extends React.Component<Props, State> {
<div>
<div className="ConfModal-summary">
<div className="ConfModal-summary-icon ConfModal-summary-icon--from">
<Identicon size="100%" address={fromAddress} />
<Identicon size="100%" address={from} />
</div>
<div className="ConfModal-summary-amount">
<div className="ConfModal-summary-amount-arrow" />
@ -139,7 +133,7 @@ class ConfirmationModal extends React.Component<Props, State> {
<ul className="ConfModal-details">
<li className="ConfModal-details-detail">
You are sending from <code>{fromAddress}</code>
You are sending from <code>{from}</code>
</li>
<li className="ConfModal-details-detail">
You are sending to <code>{toAddress}</code>
@ -192,38 +186,6 @@ class ConfirmationModal extends React.Component<Props, State> {
window.clearInterval(this.readTimer);
}
private async setWalletAddress(wallet: IWallet) {
// TODO move getAddress to saga
const fromAddress = await wallet.getAddress();
this.setState({ fromAddress });
}
private decodeTransaction() {
const { transaction, token } = this.props;
const { to, value, data, gasPrice, nonce } = getTransactionFields(
transaction
);
let fixedValue;
let toAddress;
if (token) {
const tokenData = ERC20.$transfer(data);
fixedValue = toTokenDisplay(new Big(tokenData.value), token).toString();
toAddress = tokenData.to;
} else {
fixedValue = toUnit(new Big(value, 16), 'wei', 'ether').toString();
toAddress = to;
}
return {
value: fixedValue,
gasPrice: toUnit(new Big(gasPrice, 16), 'wei', 'gwei').toString(),
data,
toAddress,
nonce
};
}
private confirm = () => {
if (this.state.timeToRead < 1) {
this.props.onConfirm(this.props.signedTx);
@ -239,6 +201,8 @@ function mapStateToProps(state, props) {
// Network config for defaults
const network = getNetworkConfig(state);
const node = getNodeConfig(state);
const lang = getLanguageSelection(state);
const broadCastTxStatus = getTxFromState(state, props.signedTx);
@ -249,6 +213,7 @@ function mapStateToProps(state, props) {
const token = data && tokens.find(t => t.address === to);
return {
node,
broadCastTxStatus,
transaction,
token,

View File

@ -15,7 +15,7 @@ import {
} from './components';
import NavigationPrompt from './components/NavigationPrompt';
// CONFIG
import { donationAddressMap, NetworkConfig, NodeConfig } from 'config/data';
import { donationAddressMap, NetworkConfig } from 'config/data';
// LIBS
import { stripHexPrefix } from 'libs/values';
import { TransactionWithoutGas } from 'libs/messages';
@ -51,7 +51,6 @@ import {
import {
getGasPriceGwei,
getNetworkConfig,
getNodeConfig,
getNodeLib
} from 'selectors/config';
import {
@ -86,12 +85,12 @@ interface State {
nonce: number | null | undefined;
hasSetDefaultNonce: boolean;
generateTxProcessing: boolean;
walletAddress: string | null;
}
interface Props {
wallet: IWallet;
balance: Ether;
node: NodeConfig;
nodeLib: RPCNode;
network: NetworkConfig;
tokens: MergedToken[];
@ -122,7 +121,8 @@ const initialState: State = {
generateDisabled: true,
nonce: null,
hasSetDefaultNonce: false,
generateTxProcessing: false
generateTxProcessing: false,
walletAddress: null
};
export class SendTransaction extends React.Component<Props, State> {
@ -220,12 +220,22 @@ export class SendTransaction extends React.Component<Props, State> {
}
}
public async setWalletAddressOnUpdate() {
if (this.props.wallet) {
const walletAddress = await this.props.wallet.getAddress();
if (walletAddress !== this.state.walletAddress) {
this.setState({ walletAddress });
}
}
}
public componentDidUpdate(prevProps: Props, prevState: State) {
this.handleGasEstimationOnUpdate(prevState);
this.handleGenerateDisabledOnUpdate();
this.handleBroadcastTransactionOnUpdate();
this.handleSetNonceWhenOfflineOnUpdate();
this.handleWalletStateOnUpdate(prevProps);
this.setWalletAddressOnUpdate();
}
public onNonceChange = (value: number) => {
@ -297,14 +307,14 @@ export class SendTransaction extends React.Component<Props, State> {
onChange={readOnly ? void 0 : this.onGasChange}
/>
{(offline || forceOffline) && (
<div>
<NonceField
value={nonce}
onChange={this.onNonceChange}
placeholder={'0'}
/>
</div>
)}
<div>
<NonceField
value={nonce}
onChange={this.onNonceChange}
placeholder={'0'}
/>
</div>
)}
{unit === 'ether' && (
<DataField
value={data}
@ -398,8 +408,7 @@ export class SendTransaction extends React.Component<Props, State> {
{transaction &&
showTxConfirm && (
<ConfirmationModal
wallet={this.props.wallet}
node={this.props.node}
fromAddress={this.state.walletAddress}
signedTx={transaction.signedTx}
onClose={this.hideConfirmTx}
onConfirm={this.confirmTx}
@ -640,7 +649,6 @@ function mapStateToProps(state: AppState) {
wallet: state.wallet.inst,
balance: state.wallet.balance,
tokenBalances: getTokenBalances(state),
node: getNodeConfig(state),
nodeLib: getNodeLib(state),
network: getNetworkConfig(state),
tokens: getTokens(state),

View File

@ -5,9 +5,16 @@ import ERC20 from 'libs/erc20';
import { TransactionWithoutGas } from 'libs/messages';
import { RPCNode } from 'libs/nodes';
import { INode } from 'libs/nodes/INode';
import { Ether, toTokenUnit, UnitKey, Wei } from 'libs/units';
import {
Ether,
toTokenUnit,
UnitKey,
Wei,
toTokenDisplay,
toUnit
} from 'libs/units';
import { isValidETHAddress } from 'libs/validators';
import { stripHexPrefixAndLower, valueToHex } from 'libs/values';
import { stripHexPrefixAndLower, valueToHex, sanitizeHex } from 'libs/values';
import { IWallet } from 'libs/wallet';
import { translateRaw } from 'translations';
import Big, { BigNumber } from 'bignumber.js';
@ -61,6 +68,7 @@ export function getTransactionFields(tx: EthTx) {
data: data === '0x' ? null : data,
// To address is unchecksummed, which could cause mismatches in comparisons
to: toChecksumAddress(to),
from: sanitizeHex(tx.getSenderAddress().toString('hex')),
// Everything else is as-is
nonce,
gasPrice,
@ -281,3 +289,29 @@ export function getBalanceMinusGasCosts(
const weiBalanceMinusGasCosts = balance.amount.minus(weiGasCosts);
return new Ether(weiBalanceMinusGasCosts);
}
export function decodeTransaction(transaction: EthTx, token: Token | false) {
const { to, value, data, gasPrice, nonce, from } = getTransactionFields(
transaction
);
let fixedValue;
let toAddress;
if (token) {
const tokenData = ERC20.$transfer(data);
fixedValue = toTokenDisplay(new Big(tokenData.value), token).toString();
toAddress = tokenData.to;
} else {
fixedValue = toUnit(new Big(value, 16), 'wei', 'ether').toString();
toAddress = to;
}
return {
value: fixedValue,
gasPrice: toUnit(new Big(gasPrice, 16), 'wei', 'gwei').toString(),
data,
toAddress,
nonce,
from
};
}

View File

@ -14,3 +14,16 @@ export function valueToHex(value: Ether): string {
// Finally, hex it up!
return `0x${wei.toString(16)}`;
}
export function padLeftEven(hex: string) {
return hex.length % 2 !== 0 ? `0${hex}` : hex;
}
// TODO: refactor to not mutate argument
export function sanitizeHex(hex: string) {
hex = hex.substring(0, 2) === '0x' ? hex.substring(2) : hex;
if (hex === '') {
return '';
}
return `0x${padLeftEven(hex)}`;
}

View File

@ -42,7 +42,7 @@ declare module 'ethereumjs-tx' {
class ITx {
public raw: Buffer;
constructor(data: Data);
constructor(data: Data | string);
/**
* If the tx's `to` is to the creation address
* @return {Boolean}