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:
Daniel Ternyak 2017-08-30 21:00:31 -07:00 committed by GitHub
parent ae0ada9c06
commit a4ec6f6139
24 changed files with 566 additions and 573 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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)
);
};
}

View File

@ -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;

View File

@ -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'

View 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;

View File

@ -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">

View File

@ -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,

View File

@ -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
}
}

View File

@ -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
);

View File

@ -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;

View File

@ -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');
}
}

View File

@ -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(),

View File

@ -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;
});

View File

@ -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 & {

View File

@ -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');
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -12,7 +12,6 @@ import wallet from './wallet';
import handleConfigChanges from './config';
import deterministicWallets from './deterministicWallets';
export default {
bityTimeRemaining,
handleConfigChanges,

View File

@ -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)
];
}

View File

@ -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;
});
}

View File

@ -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);
}

View File

@ -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();