mirror of
https://github.com/status-im/MyCrypto.git
synced 2025-01-10 19:16:10 +00:00
Finalize send (broadcast signedTx, loading indicators, error handling, form validation) (#141)
* hide buttons during send loading state * fix transaction succeeded not clickable; provide error in action * move BroadcastStatusTransaction into 'libs/transaction' * use more succint Array.prototype.find * rename resetState -> resetTransaction * refactor and component componentDidUpdate logic * rename disabled -> generateDisabled; comment componentDidUpdate * add size to Spinner, use in ConfirmationModal; disable instead of hide buttons in Modal * fix flow not understanding that an object wouldn't be null in this case anyway. silly flow * various refactors; send entire balance working
This commit is contained in:
parent
ae0ada9c06
commit
a4ec6f6139
@ -85,9 +85,27 @@ export function setTokenBalances(payload: {
|
||||
};
|
||||
}
|
||||
|
||||
/*** Broadcast Tx ***/
|
||||
export type BroadcastTxRequestedAction = {
|
||||
type: 'WALLET_BROADCAST_TX_REQUESTED',
|
||||
payload: {
|
||||
signedTx: string
|
||||
}
|
||||
};
|
||||
|
||||
export function broadcastTx(signedTx: string): BroadcastTxRequestedAction {
|
||||
return {
|
||||
type: 'WALLET_BROADCAST_TX_REQUESTED',
|
||||
payload: {
|
||||
signedTx
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*** Union Type ***/
|
||||
export type WalletAction =
|
||||
| UnlockPrivateKeyAction
|
||||
| SetWalletAction
|
||||
| SetBalanceAction
|
||||
| SetTokenBalancesAction;
|
||||
| SetTokenBalancesAction
|
||||
| BroadcastTxRequestedAction;
|
||||
|
@ -14,6 +14,7 @@ import { formatNumber } from 'utils/formatters';
|
||||
import { Identicon } from 'components/ui';
|
||||
import translate from 'translations';
|
||||
import * as customTokenActions from 'actions/customTokens';
|
||||
import { showNotification } from 'actions/notifications';
|
||||
|
||||
type Props = {
|
||||
wallet: BaseWallet,
|
||||
@ -21,6 +22,7 @@ type Props = {
|
||||
network: NetworkConfig,
|
||||
tokenBalances: TokenBalance[],
|
||||
rates: { [string]: number },
|
||||
showNotification: Function,
|
||||
addCustomToken: typeof customTokenActions.addCustomToken,
|
||||
removeCustomToken: typeof customTokenActions.removeCustomToken
|
||||
};
|
||||
@ -39,8 +41,7 @@ export class BalanceSidebar extends React.Component {
|
||||
this.setState({ address: addr });
|
||||
})
|
||||
.catch(err => {
|
||||
//TODO: communicate error in UI
|
||||
console.log(err);
|
||||
this.props.showNotification('danger', err);
|
||||
});
|
||||
}
|
||||
|
||||
@ -130,35 +131,35 @@ export class BalanceSidebar extends React.Component {
|
||||
{rates['REP'] &&
|
||||
<li>
|
||||
<span className="mono wrap">
|
||||
{formatNumber(balance.times(rates['REP']))}
|
||||
{formatNumber(balance.times(rates['REP']), 2)}
|
||||
</span>{' '}
|
||||
REP
|
||||
</li>}
|
||||
{rates['EUR'] &&
|
||||
<li>
|
||||
<span className="mono wrap">
|
||||
€{formatNumber(balance.times(rates['EUR']))}
|
||||
€{formatNumber(balance.times(rates['EUR']), 2)}
|
||||
</span>
|
||||
{' EUR'}
|
||||
</li>}
|
||||
{rates['USD'] &&
|
||||
<li>
|
||||
<span className="mono wrap">
|
||||
${formatNumber(balance.times(rates['USD']))}
|
||||
${formatNumber(balance.times(rates['USD']), 2)}
|
||||
</span>
|
||||
{' USD'}
|
||||
</li>}
|
||||
{rates['GBP'] &&
|
||||
<li>
|
||||
<span className="mono wrap">
|
||||
£{formatNumber(balance.times(rates['GBP']))}
|
||||
£{formatNumber(balance.times(rates['GBP']), 2)}
|
||||
</span>
|
||||
{' GBP'}
|
||||
</li>}
|
||||
{rates['CHF'] &&
|
||||
<li>
|
||||
<span className="mono wrap">
|
||||
{formatNumber(balance.times(rates['CHF']))}
|
||||
{formatNumber(balance.times(rates['CHF']), 2)}
|
||||
</span>{' '}
|
||||
CHF
|
||||
</li>}
|
||||
@ -191,4 +192,7 @@ function mapStateToProps(state: State) {
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, customTokenActions)(BalanceSidebar);
|
||||
export default connect(mapStateToProps, {
|
||||
...customTokenActions,
|
||||
showNotification
|
||||
})(BalanceSidebar);
|
||||
|
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import bityConfig from 'config/bity';
|
||||
import translate from 'translations';
|
||||
export type TransactionSucceededProps = {
|
||||
txHash: string
|
||||
};
|
||||
|
||||
const TransactionSucceeded = ({ txHash }: TransactionSucceededProps) => {
|
||||
// const checkTxLink = `https://www.myetherwallet.com?txHash=${txHash}/#check-tx-status`;
|
||||
const txHashLink = bityConfig.ethExplorer.replace('[[txHash]]', txHash);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
{translate('SUCCESS_3', true) + txHash}
|
||||
</p>
|
||||
<a
|
||||
className="btn btn-xs btn-info string"
|
||||
href={txHashLink}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Verify Transaction
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionSucceeded;
|
@ -165,7 +165,7 @@ export class WalletDecrypt extends Component {
|
||||
|
||||
onUnlock = (payload: any) => {
|
||||
this.props.dispatch(
|
||||
WALLETS[this.state.selectedWalletKey].unlock(payload || this.state.value)
|
||||
WALLETS[this.state.selectedWalletKey].unlock(this.state.value || payload)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ type Props = {
|
||||
onClick?: () => void
|
||||
}[],
|
||||
handleClose: () => void,
|
||||
disableButtons?: boolean,
|
||||
children: any
|
||||
};
|
||||
|
||||
@ -65,8 +66,10 @@ export default class Modal extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
_renderButtons() {
|
||||
return this.props.buttons.map((btn, idx) => {
|
||||
_renderButtons = () => {
|
||||
const { disableButtons, buttons } = this.props;
|
||||
|
||||
return buttons.map((btn, idx) => {
|
||||
let btnClass = 'Modal-footer-btn btn';
|
||||
|
||||
if (btn.type) {
|
||||
@ -78,13 +81,13 @@ export default class Modal extends Component {
|
||||
className={btnClass}
|
||||
onClick={btn.onClick}
|
||||
key={idx}
|
||||
disabled={btn.disabled}
|
||||
disabled={disableButtons || btn.disabled}
|
||||
>
|
||||
{btn.text}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isOpen, title, children, buttons, handleClose } = this.props;
|
||||
|
@ -5,9 +5,7 @@ import type { Element } from 'react';
|
||||
const DEFAULT_BUTTON_TYPE = 'primary';
|
||||
const DEFAULT_BUTTON_SIZE = 'lg';
|
||||
|
||||
const Spinner = () => {
|
||||
return <i className="fa fa-spinner fa-spin fa-fw" />;
|
||||
};
|
||||
import Spinner from './Spinner';
|
||||
|
||||
type ButtonType =
|
||||
| 'default'
|
||||
|
13
common/components/ui/Spinner.jsx
Normal file
13
common/components/ui/Spinner.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
type size = 'lg' | '2x' | '3x' | '4x' | '5x';
|
||||
|
||||
type SpinnerProps = {
|
||||
size?: size
|
||||
};
|
||||
|
||||
const Spinner = ({ size = 'fa-' }: SpinnerProps) => {
|
||||
return <i className={`fa fa-spinner fa-spin fa-${size ? size : 'fw'}`} />;
|
||||
};
|
||||
|
||||
export default Spinner;
|
@ -26,12 +26,15 @@ export class AddressField extends React.Component {
|
||||
return (
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-11">
|
||||
<label>{translate('SEND_addr')}:</label>
|
||||
<label>
|
||||
{translate('SEND_addr')}:
|
||||
</label>
|
||||
<input
|
||||
className={`form-control ${isValidENSorEtherAddress(value)
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={this.onChange}
|
||||
disabled={isReadonly}
|
||||
@ -39,9 +42,7 @@ export class AddressField extends React.Component {
|
||||
{!!ensAddress &&
|
||||
<p className="ens-response">
|
||||
↳
|
||||
<span className="mono">
|
||||
{ensAddress}
|
||||
</span>
|
||||
<span className="mono">{ensAddress}</span>
|
||||
</p>}
|
||||
</div>
|
||||
<div className="col-xs-1 address-identicon-container">
|
||||
|
@ -11,27 +11,31 @@ import ERC20 from 'libs/erc20';
|
||||
import { getTransactionFields } from 'libs/transaction';
|
||||
import { getTokens } from 'selectors/wallet';
|
||||
import { getNetworkConfig, getLanguageSelection } from 'selectors/config';
|
||||
import { getTxFromState } from 'selectors/wallet';
|
||||
import type { NodeConfig } from 'config/data';
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
signedTransaction: string,
|
||||
signedTx: string,
|
||||
transaction: EthTx,
|
||||
wallet: BaseWallet,
|
||||
node: NodeConfig,
|
||||
token: ?Token,
|
||||
network: NetworkConfig,
|
||||
onConfirm: (string, EthTx) => void,
|
||||
onCancel: () => void,
|
||||
lang: string
|
||||
onClose: () => void,
|
||||
lang: string,
|
||||
broadCastStatusTx: BroadcastStatusTransaction
|
||||
};
|
||||
|
||||
type State = {
|
||||
fromAddress: string,
|
||||
timeToRead: number
|
||||
timeToRead: number,
|
||||
hasBroadCasted: boolean
|
||||
};
|
||||
|
||||
class ConfirmationModal extends React.Component {
|
||||
@ -43,7 +47,8 @@ class ConfirmationModal extends React.Component {
|
||||
|
||||
this.state = {
|
||||
fromAddress: '',
|
||||
timeToRead: 5
|
||||
timeToRead: 5,
|
||||
hasBroadCasted: false
|
||||
};
|
||||
}
|
||||
|
||||
@ -54,6 +59,15 @@ class ConfirmationModal extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (
|
||||
this.state.hasBroadCasted &&
|
||||
!this.props.broadCastStatusTx.isBroadcasting
|
||||
) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
// Count down 5 seconds before allowing them to confirm
|
||||
readTimer = 0;
|
||||
componentDidMount() {
|
||||
@ -72,10 +86,10 @@ class ConfirmationModal extends React.Component {
|
||||
clearInterval(this.readTimer);
|
||||
}
|
||||
|
||||
_setWalletAddress(wallet: BaseWallet) {
|
||||
wallet.getAddress().then(fromAddress => {
|
||||
this.setState({ fromAddress });
|
||||
});
|
||||
async _setWalletAddress(wallet: BaseWallet) {
|
||||
// TODO move getAddress to saga
|
||||
const fromAddress = await wallet.getAddress();
|
||||
this.setState({ fromAddress });
|
||||
}
|
||||
|
||||
_decodeTransaction() {
|
||||
@ -102,15 +116,15 @@ class ConfirmationModal extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
_confirm() {
|
||||
_confirm = () => {
|
||||
if (this.state.timeToRead < 1) {
|
||||
const { signedTransaction, transaction } = this.props;
|
||||
this.props.onConfirm(signedTransaction, transaction);
|
||||
this.props.onConfirm(this.props.signedTx);
|
||||
this.setState({ hasBroadCasted: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { node, token, network, onCancel } = this.props;
|
||||
const { node, token, network, onClose, broadCastStatusTx } = this.props;
|
||||
const { fromAddress, timeToRead } = this.state;
|
||||
const { toAddress, value, gasPrice, data } = this._decodeTransaction();
|
||||
|
||||
@ -120,98 +134,114 @@ class ConfirmationModal extends React.Component {
|
||||
text: buttonPrefix + translateRaw('SENDModal_Yes'),
|
||||
type: 'primary',
|
||||
disabled: timeToRead > 0,
|
||||
onClick: this._confirm()
|
||||
onClick: this._confirm
|
||||
},
|
||||
{
|
||||
text: translateRaw('SENDModal_No'),
|
||||
type: 'default',
|
||||
onClick: onCancel
|
||||
onClick: onClose
|
||||
}
|
||||
];
|
||||
|
||||
const symbol = token ? token.symbol : network.unit;
|
||||
|
||||
const isBroadcasting =
|
||||
broadCastStatusTx && broadCastStatusTx.isBroadcasting;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Confirm Your Transaction"
|
||||
buttons={buttons}
|
||||
handleClose={onCancel}
|
||||
handleClose={onClose}
|
||||
disableButtons={isBroadcasting}
|
||||
isOpen={true}
|
||||
>
|
||||
<div className="ConfModal">
|
||||
<div className="ConfModal-summary">
|
||||
<div className="ConfModal-summary-icon ConfModal-summary-icon--from">
|
||||
<Identicon size="100%" address={fromAddress} />
|
||||
</div>
|
||||
<div className="ConfModal-summary-amount">
|
||||
<div className="ConfModal-summary-amount-arrow" />
|
||||
<div className="ConfModal-summary-amount-currency">
|
||||
{value} {symbol}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ConfModal-summary-icon ConfModal-summary-icon--to">
|
||||
<Identicon size="100%" address={toAddress} />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
<div className="ConfModal">
|
||||
{isBroadcasting
|
||||
? <div className="ConfModal-loading">
|
||||
<Spinner size="5x" />
|
||||
</div>
|
||||
: <div>
|
||||
<div className="ConfModal-summary">
|
||||
<div className="ConfModal-summary-icon ConfModal-summary-icon--from">
|
||||
<Identicon size="100%" address={fromAddress} />
|
||||
</div>
|
||||
<div className="ConfModal-summary-amount">
|
||||
<div className="ConfModal-summary-amount-arrow" />
|
||||
<div className="ConfModal-summary-amount-currency">
|
||||
{value} {symbol}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ConfModal-summary-icon ConfModal-summary-icon--to">
|
||||
<Identicon size="100%" address={toAddress} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="ConfModal-details">
|
||||
<li className="ConfModal-details-detail">
|
||||
You are sending from <code>{fromAddress}</code>
|
||||
</li>
|
||||
<li className="ConfModal-details-detail">
|
||||
You are sending to <code>{toAddress}</code>
|
||||
</li>
|
||||
<li className="ConfModal-details-detail">
|
||||
You are sending{' '}
|
||||
<strong>
|
||||
{value} {symbol}
|
||||
</strong>{' '}
|
||||
with a gas price of <strong>{gasPrice} gwei</strong>
|
||||
</li>
|
||||
<li className="ConfModal-details-detail">
|
||||
You are interacting with the <strong>{node.network}</strong>{' '}
|
||||
network provided by <strong>{node.service}</strong>
|
||||
</li>
|
||||
{!token &&
|
||||
<li className="ConfModal-details-detail">
|
||||
{data
|
||||
? <span>
|
||||
You are sending the following data:{' '}
|
||||
<textarea
|
||||
className="form-control"
|
||||
value={data}
|
||||
rows="3"
|
||||
disabled
|
||||
/>
|
||||
</span>
|
||||
: 'There is no data attached to this transaction'}
|
||||
</li>}
|
||||
</ul>
|
||||
<ul className="ConfModal-details">
|
||||
<li className="ConfModal-details-detail">
|
||||
You are sending from <code>{fromAddress}</code>
|
||||
</li>
|
||||
<li className="ConfModal-details-detail">
|
||||
You are sending to <code>{toAddress}</code>
|
||||
</li>
|
||||
<li className="ConfModal-details-detail">
|
||||
You are sending{' '}
|
||||
<strong>
|
||||
{value} {symbol}
|
||||
</strong>{' '}
|
||||
with a gas price of <strong>{gasPrice} gwei</strong>
|
||||
</li>
|
||||
<li className="ConfModal-details-detail">
|
||||
You are interacting with the{' '}
|
||||
<strong>{node.network}</strong> network provided by{' '}
|
||||
<strong>{node.service}</strong>
|
||||
</li>
|
||||
{!token &&
|
||||
<li className="ConfModal-details-detail">
|
||||
{data
|
||||
? <span>
|
||||
You are sending the following data:{' '}
|
||||
<textarea
|
||||
className="form-control"
|
||||
value={data}
|
||||
rows="3"
|
||||
disabled
|
||||
/>
|
||||
</span>
|
||||
: 'There is no data attached to this transaction'}
|
||||
</li>}
|
||||
</ul>
|
||||
|
||||
<div className="ConfModal-confirm">
|
||||
{translate('SENDModal_Content_3')}
|
||||
<div className="ConfModal-confirm">
|
||||
{translate('SENDModal_Content_3')}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state, props) {
|
||||
// Convert the signedTransaction to an EthTx transaction
|
||||
const transaction = new EthTx(props.signedTransaction);
|
||||
// Convert the signedTx to an EthTx transaction
|
||||
const transaction = new EthTx(props.signedTx);
|
||||
|
||||
// Network config for defaults
|
||||
const network = getNetworkConfig(state);
|
||||
|
||||
const lang = getLanguageSelection(state);
|
||||
|
||||
const broadCastStatusTx = getTxFromState(state, props.signedTx);
|
||||
|
||||
// Determine if we're sending to a token from the transaction to address
|
||||
const { to, data } = getTransactionFields(transaction);
|
||||
const tokens = getTokens(state);
|
||||
const token = data && tokens.find(t => t.address === to);
|
||||
|
||||
return {
|
||||
broadCastStatusTx,
|
||||
transaction,
|
||||
token,
|
||||
network,
|
||||
|
@ -44,4 +44,10 @@ $summary-height: 54px;
|
||||
font-weight: bold;
|
||||
font-size: $font-size-medium-bump;
|
||||
}
|
||||
|
||||
&-loading {
|
||||
text-align: center;
|
||||
font-size: $font-size-medium-bump
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import BaseWallet from 'libs/wallet/base';
|
||||
import customMessages from './messages';
|
||||
import { donationAddressMap } from 'config/data';
|
||||
import { isValidETHAddress } from 'libs/validators';
|
||||
import { toUnit } from 'libs/units';
|
||||
import {
|
||||
getNodeLib,
|
||||
getNetworkConfig,
|
||||
@ -32,8 +33,14 @@ import Big from 'bignumber.js';
|
||||
import { valueToHex } from 'libs/values';
|
||||
import ERC20 from 'libs/erc20';
|
||||
import type { TokenBalance } from 'selectors/wallet';
|
||||
import { getTokenBalances } from 'selectors/wallet';
|
||||
import {
|
||||
getTokenBalances,
|
||||
getTxFromBroadcastStatusTransactions
|
||||
} from 'selectors/wallet';
|
||||
import type { RPCNode } from 'libs/nodes';
|
||||
import { broadcastTx } from 'actions/wallet';
|
||||
import type { BroadcastTxRequestedAction } from 'actions/wallet';
|
||||
import type { BroadcastStatusTransaction } from 'libs/transaction';
|
||||
import type {
|
||||
TransactionWithoutGas,
|
||||
BroadcastTransaction
|
||||
@ -45,12 +52,13 @@ import { showNotification } from 'actions/notifications';
|
||||
import type { ShowNotificationAction } from 'actions/notifications';
|
||||
import type { NodeConfig } from 'config/data';
|
||||
import { getNodeConfig } from 'selectors/config';
|
||||
import { generateTransaction } from 'libs/transaction';
|
||||
import { generateTransaction, getBalanceMinusGasCosts } from 'libs/transaction';
|
||||
|
||||
type State = {
|
||||
hasQueryString: boolean,
|
||||
readOnly: boolean,
|
||||
to: string,
|
||||
// amount value
|
||||
value: string,
|
||||
// $FlowFixMe - Comes from getParam not validating unit
|
||||
unit: UNIT,
|
||||
@ -59,7 +67,8 @@ type State = {
|
||||
data: string,
|
||||
gasChanged: boolean,
|
||||
transaction: ?BroadcastTransaction,
|
||||
showTxConfirm: boolean
|
||||
showTxConfirm: boolean,
|
||||
generateDisabled: boolean
|
||||
};
|
||||
|
||||
function getParam(query: { [string]: string }, key: string) {
|
||||
@ -89,29 +98,33 @@ type Props = {
|
||||
tokens: Token[],
|
||||
tokenBalances: TokenBalance[],
|
||||
gasPrice: number,
|
||||
broadcastTx: (signedTx: string) => BroadcastTxRequestedAction,
|
||||
showNotification: (
|
||||
level: string,
|
||||
msg: string,
|
||||
duration?: number
|
||||
) => ShowNotificationAction
|
||||
) => ShowNotificationAction,
|
||||
transactions: Array<BroadcastStatusTransaction>
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
hasQueryString: false,
|
||||
readOnly: false,
|
||||
to: '',
|
||||
value: '',
|
||||
unit: 'ether',
|
||||
token: null,
|
||||
gasLimit: '21000',
|
||||
data: '',
|
||||
gasChanged: false,
|
||||
showTxConfirm: false,
|
||||
transaction: null,
|
||||
generateDisabled: true
|
||||
};
|
||||
|
||||
export class SendTransaction extends React.Component {
|
||||
props: Props;
|
||||
state: State = {
|
||||
hasQueryString: false,
|
||||
readOnly: false,
|
||||
// FIXME use correct defaults
|
||||
to: '',
|
||||
value: '',
|
||||
unit: 'ether',
|
||||
token: null,
|
||||
gasLimit: '21000',
|
||||
data: '',
|
||||
gasChanged: false,
|
||||
showTxConfirm: false,
|
||||
transaction: null
|
||||
};
|
||||
state: State = initialState;
|
||||
|
||||
componentDidMount() {
|
||||
const queryPresets = pickBy(this.parseQuery());
|
||||
@ -121,27 +134,46 @@ export class SendTransaction extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps: Props, prevState: State) {
|
||||
// if gas is not changed
|
||||
// and we have valid tx
|
||||
// and relevant fields changed
|
||||
// estimate gas
|
||||
// TODO we might want to listen to gas price changes here
|
||||
// TODO debunce the call
|
||||
// TODO listen to gas price changes here
|
||||
// TODO debounce the call
|
||||
if (
|
||||
// if gas has not changed
|
||||
!this.state.gasChanged &&
|
||||
// if we have valid tx
|
||||
this.isValid() &&
|
||||
// if any relevant fields changed
|
||||
(this.state.to !== prevState.to ||
|
||||
this.state.value !== prevState.value ||
|
||||
this.state.unit !== prevState.unit ||
|
||||
this.state.data !== prevState.data)
|
||||
) {
|
||||
this.estimateGas();
|
||||
if (!isNaN(parseInt(this.state.value))) {
|
||||
this.estimateGas();
|
||||
}
|
||||
}
|
||||
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(
|
||||
this.props.transactions,
|
||||
componentStateTransaction.signedTx
|
||||
);
|
||||
// if there is a matching tx in redux state
|
||||
if (currentTxAsBroadcastTransaction) {
|
||||
// if the broad-casted transaction attempt is successful, clear the form
|
||||
if (currentTxAsBroadcastTransaction.successfullyBroadcast) {
|
||||
this.resetTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const unlocked = !!this.props.wallet;
|
||||
const hasEnoughBalance = false;
|
||||
const {
|
||||
to,
|
||||
value,
|
||||
@ -170,7 +202,7 @@ export class SendTransaction extends React.Component {
|
||||
|
||||
{unlocked &&
|
||||
<article className="row">
|
||||
{'' /* <!-- Sidebar --> */}
|
||||
{/* <!-- Sidebar --> */}
|
||||
<section className="col-sm-4">
|
||||
<div style={{ maxWidth: 350 }}>
|
||||
<BalanceSidebar />
|
||||
@ -180,19 +212,6 @@ export class SendTransaction extends React.Component {
|
||||
</section>
|
||||
|
||||
<section className="col-sm-8">
|
||||
{readOnly &&
|
||||
!hasEnoughBalance &&
|
||||
<div className="row form-group">
|
||||
<div className="alert alert-danger col-xs-12 clearfix">
|
||||
<strong>
|
||||
Warning! You do not have enough funds to complete this
|
||||
swap.
|
||||
</strong>
|
||||
<br />
|
||||
Please add more funds or access a different wallet.
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<div className="row form-group">
|
||||
<h4 className="col-xs-12">
|
||||
{translate('SEND_trans')}
|
||||
@ -225,12 +244,13 @@ export class SendTransaction extends React.Component {
|
||||
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-12 clearfix">
|
||||
<a
|
||||
<button
|
||||
disabled={this.state.generateDisabled}
|
||||
className="btn btn-info btn-block"
|
||||
onClick={this.generateTx}
|
||||
>
|
||||
{translate('SEND_generate')}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -262,12 +282,12 @@ export class SendTransaction extends React.Component {
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<a
|
||||
<button
|
||||
className="btn btn-primary btn-block col-sm-11"
|
||||
onClick={this.openTxModal}
|
||||
>
|
||||
{translate('SEND_trans')}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>}
|
||||
</section>
|
||||
@ -279,8 +299,8 @@ export class SendTransaction extends React.Component {
|
||||
<ConfirmationModal
|
||||
wallet={this.props.wallet}
|
||||
node={this.props.node}
|
||||
signedTransaction={transaction.signedTx}
|
||||
onCancel={this.cancelTx}
|
||||
signedTx={transaction.signedTx}
|
||||
onClose={this.hideConfirmTx}
|
||||
onConfirm={this.confirmTx}
|
||||
/>}
|
||||
</section>
|
||||
@ -298,19 +318,20 @@ export class SendTransaction extends React.Component {
|
||||
if (gasLimit === null) {
|
||||
gasLimit = getParam(query, 'limit');
|
||||
}
|
||||
const readOnly = getParam(query, 'readOnly') == null ? false : true;
|
||||
|
||||
const readOnly = getParam(query, 'readOnly') != null;
|
||||
return { to, data, value, unit, gasLimit, readOnly };
|
||||
}
|
||||
|
||||
isValid() {
|
||||
const { to, value } = this.state;
|
||||
const { to, value, gasLimit } = this.state;
|
||||
return (
|
||||
isValidETHAddress(to) &&
|
||||
value &&
|
||||
Number(value) > 0 &&
|
||||
!isNaN(Number(value)) &&
|
||||
isFinite(Number(value))
|
||||
isFinite(Number(value)) &&
|
||||
!isNaN(parseInt(gasLimit)) &&
|
||||
isFinite(parseInt(gasLimit))
|
||||
);
|
||||
}
|
||||
|
||||
@ -343,20 +364,20 @@ export class SendTransaction extends React.Component {
|
||||
}
|
||||
|
||||
async estimateGas() {
|
||||
const trans = await this.getTransactionInfoFromState();
|
||||
if (!trans) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
this.props.nodeLib.estimateGas(trans).then(gasLimit => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME use mkTx instead or something that could take care of default gas/data and whatnot,
|
||||
@ -398,20 +419,26 @@ export class SendTransaction extends React.Component {
|
||||
};
|
||||
|
||||
onAmountChange = (value: string, unit: string) => {
|
||||
// TODO sub gas for eth
|
||||
if (value === 'everything') {
|
||||
if (unit === 'ether') {
|
||||
value = this.props.balance.toString();
|
||||
const { balance, gasPrice } = this.props;
|
||||
const { gasLimit } = this.state;
|
||||
const weiBalance = toWei(balance, 'ether');
|
||||
value = getBalanceMinusGasCosts(
|
||||
new Big(gasLimit),
|
||||
gasPrice,
|
||||
weiBalance
|
||||
);
|
||||
} else {
|
||||
const tokenBalance = this.props.tokenBalances.find(
|
||||
tokenBalance => tokenBalance.symbol === unit
|
||||
);
|
||||
if (!tokenBalance) {
|
||||
return;
|
||||
}
|
||||
value = tokenBalance.balance.toString();
|
||||
}
|
||||
const token = this.props.tokenBalances.find(
|
||||
token => token.symbol === unit
|
||||
);
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
value = token.balance.toString();
|
||||
}
|
||||
|
||||
let token = this.props.tokens.find(x => x.symbol === unit);
|
||||
|
||||
this.setState({
|
||||
@ -438,7 +465,6 @@ export class SendTransaction extends React.Component {
|
||||
wallet,
|
||||
token
|
||||
);
|
||||
|
||||
this.setState({ transaction });
|
||||
} catch (err) {
|
||||
this.props.showNotification('danger', err.message, 5000);
|
||||
@ -451,13 +477,20 @@ export class SendTransaction extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
cancelTx = () => {
|
||||
hideConfirmTx = () => {
|
||||
this.setState({ showTxConfirm: false });
|
||||
};
|
||||
|
||||
confirmTx = () => {
|
||||
// TODO: Broadcast transaction
|
||||
console.log(this.state.transaction);
|
||||
resetTransaction = () => {
|
||||
this.setState({
|
||||
to: '',
|
||||
value: '',
|
||||
transaction: null
|
||||
});
|
||||
};
|
||||
|
||||
confirmTx = (signedTx: string) => {
|
||||
this.props.broadcastTx(signedTx);
|
||||
};
|
||||
}
|
||||
|
||||
@ -470,8 +503,11 @@ 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'),
|
||||
transactions: state.wallet.transactions
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { showNotification })(SendTransaction);
|
||||
export default connect(mapStateToProps, { showNotification, broadcastTx })(
|
||||
SendTransaction
|
||||
);
|
||||
|
@ -1,355 +0,0 @@
|
||||
'use strict';
|
||||
var sendTxCtrl = function($scope, $sce, walletService) {
|
||||
$scope.ajaxReq = ajaxReq;
|
||||
$scope.unitReadable = ajaxReq.type;
|
||||
$scope.sendTxModal = new Modal(document.getElementById('sendTransaction'));
|
||||
walletService.wallet = null;
|
||||
walletService.password = '';
|
||||
$scope.showAdvance = $scope.showRaw = false;
|
||||
$scope.dropdownEnabled = true;
|
||||
$scope.Validator = Validator;
|
||||
$scope.gasLimitChanged = false;
|
||||
// Tokens
|
||||
$scope.tokenVisibility = 'hidden';
|
||||
$scope.tokenTx = {
|
||||
to: '',
|
||||
value: 0,
|
||||
id: -1
|
||||
};
|
||||
$scope.customGasMsg = '';
|
||||
|
||||
// For token sale holders:
|
||||
// 1. Add the address users are sending to
|
||||
// 2. Add the gas limit users should use to send successfully (this avoids OOG errors)
|
||||
// 3. Add any data if applicable
|
||||
// 4. Add a message if you want.
|
||||
|
||||
$scope.tx = {
|
||||
// if there is no gasLimit or gas key in the URI, use the default value. Otherwise use value of gas or gasLimit. gasLimit wins over gas if both present
|
||||
gasLimit: globalFuncs.urlGet('gaslimit') != null ||
|
||||
globalFuncs.urlGet('gas') != null
|
||||
? globalFuncs.urlGet('gaslimit') != null
|
||||
? globalFuncs.urlGet('gaslimit')
|
||||
: globalFuncs.urlGet('gas')
|
||||
: globalFuncs.defaultTxGasLimit,
|
||||
data: globalFuncs.urlGet('data') == null ? '' : globalFuncs.urlGet('data'),
|
||||
to: globalFuncs.urlGet('to') == null ? '' : globalFuncs.urlGet('to'),
|
||||
unit: 'ether',
|
||||
value: globalFuncs.urlGet('value') == null
|
||||
? ''
|
||||
: globalFuncs.urlGet('value'),
|
||||
nonce: null,
|
||||
gasPrice: null,
|
||||
donate: false,
|
||||
tokenSymbol: globalFuncs.urlGet('tokenSymbol') == null
|
||||
? false
|
||||
: globalFuncs.urlGet('tokenSymbol'),
|
||||
readOnly: globalFuncs.urlGet('readOnly') == null ? false : true
|
||||
};
|
||||
$scope.setSendMode = function(sendMode, tokenId = '', tokenSymbol = '') {
|
||||
$scope.tx.sendMode = sendMode;
|
||||
$scope.unitReadable = '';
|
||||
if (sendMode == 'ether') {
|
||||
$scope.unitReadable = ajaxReq.type;
|
||||
} else {
|
||||
$scope.unitReadable = tokenSymbol;
|
||||
$scope.tokenTx.id = tokenId;
|
||||
}
|
||||
$scope.dropdownAmount = false;
|
||||
};
|
||||
$scope.setTokenSendMode = function() {
|
||||
if ($scope.tx.sendMode == 'token' && !$scope.tx.tokenSymbol) {
|
||||
$scope.tx.tokenSymbol = $scope.wallet.tokenObjs[0].symbol;
|
||||
$scope.wallet.tokenObjs[0].type = 'custom';
|
||||
$scope.setSendMode($scope.tx.sendMode, 0, $scope.tx.tokenSymbol);
|
||||
} else if ($scope.tx.tokenSymbol) {
|
||||
for (var i = 0; i < $scope.wallet.tokenObjs.length; i++) {
|
||||
if (
|
||||
$scope.wallet.tokenObjs[i].symbol
|
||||
.toLowerCase()
|
||||
.indexOf($scope.tx.tokenSymbol.toLowerCase()) !== -1
|
||||
) {
|
||||
$scope.wallet.tokenObjs[i].type = 'custom';
|
||||
$scope.setSendMode('token', i, $scope.wallet.tokenObjs[i].symbol);
|
||||
break;
|
||||
} else $scope.tokenTx.id = -1;
|
||||
}
|
||||
}
|
||||
if ($scope.tx.sendMode != 'token') $scope.tokenTx.id = -1;
|
||||
};
|
||||
var applyScope = function() {
|
||||
if (!$scope.$$phase) $scope.$apply();
|
||||
};
|
||||
var defaultInit = function() {
|
||||
globalFuncs.urlGet('sendMode') == null
|
||||
? $scope.setSendMode('ether')
|
||||
: $scope.setSendMode(globalFuncs.urlGet('sendMode'));
|
||||
$scope.showAdvance =
|
||||
globalFuncs.urlGet('gaslimit') != null ||
|
||||
globalFuncs.urlGet('gas') != null ||
|
||||
globalFuncs.urlGet('data') != null;
|
||||
if (
|
||||
globalFuncs.urlGet('data') ||
|
||||
globalFuncs.urlGet('value') ||
|
||||
globalFuncs.urlGet('to') ||
|
||||
globalFuncs.urlGet('gaslimit') ||
|
||||
globalFuncs.urlGet('sendMode') ||
|
||||
globalFuncs.urlGet('gas') ||
|
||||
globalFuncs.urlGet('tokenSymbol')
|
||||
)
|
||||
$scope.hasQueryString = true; // if there is a query string, show an warning at top of page
|
||||
};
|
||||
$scope.$watch(
|
||||
function() {
|
||||
if (walletService.wallet == null) return null;
|
||||
return walletService.wallet.getAddressString();
|
||||
},
|
||||
function() {
|
||||
if (walletService.wallet == null) return;
|
||||
$scope.wallet = walletService.wallet;
|
||||
$scope.wd = true;
|
||||
$scope.wallet.setBalance(applyScope);
|
||||
$scope.wallet.setTokens();
|
||||
if ($scope.parentTxConfig) {
|
||||
var setTxObj = function() {
|
||||
$scope.tx.to = $scope.parentTxConfig.to;
|
||||
$scope.tx.value = $scope.parentTxConfig.value;
|
||||
$scope.tx.sendMode = $scope.parentTxConfig.sendMode
|
||||
? $scope.parentTxConfig.sendMode
|
||||
: 'ether';
|
||||
$scope.tx.tokenSymbol = $scope.parentTxConfig.tokenSymbol
|
||||
? $scope.parentTxConfig.tokenSymbol
|
||||
: '';
|
||||
$scope.tx.readOnly = $scope.parentTxConfig.readOnly
|
||||
? $scope.parentTxConfig.readOnly
|
||||
: false;
|
||||
};
|
||||
$scope.$watch(
|
||||
'parentTxConfig',
|
||||
function() {
|
||||
setTxObj();
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
$scope.setTokenSendMode();
|
||||
defaultInit();
|
||||
}
|
||||
);
|
||||
$scope.$watch('ajaxReq.key', function() {
|
||||
if ($scope.wallet) {
|
||||
$scope.setSendMode('ether');
|
||||
$scope.wallet.setBalance(applyScope);
|
||||
$scope.wallet.setTokens();
|
||||
}
|
||||
});
|
||||
$scope.$watch(
|
||||
'tokenTx',
|
||||
function() {
|
||||
if (
|
||||
$scope.wallet &&
|
||||
$scope.wallet.tokenObjs !== undefined &&
|
||||
$scope.wallet.tokenObjs[$scope.tokenTx.id] !== undefined &&
|
||||
$scope.Validator.isValidAddress($scope.tokenTx.to) &&
|
||||
$scope.Validator.isPositiveNumber($scope.tokenTx.value)
|
||||
) {
|
||||
if ($scope.estimateTimer) clearTimeout($scope.estimateTimer);
|
||||
$scope.estimateTimer = setTimeout(function() {
|
||||
$scope.estimateGasLimit();
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
$scope.$watch(
|
||||
'tx',
|
||||
function(newValue, oldValue) {
|
||||
$scope.showRaw = false;
|
||||
if (
|
||||
oldValue.sendMode != newValue.sendMode &&
|
||||
newValue.sendMode == 'ether'
|
||||
) {
|
||||
$scope.tx.data = '';
|
||||
$scope.tx.gasLimit = globalFuncs.defaultTxGasLimit;
|
||||
}
|
||||
if (
|
||||
newValue.gasLimit == oldValue.gasLimit &&
|
||||
$scope.wallet &&
|
||||
$scope.Validator.isValidAddress($scope.tx.to) &&
|
||||
$scope.Validator.isPositiveNumber($scope.tx.value) &&
|
||||
$scope.Validator.isValidHex($scope.tx.data) &&
|
||||
$scope.tx.sendMode != 'token'
|
||||
) {
|
||||
if ($scope.estimateTimer) clearTimeout($scope.estimateTimer);
|
||||
$scope.estimateTimer = setTimeout(function() {
|
||||
$scope.estimateGasLimit();
|
||||
}, 500);
|
||||
}
|
||||
if ($scope.tx.sendMode == 'token') {
|
||||
$scope.tokenTx.to = $scope.tx.to;
|
||||
$scope.tokenTx.value = $scope.tx.value;
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
$scope.estimateGasLimit = function() {
|
||||
$scope.customGasMsg = '';
|
||||
if ($scope.gasLimitChanged) return;
|
||||
for (var i in $scope.customGas) {
|
||||
if ($scope.tx.to.toLowerCase() == $scope.customGas[i].to.toLowerCase()) {
|
||||
$scope.showAdvance = $scope.customGas[i].data != '' ? true : false;
|
||||
$scope.tx.gasLimit = $scope.customGas[i].gasLimit;
|
||||
$scope.tx.data = $scope.customGas[i].data;
|
||||
$scope.customGasMsg = $scope.customGas[i].msg != ''
|
||||
? $scope.customGas[i].msg
|
||||
: '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (globalFuncs.lightMode) {
|
||||
$scope.tx.gasLimit = globalFuncs.defaultTokenGasLimit;
|
||||
return;
|
||||
}
|
||||
var estObj = {
|
||||
to: $scope.tx.to,
|
||||
from: $scope.wallet.getAddressString(),
|
||||
value: ethFuncs.sanitizeHex(
|
||||
ethFuncs.decimalToHex(etherUnits.toWei($scope.tx.value, $scope.tx.unit))
|
||||
)
|
||||
};
|
||||
if ($scope.tx.data != '')
|
||||
estObj.data = ethFuncs.sanitizeHex($scope.tx.data);
|
||||
if ($scope.tx.sendMode == 'token') {
|
||||
estObj.to = $scope.wallet.tokenObjs[
|
||||
$scope.tokenTx.id
|
||||
].getContractAddress();
|
||||
estObj.data = $scope.wallet.tokenObjs[$scope.tokenTx.id].getData(
|
||||
$scope.tokenTx.to,
|
||||
$scope.tokenTx.value
|
||||
).data;
|
||||
estObj.value = '0x00';
|
||||
}
|
||||
ethFuncs.estimateGas(estObj, function(data) {
|
||||
uiFuncs.notifier.close();
|
||||
if (!data.error) {
|
||||
if (data.data == '-1')
|
||||
$scope.notifier.danger(globalFuncs.errorMsgs[21]);
|
||||
$scope.tx.gasLimit = data.data;
|
||||
} else $scope.notifier.danger(data.msg);
|
||||
});
|
||||
};
|
||||
var isEnough = function(valA, valB) {
|
||||
return new BigNumber(valA).lte(new BigNumber(valB));
|
||||
};
|
||||
$scope.hasEnoughBalance = function() {
|
||||
if ($scope.wallet.balance == 'loading') return false;
|
||||
return isEnough($scope.tx.value, $scope.wallet.balance);
|
||||
};
|
||||
$scope.generateTx = function() {
|
||||
if (!$scope.Validator.isValidAddress($scope.tx.to)) {
|
||||
$scope.notifier.danger(globalFuncs.errorMsgs[5]);
|
||||
return;
|
||||
}
|
||||
var txData = uiFuncs.getTxData($scope);
|
||||
if ($scope.tx.sendMode == 'token') {
|
||||
// if the amount of tokens you are trying to send > tokens you have, throw error
|
||||
if (
|
||||
!isEnough(
|
||||
$scope.tx.value,
|
||||
$scope.wallet.tokenObjs[$scope.tokenTx.id].balance
|
||||
)
|
||||
) {
|
||||
$scope.notifier.danger(globalFuncs.errorMsgs[0]);
|
||||
return;
|
||||
}
|
||||
txData.to = $scope.wallet.tokenObjs[
|
||||
$scope.tokenTx.id
|
||||
].getContractAddress();
|
||||
txData.data = $scope.wallet.tokenObjs[$scope.tokenTx.id].getData(
|
||||
$scope.tokenTx.to,
|
||||
$scope.tokenTx.value
|
||||
).data;
|
||||
txData.value = '0x00';
|
||||
}
|
||||
uiFuncs.generateTx(txData, function(rawTx) {
|
||||
if (!rawTx.isError) {
|
||||
$scope.rawTx = rawTx.rawTx;
|
||||
$scope.signedTx = rawTx.signedTx;
|
||||
$scope.showRaw = true;
|
||||
} else {
|
||||
$scope.showRaw = false;
|
||||
$scope.notifier.danger(rawTx.error);
|
||||
}
|
||||
if (!$scope.$$phase) $scope.$apply();
|
||||
});
|
||||
};
|
||||
$scope.sendTx = function() {
|
||||
$scope.sendTxModal.close();
|
||||
uiFuncs.sendTx($scope.signedTx, function(resp) {
|
||||
if (!resp.isError) {
|
||||
var bExStr = $scope.ajaxReq.type != nodes.nodeTypes.Custom
|
||||
? "<a class='strong' href='" +
|
||||
$scope.ajaxReq.blockExplorerTX.replace('[[txHash]]', resp.data) +
|
||||
"' class='strong' target='_blank'>View TX</a><br />"
|
||||
: '';
|
||||
var emailLink =
|
||||
'<a class="strong" href="mailto:support@myetherwallet.com?Subject=Issue%20regarding%20my%20TX%20&Body=Hi%20Taylor%2C%20%0A%0AI%20have%20a%20question%20concerning%20my%20transaction.%20%0A%0AI%20was%20attempting%20to%3A%0A-%20Send%20ETH%0A-%20Send%20Tokens%0A-%20Send%20via%20my%20Ledger%0A-%20Send%20via%20my%20TREZOR%0A-%20Send%20via%20the%20offline%20tab%0A%0AFrom%20address%3A%20%0A%0ATo%20address%3A%20%0A%0AUnfortunately%20it%3A%0A-%20Never%20showed%20on%20the%20blockchain%0A-%20Failed%20due%20to%20out%20of%20gas%0A-%20Failed%20for%20another%20reason%0A-%20Never%20showed%20up%20in%20the%20account%20I%20was%20sending%20to%0A%0A%5B%20INSERT%20MORE%20INFORMATION%20HERE%20%5D%0A%0AThank%20you%0A%0A' +
|
||||
'%0A%20TO%20' +
|
||||
$scope.tx.to +
|
||||
'%0A%20FROM%20' +
|
||||
$scope.wallet.getAddressString() +
|
||||
'%0A%20AMT%20' +
|
||||
$scope.tx.value +
|
||||
'%0A%20CUR%20' +
|
||||
$scope.unitReadable +
|
||||
'%0A%20NODE%20TYPE%20' +
|
||||
$scope.ajaxReq.type +
|
||||
'%0A%20TOKEN%20' +
|
||||
$scope.tx.tokenSymbol +
|
||||
'%0A%20TOKEN%20TO%20' +
|
||||
$scope.tokenTx.to +
|
||||
'%0A%20TOKEN%20AMT%20' +
|
||||
$scope.tokenTx.value +
|
||||
'%0A%20TOKEN%20CUR%20' +
|
||||
$scope.unitReadable +
|
||||
'%0A%20TX%20' +
|
||||
resp.data +
|
||||
'" target="_blank">Confused? Email Us.</a>';
|
||||
$scope.notifier.success(
|
||||
globalFuncs.successMsgs[2] +
|
||||
resp.data +
|
||||
'<p>' +
|
||||
bExStr +
|
||||
'</p><p>' +
|
||||
emailLink +
|
||||
'</p>'
|
||||
);
|
||||
$scope.wallet.setBalance(applyScope);
|
||||
if ($scope.tx.sendMode == 'token')
|
||||
$scope.wallet.tokenObjs[$scope.tokenTx.id].setBalance();
|
||||
} else {
|
||||
$scope.notifier.danger(resp.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
$scope.transferAllBalance = function() {
|
||||
if ($scope.tx.sendMode != 'token') {
|
||||
uiFuncs.transferAllBalance(
|
||||
$scope.wallet.getAddressString(),
|
||||
$scope.tx.gasLimit,
|
||||
function(resp) {
|
||||
if (!resp.isError) {
|
||||
$scope.tx.unit = resp.unit;
|
||||
$scope.tx.value = resp.value;
|
||||
} else {
|
||||
$scope.showRaw = false;
|
||||
$scope.notifier.danger(resp.error);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$scope.tx.value = $scope.wallet.tokenObjs[$scope.tokenTx.id].getBalance();
|
||||
}
|
||||
};
|
||||
};
|
||||
module.exports = sendTxCtrl;
|
@ -23,4 +23,8 @@ export default class BaseNode {
|
||||
async getTransactionCount(_address: string): Promise<string> {
|
||||
throw new Error('Implement me');
|
||||
}
|
||||
|
||||
async sendRawTx(_tx: string): Promise<string> {
|
||||
throw new Error('Implement me');
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,8 @@ import type {
|
||||
GetBalanceRequest,
|
||||
GetTokenBalanceRequest,
|
||||
EstimateGasRequest,
|
||||
GetTransactionCountRequest
|
||||
GetTransactionCountRequest,
|
||||
SendRawTxRequest
|
||||
} from './types';
|
||||
import type { Token } from 'config/data';
|
||||
|
||||
@ -18,6 +19,15 @@ function id(): string {
|
||||
return randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
export function sendRawTx(signedTx: string): SendRawTxRequest {
|
||||
return {
|
||||
id: id(),
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_sendRawTransaction',
|
||||
params: [signedTx]
|
||||
};
|
||||
}
|
||||
|
||||
export function estimateGas<T: *>(transaction: T): EstimateGasRequest {
|
||||
return {
|
||||
id: id(),
|
||||
|
@ -6,7 +6,8 @@ import RPCClient, {
|
||||
getBalance,
|
||||
estimateGas,
|
||||
getTransactionCount,
|
||||
getTokenBalance
|
||||
getTokenBalance,
|
||||
sendRawTx
|
||||
} from './client';
|
||||
import type { Token } from 'config/data';
|
||||
|
||||
@ -20,27 +21,30 @@ export default class RpcNode extends BaseNode {
|
||||
async getBalance(address: string): Promise<Big> {
|
||||
return this.client.call(getBalance(address)).then(response => {
|
||||
if (response.error) {
|
||||
throw new Error('getBalance error');
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
return new Big(Number(response.result));
|
||||
return new Big(String(response.result));
|
||||
});
|
||||
}
|
||||
|
||||
async estimateGas(transaction: TransactionWithoutGas): Promise<Big> {
|
||||
return this.client.call(estimateGas(transaction)).then(response => {
|
||||
if (response.error) {
|
||||
throw new Error('estimateGas error');
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
return new Big(Number(response.result));
|
||||
return new Big(String(response.result));
|
||||
});
|
||||
}
|
||||
|
||||
async getTokenBalance(address: string, token: Token): Promise<Big> {
|
||||
return this.client.call(getTokenBalance(address, token)).then(response => {
|
||||
if (response.error) {
|
||||
// TODO - Error handling
|
||||
return Big(0);
|
||||
}
|
||||
return new Big(response.result).div(new Big(10).pow(token.decimal));
|
||||
return new Big(String(response.result)).div(
|
||||
new Big(10).pow(token.decimal)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -53,15 +57,30 @@ export default class RpcNode extends BaseNode {
|
||||
if (item.error) {
|
||||
return new Big(0);
|
||||
}
|
||||
return new Big(item.result).div(new Big(10).pow(tokens[idx].decimal));
|
||||
return new Big(String(item.result)).div(
|
||||
new Big(10).pow(tokens[idx].decimal)
|
||||
);
|
||||
});
|
||||
});
|
||||
// TODO - Error handling
|
||||
}
|
||||
|
||||
async getTransactionCount(address: string): Promise<string> {
|
||||
return this.client.call(getTransactionCount(address)).then(response => {
|
||||
if (response.error) {
|
||||
throw new Error('getTransactionCount error');
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
return response.result;
|
||||
});
|
||||
}
|
||||
|
||||
async sendRawTx(signedTx: string): Promise<string> {
|
||||
return this.client.call(sendRawTx(signedTx)).then(response => {
|
||||
if (response.error) {
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
if (response.errorMessage) {
|
||||
throw new Error(response.errorMessage);
|
||||
}
|
||||
return response.result;
|
||||
});
|
||||
|
@ -1,8 +1,9 @@
|
||||
// @flow
|
||||
// don't use flow temporarily
|
||||
import type { TransactionWithoutGas } from 'libs/transaction';
|
||||
|
||||
type DATA = string;
|
||||
type QUANTITY = string;
|
||||
type TX = string;
|
||||
|
||||
export type DEFAULT_BLOCK = string | 'earliest' | 'latest' | 'pending';
|
||||
|
||||
@ -19,11 +20,19 @@ type JsonRpcError = {|
|
||||
}
|
||||
|};
|
||||
|
||||
export type JSONRPC2 = '2.0';
|
||||
|
||||
export type JsonRpcResponse = JsonRpcSuccess | JsonRpcError;
|
||||
|
||||
type RPCRequestBase = {
|
||||
id: string,
|
||||
jsonrpc: '2.0'
|
||||
jsonrpc: JSONRPC2,
|
||||
method: string
|
||||
};
|
||||
|
||||
export type SendRawTxRequest = RPCRequestBase & {
|
||||
method: 'eth_sendRawTransaction',
|
||||
params: [TX]
|
||||
};
|
||||
|
||||
export type GetBalanceRequest = RPCRequestBase & {
|
||||
|
@ -10,6 +10,13 @@ import type BaseNode from 'libs/nodes/base';
|
||||
import type { BaseWallet } from 'libs/wallet';
|
||||
import type { Token } from 'config/data';
|
||||
import type EthTx from 'ethereumjs-tx';
|
||||
import { toUnit } from 'libs/units';
|
||||
|
||||
export type BroadcastStatusTransaction = {
|
||||
isBroadcasting: boolean,
|
||||
signedTx: string,
|
||||
successfullyBroadcast: boolean
|
||||
};
|
||||
|
||||
// TODO: Enforce more bigs, or find better way to avoid ether vs wei for value
|
||||
export type TransactionWithoutGas = {|
|
||||
@ -154,3 +161,14 @@ export async function generateTransaction(
|
||||
signedTx: signedTx
|
||||
};
|
||||
}
|
||||
|
||||
// TODO determine best place for helper function
|
||||
export function getBalanceMinusGasCosts(
|
||||
weiGasLimit: Big,
|
||||
weiGasPrice: Big,
|
||||
weiBalance: Big
|
||||
): Big {
|
||||
const weiGasCosts = weiGasPrice.times(weiGasLimit);
|
||||
const weiBalanceMinusGasCosts = weiBalance.minus(weiGasCosts);
|
||||
return toUnit(weiBalanceMinusGasCosts, 'wei', 'ether');
|
||||
}
|
||||
|
@ -143,5 +143,5 @@ export function isValidRawTx(rawTx: RawTransaction): boolean {
|
||||
// Full length deterministic wallet paths from BIP32
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
||||
export function isValidPath(dPath: string) {
|
||||
return dPath.split('\'/').length === 4;
|
||||
return dPath.split("'/").length === 4;
|
||||
}
|
||||
|
@ -8,20 +8,24 @@ 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';
|
||||
export type State = {
|
||||
inst: ?BaseWallet,
|
||||
// in ETH
|
||||
balance: Big,
|
||||
tokens: {
|
||||
[string]: Big
|
||||
}
|
||||
},
|
||||
transactions: Array<BroadcastStatusTransaction>
|
||||
};
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
inst: null,
|
||||
balance: new Big(0),
|
||||
tokens: {}
|
||||
tokens: {},
|
||||
isBroadcasting: false,
|
||||
transactions: []
|
||||
};
|
||||
|
||||
function setWallet(state: State, action: SetWalletAction): State {
|
||||
@ -37,6 +41,69 @@ function setTokenBalances(state: State, action: SetTokenBalancesAction): State {
|
||||
return { ...state, tokens: { ...state.tokens, ...action.payload } };
|
||||
}
|
||||
|
||||
function handleUpdateTxArray(
|
||||
transactions: Array<BroadcastStatusTransaction>,
|
||||
broadcastStatusTx: BroadcastStatusTransaction,
|
||||
isBroadcasting: boolean,
|
||||
successfullyBroadcast: boolean
|
||||
): Array<BroadcastStatusTransaction> {
|
||||
return transactions.map(item => {
|
||||
if (item === broadcastStatusTx) {
|
||||
return { ...item, isBroadcasting, successfullyBroadcast };
|
||||
} else {
|
||||
return { ...item };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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(
|
||||
state.transactions,
|
||||
signedTx
|
||||
);
|
||||
if (existingTx) {
|
||||
const isBroadcasting = false;
|
||||
return handleUpdateTxArray(
|
||||
state.transactions,
|
||||
existingTx,
|
||||
isBroadcasting,
|
||||
successfullyBroadcast
|
||||
);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function handleBroadcastTxRequested(state: State, signedTx: string) {
|
||||
const existingTx = getTxFromBroadcastStatusTransactions(
|
||||
state.transactions,
|
||||
signedTx
|
||||
);
|
||||
const isBroadcasting = true;
|
||||
const successfullyBroadcast = false;
|
||||
if (!existingTx) {
|
||||
return state.transactions.concat([
|
||||
{
|
||||
signedTx,
|
||||
isBroadcasting,
|
||||
successfullyBroadcast
|
||||
}
|
||||
]);
|
||||
} else {
|
||||
return handleUpdateTxArray(
|
||||
state.transactions,
|
||||
existingTx,
|
||||
isBroadcasting,
|
||||
successfullyBroadcast
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function wallet(
|
||||
state: State = INITIAL_STATE,
|
||||
action: WalletAction
|
||||
@ -48,6 +115,30 @@ export function wallet(
|
||||
return setBalance(state, action);
|
||||
case 'WALLET_SET_TOKEN_BALANCES':
|
||||
return setTokenBalances(state, action);
|
||||
case 'WALLET_BROADCAST_TX_REQUESTED':
|
||||
return {
|
||||
...state,
|
||||
isBroadcasting: true,
|
||||
transactions: handleBroadcastTxRequested(state, action.payload.signedTx)
|
||||
};
|
||||
case 'WALLET_BROADCAST_TX_SUCCEEDED':
|
||||
return {
|
||||
...state,
|
||||
transactions: handleTxBroadcastCompleted(
|
||||
state,
|
||||
action.payload.signedTx,
|
||||
true
|
||||
)
|
||||
};
|
||||
case 'WALLET_BROADCAST_TX_FAILED':
|
||||
return {
|
||||
...state,
|
||||
transactions: handleTxBroadcastCompleted(
|
||||
state,
|
||||
action.payload.signedTx,
|
||||
false
|
||||
)
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import wallet from './wallet';
|
||||
import handleConfigChanges from './config';
|
||||
import deterministicWallets from './deterministicWallets';
|
||||
|
||||
|
||||
export default {
|
||||
bityTimeRemaining,
|
||||
handleConfigChanges,
|
||||
|
@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { takeEvery, call, apply, put, select, fork } from 'redux-saga/effects';
|
||||
import type { Effect } from 'redux-saga/effects';
|
||||
import { setWallet, setBalance, setTokenBalances } from 'actions/wallet';
|
||||
@ -19,41 +20,52 @@ import {
|
||||
import { BaseNode } from 'libs/nodes';
|
||||
import { getNodeLib } from 'selectors/config';
|
||||
import { getWalletInst, getTokens } from 'selectors/wallet';
|
||||
|
||||
import { determineKeystoreType } from 'libs/keystore';
|
||||
import TransactionSucceeded from 'components/ExtendedNotifications/TransactionSucceeded';
|
||||
import type { BroadcastTxRequestedAction } from 'actions/wallet';
|
||||
|
||||
function* updateAccountBalance() {
|
||||
const node: BaseNode = yield select(getNodeLib);
|
||||
const wallet: ?BaseWallet = yield select(getWalletInst);
|
||||
if (!wallet) {
|
||||
return;
|
||||
try {
|
||||
const wallet: ?BaseWallet = yield select(getWalletInst);
|
||||
if (!wallet) {
|
||||
return;
|
||||
}
|
||||
const node: BaseNode = yield select(getNodeLib);
|
||||
const address = yield wallet.getAddress();
|
||||
// network request
|
||||
let balance = yield apply(node, node.getBalance, [address]);
|
||||
yield put(setBalance(balance));
|
||||
} catch (error) {
|
||||
yield put({ type: 'updateAccountBalance_error', error });
|
||||
}
|
||||
const address = yield wallet.getAddress();
|
||||
let balance = yield apply(node, node.getBalance, [address]);
|
||||
yield put(setBalance(balance));
|
||||
}
|
||||
|
||||
function* updateTokenBalances() {
|
||||
const node: BaseNode = yield select(getNodeLib);
|
||||
const wallet: ?BaseWallet = yield select(getWalletInst);
|
||||
const tokens = yield select(getTokens);
|
||||
if (!wallet || !node) {
|
||||
return;
|
||||
try {
|
||||
const node: BaseNode = yield select(getNodeLib);
|
||||
const wallet: ?BaseWallet = yield select(getWalletInst);
|
||||
const tokens = yield select(getTokens);
|
||||
if (!wallet || !node) {
|
||||
return;
|
||||
}
|
||||
// FIXME handle errors
|
||||
const address = yield wallet.getAddress();
|
||||
// network request
|
||||
const tokenBalances = yield apply(node, node.getTokenBalances, [
|
||||
address,
|
||||
tokens
|
||||
]);
|
||||
yield put(
|
||||
setTokenBalances(
|
||||
tokens.reduce((acc, t, i) => {
|
||||
acc[t.symbol] = tokenBalances[i];
|
||||
return acc;
|
||||
}, {})
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
yield put({ type: 'UPDATE_TOKEN_BALANCE_FAILED', error });
|
||||
}
|
||||
// FIXME handle errors
|
||||
const address = yield wallet.getAddress();
|
||||
const tokenBalances = yield apply(node, node.getTokenBalances, [
|
||||
address,
|
||||
tokens
|
||||
]);
|
||||
yield put(
|
||||
setTokenBalances(
|
||||
tokens.reduce((acc, t, i) => {
|
||||
acc[t.symbol] = tokenBalances[i];
|
||||
return acc;
|
||||
}, {})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function* updateBalances() {
|
||||
@ -121,10 +133,38 @@ export function* unlockKeystore(
|
||||
}
|
||||
|
||||
// TODO: provide a more descriptive error than the two 'ERROR_6' (invalid pass) messages above
|
||||
|
||||
yield put(setWallet(wallet));
|
||||
}
|
||||
|
||||
function* broadcastTx(
|
||||
action: BroadcastTxRequestedAction
|
||||
): Generator<Effect, void, any> {
|
||||
const signedTx = action.payload.signedTx;
|
||||
try {
|
||||
const node: BaseNode = yield select(getNodeLib);
|
||||
const txHash = yield apply(node, node.sendRawTx, [signedTx]);
|
||||
yield put(
|
||||
showNotification('success', <TransactionSucceeded txHash={txHash} />, 0)
|
||||
);
|
||||
yield put({
|
||||
type: 'WALLET_BROADCAST_TX_SUCCEEDED',
|
||||
payload: {
|
||||
txHash,
|
||||
signedTx
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
yield put(showNotification('danger', String(error)));
|
||||
yield put({
|
||||
type: 'WALLET_BROADCAST_TX_FAILED',
|
||||
payload: {
|
||||
signedTx,
|
||||
error: String(error)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function* walletSaga(): Generator<Effect | Effect[], void, any> {
|
||||
// useful for development
|
||||
yield call(updateBalances);
|
||||
@ -132,6 +172,8 @@ export default function* walletSaga(): Generator<Effect | Effect[], void, any> {
|
||||
takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey),
|
||||
takeEvery('WALLET_UNLOCK_KEYSTORE', unlockKeystore),
|
||||
takeEvery('WALLET_SET', updateBalances),
|
||||
takeEvery('CUSTOM_TOKEN_ADD', updateTokenBalances)
|
||||
takeEvery('CUSTOM_TOKEN_ADD', updateTokenBalances),
|
||||
// $FlowFixMe but how do I specify param types here flow?
|
||||
takeEvery('WALLET_BROADCAST_TX_REQUESTED', broadcastTx)
|
||||
];
|
||||
}
|
||||
|
@ -4,6 +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';
|
||||
|
||||
export function getWalletInst(state: State): ?BaseWallet {
|
||||
return state.wallet.inst;
|
||||
@ -39,3 +40,20 @@ export function getTokenBalances(state: State): TokenBalance[] {
|
||||
custom: t.custom
|
||||
}));
|
||||
}
|
||||
|
||||
export function getTxFromState(
|
||||
state: State,
|
||||
signedTx: string
|
||||
): ?BroadcastStatusTransaction {
|
||||
const transactions = state.wallet.transactions;
|
||||
return getTxFromBroadcastStatusTransactions(transactions, signedTx);
|
||||
}
|
||||
|
||||
export function getTxFromBroadcastStatusTransactions(
|
||||
transactions: Array<BroadcastStatusTransaction>,
|
||||
signedTx: string
|
||||
): ?BroadcastStatusTransaction {
|
||||
return transactions.find(transaction => {
|
||||
return transaction.signedTx === signedTx;
|
||||
});
|
||||
}
|
||||
|
4
common/vendor/trezor-connect.js
vendored
4
common/vendor/trezor-connect.js
vendored
@ -715,7 +715,7 @@ function TrezorConnect() {
|
||||
|
||||
var LOGIN_ONCLICK =
|
||||
'TrezorConnect.requestLogin(' +
|
||||
'\'@hosticon@\',\'@challenge_hidden@\',\'@challenge_visual@\',\'@callback@\'' +
|
||||
"'@hosticon@','@challenge_hidden@','@challenge_visual@','@callback@'" +
|
||||
')';
|
||||
|
||||
var LOGIN_HTML =
|
||||
@ -775,7 +775,7 @@ function parseHDPath(string) {
|
||||
})
|
||||
.map(function(p) {
|
||||
var hardened = false;
|
||||
if (p[p.length - 1] === '\'') {
|
||||
if (p[p.length - 1] === "'") {
|
||||
hardened = true;
|
||||
p = p.substr(0, p.length - 1);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ describe('Validator', () => {
|
||||
});
|
||||
|
||||
it('should validate a correct DPath as true', () => {
|
||||
expect(isValidPath('m/44\'/60\'/0\'/0')).toBeTruthy();
|
||||
expect(isValidPath("m/44'/60'/0'/0")).toBeTruthy();
|
||||
});
|
||||
it('should validate an incorrect DPath as false', () => {
|
||||
expect(isValidPath('m/44/60/0/0')).toBeFalsy();
|
||||
|
Loading…
x
Reference in New Issue
Block a user