775 lines
22 KiB
TypeScript
775 lines
22 KiB
TypeScript
// COMPONENTS
|
|
import Spinner from 'components/ui/Spinner';
|
|
import TabSection from 'containers/TabSection';
|
|
import { BalanceSidebar } from 'components';
|
|
import { UnlockHeader } from 'components/ui';
|
|
import {
|
|
NonceField,
|
|
AddressField,
|
|
AmountField,
|
|
ConfirmationModal,
|
|
CustomMessage,
|
|
DataField,
|
|
GasField
|
|
} from './components';
|
|
import TransactionSucceeded from 'components/ExtendedNotifications/TransactionSucceeded';
|
|
import NavigationPrompt from './components/NavigationPrompt';
|
|
// CONFIG
|
|
import { donationAddressMap, NetworkConfig } from 'config/data';
|
|
// LIBS
|
|
import { stripHexPrefix } from 'libs/values';
|
|
import { TransactionWithoutGas } from 'libs/messages';
|
|
import { RPCNode } from 'libs/nodes';
|
|
import {
|
|
BroadcastTransactionStatus,
|
|
CompleteTransaction,
|
|
confirmAndSendWeb3Transaction,
|
|
formatTxInput,
|
|
generateCompleteTransaction,
|
|
getBalanceMinusGasCosts,
|
|
TransactionInput
|
|
} from 'libs/transaction';
|
|
import { UnitKey, Wei, getDecimal, toWei } from 'libs/units';
|
|
import { isValidETHAddress } from 'libs/validators';
|
|
// LIBS
|
|
import {
|
|
IWallet,
|
|
Balance,
|
|
Web3Wallet,
|
|
LedgerWallet,
|
|
TrezorWallet
|
|
} from 'libs/wallet';
|
|
import pickBy from 'lodash/pickBy';
|
|
import React from 'react';
|
|
// REDUX
|
|
import { connect } from 'react-redux';
|
|
import { AppState } from 'reducers';
|
|
import { showNotification, TShowNotification } from 'actions/notifications';
|
|
import {
|
|
broadcastTx,
|
|
TBroadcastTx,
|
|
resetWallet,
|
|
TResetWallet
|
|
} from 'actions/wallet';
|
|
import {
|
|
pollOfflineStatus as dPollOfflineStatus,
|
|
TPollOfflineStatus
|
|
} from 'actions/config';
|
|
// SELECTORS
|
|
import {
|
|
getGasPriceGwei,
|
|
getNetworkConfig,
|
|
getNodeLib
|
|
} from 'selectors/config';
|
|
import {
|
|
getTokenBalances,
|
|
getTokens,
|
|
getTxFromBroadcastTransactionStatus,
|
|
MergedToken,
|
|
TokenBalance
|
|
} from 'selectors/wallet';
|
|
import translate from 'translations';
|
|
// UTILS
|
|
import { formatGasLimit } from 'utils/formatters';
|
|
import { getParam } from 'utils/helpers';
|
|
import queryString from 'query-string';
|
|
// MISC
|
|
import customMessages from './messages';
|
|
|
|
interface State {
|
|
hasQueryString: boolean;
|
|
readOnly: boolean;
|
|
to: string;
|
|
// amount value
|
|
value: string;
|
|
unit: UnitKey;
|
|
token?: MergedToken | null;
|
|
gasLimit: string;
|
|
data: string;
|
|
gasChanged: boolean;
|
|
transaction: CompleteTransaction | null;
|
|
showTxConfirm: boolean;
|
|
generateDisabled: boolean;
|
|
nonce: number | null | undefined;
|
|
hasSetDefaultNonce: boolean;
|
|
generateTxProcessing: boolean;
|
|
walletAddress: string | null;
|
|
}
|
|
|
|
interface Props {
|
|
wallet: IWallet;
|
|
balance: Balance;
|
|
nodeLib: RPCNode;
|
|
network: NetworkConfig;
|
|
tokens: MergedToken[];
|
|
tokenBalances: TokenBalance[];
|
|
gasPrice: Wei;
|
|
transactions: BroadcastTransactionStatus[];
|
|
showNotification: TShowNotification;
|
|
broadcastTx: TBroadcastTx;
|
|
resetWallet: TResetWallet;
|
|
offline: boolean;
|
|
forceOffline: boolean;
|
|
pollOfflineStatus: TPollOfflineStatus;
|
|
location: { search: string };
|
|
}
|
|
|
|
const initialState: State = {
|
|
hasQueryString: false,
|
|
readOnly: false,
|
|
to: '',
|
|
value: '',
|
|
unit: 'ether',
|
|
token: null,
|
|
gasLimit: '21000',
|
|
data: '',
|
|
gasChanged: false,
|
|
showTxConfirm: false,
|
|
transaction: null,
|
|
generateDisabled: true,
|
|
nonce: null,
|
|
hasSetDefaultNonce: false,
|
|
generateTxProcessing: false,
|
|
walletAddress: null
|
|
};
|
|
|
|
export class SendTransaction extends React.Component<Props, State> {
|
|
public state: State = initialState;
|
|
|
|
public componentDidMount() {
|
|
this.props.pollOfflineStatus();
|
|
const queryPresets = pickBy(this.parseQuery());
|
|
if (Object.keys(queryPresets).length) {
|
|
this.setState(state => {
|
|
return {
|
|
...state,
|
|
...queryPresets,
|
|
hasQueryString: true
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
public haveFieldsChanged(prevState) {
|
|
return (
|
|
this.state.to !== prevState.to ||
|
|
this.state.value !== prevState.value ||
|
|
this.state.unit !== prevState.unit ||
|
|
this.state.data !== prevState.data
|
|
);
|
|
}
|
|
|
|
public shouldReEstimateGas(prevState) {
|
|
// TODO listen to gas price changes here
|
|
// TODO debounce the call
|
|
// handle gas estimation
|
|
return (
|
|
// if any relevant fields changed
|
|
this.haveFieldsChanged(prevState) &&
|
|
// if gas has not changed
|
|
!this.state.gasChanged &&
|
|
// if we have valid tx
|
|
(this.isValid() || (this.props.offline || this.props.forceOffline))
|
|
);
|
|
}
|
|
|
|
public handleGasEstimationOnUpdate(prevState) {
|
|
if (this.shouldReEstimateGas(prevState)) {
|
|
this.estimateGas();
|
|
}
|
|
}
|
|
|
|
public handleGenerateDisabledOnUpdate() {
|
|
if (this.state.generateDisabled === this.isValid()) {
|
|
this.setState({ generateDisabled: !this.isValid() });
|
|
}
|
|
}
|
|
|
|
public handleBroadcastTransactionOnUpdate() {
|
|
// handle clearing the form once broadcast transaction promise resolves and compontent updates
|
|
const componentStateTransaction = this.state.transaction;
|
|
if (componentStateTransaction) {
|
|
// lives in redux state
|
|
const currentTxAsSignedTransaction = getTxFromBroadcastTransactionStatus(
|
|
this.props.transactions,
|
|
componentStateTransaction.signedTx
|
|
);
|
|
// if there is a matching tx in redux state
|
|
if (currentTxAsSignedTransaction) {
|
|
// if the broad-casted transaction attempt is successful, clear the form
|
|
if (currentTxAsSignedTransaction.successfullyBroadcast) {
|
|
this.resetTx();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public async handleSetNonceWhenOfflineOnUpdate() {
|
|
const { offline, forceOffline, wallet, nodeLib } = this.props;
|
|
const { hasSetDefaultNonce, nonce } = this.state;
|
|
const unlocked = !!wallet;
|
|
if (unlocked) {
|
|
const from = await wallet.getAddressString();
|
|
if (forceOffline && !offline && !hasSetDefaultNonce) {
|
|
const nonceHex = await nodeLib.getTransactionCount(from);
|
|
const newNonce = parseInt(stripHexPrefix(nonceHex), 10);
|
|
this.setState({ nonce: newNonce, hasSetDefaultNonce: true });
|
|
}
|
|
if (!forceOffline && !offline && nonce) {
|
|
// set hasSetDefaultNonce back to false in case user toggles force offline several times
|
|
this.setState({ nonce: null, hasSetDefaultNonce: false });
|
|
}
|
|
}
|
|
}
|
|
|
|
public handleWalletStateOnUpdate(prevProps) {
|
|
if (this.props.wallet !== prevProps.wallet && !!prevProps.wallet) {
|
|
this.setState(initialState);
|
|
}
|
|
}
|
|
|
|
public async setWalletAddressOnUpdate() {
|
|
if (this.props.wallet) {
|
|
const walletAddress = await this.props.wallet.getAddressString();
|
|
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) => {
|
|
this.setState({ nonce: value });
|
|
};
|
|
|
|
public render() {
|
|
const unlocked = !!this.props.wallet;
|
|
const {
|
|
to,
|
|
unit,
|
|
gasLimit,
|
|
data,
|
|
readOnly,
|
|
hasQueryString,
|
|
showTxConfirm,
|
|
transaction,
|
|
nonce,
|
|
generateTxProcessing
|
|
} = this.state;
|
|
const { offline, forceOffline, balance } = this.props;
|
|
const customMessage = customMessages.find(m => m.to === to);
|
|
const decimal =
|
|
unit === 'ether'
|
|
? getDecimal('ether')
|
|
: (this.state.token && this.state.token.decimal) || 0;
|
|
const isWeb3Wallet = this.props.wallet instanceof Web3Wallet;
|
|
const isLedgerWallet = this.props.wallet instanceof LedgerWallet;
|
|
const isTrezorWallet = this.props.wallet instanceof TrezorWallet;
|
|
return (
|
|
<TabSection>
|
|
<section className="Tab-content">
|
|
<UnlockHeader
|
|
title={
|
|
<div>
|
|
{translate('NAV_SendEther')}
|
|
{offline || forceOffline ? (
|
|
<span style={{ color: 'red' }}> (Offline)</span>
|
|
) : null}
|
|
</div>
|
|
}
|
|
allowReadOnly={true}
|
|
/>
|
|
<NavigationPrompt
|
|
when={unlocked}
|
|
onConfirm={this.props.resetWallet}
|
|
/>
|
|
<div className="row">
|
|
{/* Send Form */}
|
|
{unlocked &&
|
|
!(offline || (forceOffline && isWeb3Wallet)) && (
|
|
<main className="col-sm-8">
|
|
<div className="Tab-content-pane">
|
|
{hasQueryString && (
|
|
<div className="alert alert-info">
|
|
<p>{translate('WARN_Send_Link')}</p>
|
|
</div>
|
|
)}
|
|
|
|
<AddressField
|
|
placeholder={donationAddressMap.ETH}
|
|
value={this.state.to}
|
|
onChange={readOnly ? null : this.onAddressChange}
|
|
/>
|
|
<AmountField
|
|
unit={unit}
|
|
decimal={decimal}
|
|
balance={balance}
|
|
tokens={this.props.tokenBalances
|
|
.filter(token => !token.balance.eqn(0))
|
|
.map(token => token.symbol)
|
|
.sort()}
|
|
onAmountChange={this.onAmountChange}
|
|
isReadOnly={readOnly}
|
|
onUnitChange={this.onUnitChange}
|
|
/>
|
|
<GasField
|
|
value={gasLimit}
|
|
onChange={readOnly ? void 0 : this.onGasChange}
|
|
/>
|
|
{(offline || forceOffline) && (
|
|
<div>
|
|
<NonceField
|
|
value={nonce}
|
|
onChange={this.onNonceChange}
|
|
placeholder={'0'}
|
|
/>
|
|
</div>
|
|
)}
|
|
{unit === 'ether' && (
|
|
<DataField
|
|
value={data}
|
|
onChange={readOnly ? void 0 : this.onDataChange}
|
|
/>
|
|
)}
|
|
<CustomMessage message={customMessage} />
|
|
|
|
<div className="row form-group">
|
|
<div className="col-xs-12 clearfix">
|
|
<button
|
|
disabled={this.state.generateDisabled}
|
|
className="btn btn-info btn-block"
|
|
onClick={
|
|
isWeb3Wallet
|
|
? this.generateWeb3TxFromState
|
|
: this.generateTxFromState
|
|
}
|
|
>
|
|
{isWeb3Wallet
|
|
? translate('Send to MetaMask / Mist')
|
|
: translate('SEND_generate')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{generateTxProcessing && (
|
|
<div className="container">
|
|
<div className="row form-group text-center">
|
|
{isLedgerWallet || isTrezorWallet ? (
|
|
<div>
|
|
<p>
|
|
<b>Confirm transaction on hardware wallet</b>
|
|
</p>
|
|
<Spinner size="x2" />
|
|
</div>
|
|
) : (
|
|
<Spinner size="x2" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{transaction && (
|
|
<div>
|
|
<div className="row form-group">
|
|
<div className="col-sm-6">
|
|
<label>{translate('SEND_raw')}</label>
|
|
<textarea
|
|
className="form-control"
|
|
value={transaction.rawTx}
|
|
rows={4}
|
|
readOnly={true}
|
|
/>
|
|
</div>
|
|
<div className="col-sm-6">
|
|
<label>{translate('SEND_signed')}</label>
|
|
<textarea
|
|
className="form-control"
|
|
value={transaction.signedTx}
|
|
rows={4}
|
|
readOnly={true}
|
|
/>
|
|
{offline && (
|
|
<p>
|
|
To broadcast this transaction, paste the above
|
|
into{' '}
|
|
<a href="https://myetherwallet.com/pushTx">
|
|
{' '}
|
|
myetherwallet.com/pushTx
|
|
</a>{' '}
|
|
or{' '}
|
|
<a href="https://etherscan.io/pushTx">
|
|
{' '}
|
|
etherscan.io/pushTx
|
|
</a>
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{!offline && (
|
|
<div className="row form-group">
|
|
<div className="col-xs-12">
|
|
<button
|
|
className="btn btn-primary btn-block"
|
|
disabled={!this.state.transaction}
|
|
onClick={this.openTxModal}
|
|
>
|
|
{translate('SEND_trans')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</main>
|
|
)}
|
|
|
|
{unlocked &&
|
|
(offline || (forceOffline && isWeb3Wallet)) && (
|
|
<main className="col-sm-8">
|
|
<div className="Tab-content-pane">
|
|
<h4>Sorry...</h4>
|
|
<p>
|
|
MetaMask / Mist wallets are not available in offline mode.
|
|
</p>
|
|
</div>
|
|
</main>
|
|
)}
|
|
|
|
{/* Sidebar */}
|
|
{unlocked && (
|
|
<section className="col-sm-4">
|
|
<BalanceSidebar />
|
|
</section>
|
|
)}
|
|
</div>
|
|
{transaction &&
|
|
showTxConfirm && (
|
|
<ConfirmationModal
|
|
decimal={decimal}
|
|
fromAddress={this.state.walletAddress}
|
|
signedTx={transaction.signedTx}
|
|
onClose={this.hideConfirmTx}
|
|
onConfirm={this.confirmTx}
|
|
/>
|
|
)}
|
|
</section>
|
|
</TabSection>
|
|
);
|
|
}
|
|
|
|
public parseQuery() {
|
|
const searchStr = this.props.location.search;
|
|
const query = queryString.parse(searchStr);
|
|
const to = getParam(query, 'to');
|
|
const data = getParam(query, 'data');
|
|
const unit = getParam(query, 'tokenSymbol');
|
|
const token = this.props.tokens.find(x => x.symbol === unit);
|
|
const value = getParam(query, 'value');
|
|
let gasLimit = getParam(query, 'gaslimit');
|
|
if (gasLimit === null) {
|
|
gasLimit = getParam(query, 'limit');
|
|
}
|
|
const readOnly = getParam(query, 'readOnly') != null;
|
|
return { to, token, data, value, unit, gasLimit, readOnly };
|
|
}
|
|
|
|
public isValidNonce() {
|
|
const { offline, forceOffline } = this.props;
|
|
const { nonce } = this.state;
|
|
let valid = true;
|
|
if (offline || forceOffline) {
|
|
if (!nonce || nonce < 0) {
|
|
valid = false;
|
|
}
|
|
}
|
|
return valid;
|
|
}
|
|
|
|
public isValid() {
|
|
const { to, value, gasLimit } = this.state;
|
|
return (
|
|
isValidETHAddress(to) &&
|
|
value &&
|
|
Number(value) > 0 &&
|
|
!isNaN(Number(value)) &&
|
|
isFinite(Number(value)) &&
|
|
!isNaN(parseInt(gasLimit, 10)) &&
|
|
isFinite(parseInt(gasLimit, 10)) &&
|
|
this.isValidNonce()
|
|
);
|
|
}
|
|
|
|
public async getFormattedTxFromState(): Promise<TransactionWithoutGas> {
|
|
const { wallet } = this.props;
|
|
if (wallet.isReadOnly) {
|
|
throw new Error('Wallet is read-only');
|
|
}
|
|
|
|
const { token, unit, value, to, data } = this.state;
|
|
const transactionInput: TransactionInput = {
|
|
token,
|
|
unit,
|
|
value,
|
|
to,
|
|
data
|
|
};
|
|
return await formatTxInput(wallet, transactionInput);
|
|
}
|
|
|
|
public isValidValue() {
|
|
return !isNaN(parseInt(this.state.value, 10));
|
|
}
|
|
|
|
public async estimateGas() {
|
|
const { offline, forceOffline, nodeLib } = this.props;
|
|
let gasLimit;
|
|
|
|
if (offline || forceOffline) {
|
|
const { unit } = this.state;
|
|
if (unit === 'ether') {
|
|
gasLimit = 21000;
|
|
} else {
|
|
gasLimit = 150000;
|
|
}
|
|
this.setState({ gasLimit });
|
|
return;
|
|
}
|
|
|
|
if (!this.isValidValue()) {
|
|
return;
|
|
}
|
|
|
|
if (this.props.wallet) {
|
|
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;
|
|
gasLimit = await 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.setState({ generateDisabled: true });
|
|
this.props.showNotification('danger', error.message, 5000);
|
|
}
|
|
}
|
|
}
|
|
|
|
public onAddressChange = (value: string) => {
|
|
this.setState({
|
|
to: value
|
|
});
|
|
};
|
|
|
|
public onDataChange = (value: string) => {
|
|
if (this.state.unit === 'ether') {
|
|
this.setState({ data: value });
|
|
}
|
|
};
|
|
|
|
public onGasChange = (value: string) => {
|
|
this.setState({ gasLimit: value, gasChanged: true });
|
|
};
|
|
|
|
public handleEverythingAmountChange = (
|
|
value: string,
|
|
unit: string
|
|
): string => {
|
|
if (unit === 'ether') {
|
|
const { balance, gasPrice } = this.props;
|
|
const { gasLimit } = this.state;
|
|
const bigGasLimit = Wei(gasLimit);
|
|
value = getBalanceMinusGasCosts(
|
|
bigGasLimit,
|
|
gasPrice,
|
|
balance.wei
|
|
).toString();
|
|
} else {
|
|
const tokenBalance = this.props.tokenBalances.find(
|
|
tBalance => tBalance.symbol === unit
|
|
);
|
|
if (!tokenBalance) {
|
|
throw new Error(`${unit}: not found in token balances;`);
|
|
}
|
|
value = tokenBalance.balance.toString();
|
|
}
|
|
return value;
|
|
};
|
|
|
|
public onAmountChange = (value: string, unit: UnitKey) => {
|
|
if (value === 'everything') {
|
|
value = this.handleEverythingAmountChange(value, unit);
|
|
}
|
|
|
|
this.setState({
|
|
value,
|
|
unit
|
|
});
|
|
};
|
|
|
|
public onUnitChange = (unit: UnitKey) => {
|
|
const token = this.props.tokens.find(x => x.symbol === unit);
|
|
let stateToSet: any = { token };
|
|
|
|
if (unit !== this.state.unit) {
|
|
stateToSet = {
|
|
...stateToSet,
|
|
transaction: null,
|
|
generateDisabled: true,
|
|
unit
|
|
};
|
|
}
|
|
|
|
this.setState(stateToSet);
|
|
};
|
|
|
|
public resetJustTx = async (): Promise<any> =>
|
|
new Promise(resolve =>
|
|
this.setState(
|
|
{
|
|
transaction: null
|
|
},
|
|
resolve
|
|
)
|
|
);
|
|
|
|
public generateWeb3TxFromState = async () => {
|
|
await this.resetJustTx();
|
|
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
|
|
};
|
|
const bigGasLimit = Wei(gasLimit);
|
|
|
|
if (!(wallet instanceof Web3Wallet)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const txHash = await confirmAndSendWeb3Transaction(
|
|
wallet,
|
|
nodeLib,
|
|
gasPrice,
|
|
bigGasLimit,
|
|
chainId,
|
|
transactionInput
|
|
);
|
|
|
|
if (network.blockExplorer !== undefined) {
|
|
this.props.showNotification(
|
|
'success',
|
|
<TransactionSucceeded
|
|
txHash={txHash}
|
|
blockExplorer={network.blockExplorer}
|
|
/>,
|
|
0
|
|
);
|
|
}
|
|
} catch (err) {
|
|
//show an error
|
|
this.props.showNotification('danger', err.message, 5000);
|
|
}
|
|
};
|
|
|
|
public generateTxFromState = async () => {
|
|
this.setState({ generateTxProcessing: true });
|
|
await this.resetJustTx();
|
|
const { nodeLib, wallet, gasPrice, network, offline } = this.props;
|
|
const { token, unit, value, to, data, gasLimit, nonce } = this.state;
|
|
|
|
const chainId = network.chainId;
|
|
const transactionInput = {
|
|
token,
|
|
unit,
|
|
value,
|
|
to,
|
|
data
|
|
};
|
|
const bigGasLimit = Wei(gasLimit);
|
|
try {
|
|
if (wallet.isReadOnly) {
|
|
throw new Error('Wallet is read-only');
|
|
}
|
|
|
|
const signedTx = await generateCompleteTransaction(
|
|
wallet,
|
|
nodeLib,
|
|
gasPrice,
|
|
bigGasLimit,
|
|
chainId,
|
|
transactionInput,
|
|
false,
|
|
nonce,
|
|
offline
|
|
);
|
|
this.setState({ transaction: signedTx, generateTxProcessing: false });
|
|
} catch (err) {
|
|
this.setState({ generateTxProcessing: false });
|
|
this.props.showNotification('danger', err.message, 5000);
|
|
}
|
|
};
|
|
|
|
public openTxModal = () => {
|
|
this.setState({ showTxConfirm: true });
|
|
};
|
|
|
|
public hideConfirmTx = () => {
|
|
this.setState({ showTxConfirm: false });
|
|
};
|
|
|
|
public resetTx = () => {
|
|
this.setState({
|
|
to: '',
|
|
value: '',
|
|
transaction: null
|
|
});
|
|
};
|
|
|
|
public confirmTx = (signedTx: string) => {
|
|
this.props.broadcastTx(signedTx);
|
|
};
|
|
}
|
|
|
|
function mapStateToProps(state: AppState) {
|
|
return {
|
|
wallet: state.wallet.inst,
|
|
balance: state.wallet.balance,
|
|
tokenBalances: getTokenBalances(state),
|
|
nodeLib: getNodeLib(state),
|
|
network: getNetworkConfig(state),
|
|
tokens: getTokens(state),
|
|
gasPrice: toWei(`${getGasPriceGwei(state)}`, getDecimal('gwei')),
|
|
transactions: state.wallet.transactions,
|
|
offline: state.config.offline,
|
|
forceOffline: state.config.forceOffline
|
|
};
|
|
}
|
|
|
|
export default connect(mapStateToProps, {
|
|
showNotification,
|
|
broadcastTx,
|
|
resetWallet,
|
|
pollOfflineStatus: dPollOfflineStatus
|
|
})(SendTransaction);
|