mirror of
https://github.com/status-im/MyCrypto.git
synced 2025-01-22 08:58:55 +00:00
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:
parent
b94bede473
commit
d72b478c89
@ -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>
|
||||
|
@ -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',
|
||||
|
7
common/containers/Tabs/BroadcastTx/index.scss
Normal file
7
common/containers/Tabs/BroadcastTx/index.scss
Normal file
@ -0,0 +1,7 @@
|
||||
@import "common/sass/variables";
|
||||
|
||||
.BroadcastTx {
|
||||
&-title {
|
||||
margin: $space auto $space * 2.5;
|
||||
}
|
||||
}
|
140
common/containers/Tabs/BroadcastTx/index.tsx
Normal file
140
common/containers/Tabs/BroadcastTx/index.tsx
Normal 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
|
||||
);
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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)}`;
|
||||
}
|
||||
|
2
common/typescript/ethereumjs-tx.d.ts
vendored
2
common/typescript/ethereumjs-tx.d.ts
vendored
@ -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}
|
||||
|
Loading…
x
Reference in New Issue
Block a user