diff --git a/common/containers/Tabs/SendTransaction/components/ConfirmationModal.jsx b/common/containers/Tabs/SendTransaction/components/ConfirmationModal.jsx
index 196280cf..90a312d5 100644
--- a/common/containers/Tabs/SendTransaction/components/ConfirmationModal.jsx
+++ b/common/containers/Tabs/SendTransaction/components/ConfirmationModal.jsx
@@ -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 (
-
-
-
-
-
-
-
-
- {value} {symbol}
-
-
-
-
-
-
+ {
+
+ {isBroadcasting
+ ?
+
+
+ :
+
+
+
+
+
+
+
+ {value} {symbol}
+
+
+
+
+
+
-
- -
- You are sending from
{fromAddress}
-
- -
- You are sending to
{toAddress}
-
- -
- You are sending{' '}
-
- {value} {symbol}
- {' '}
- with a gas price of {gasPrice} gwei
-
- -
- You are interacting with the {node.network}{' '}
- network provided by {node.service}
-
- {!token &&
- -
- {data
- ?
- You are sending the following data:{' '}
-
-
- : 'There is no data attached to this transaction'}
-
}
-
+
+ -
+ You are sending from
{fromAddress}
+
+ -
+ You are sending to
{toAddress}
+
+ -
+ You are sending{' '}
+
+ {value} {symbol}
+ {' '}
+ with a gas price of {gasPrice} gwei
+
+ -
+ You are interacting with the{' '}
+ {node.network} network provided by{' '}
+ {node.service}
+
+ {!token &&
+ -
+ {data
+ ?
+ You are sending the following data:{' '}
+
+
+ : 'There is no data attached to this transaction'}
+
}
+
-
- {translate('SENDModal_Content_3')}
+
+ {translate('SENDModal_Content_3')}
+
+
}
-
+ }
);
}
}
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,
diff --git a/common/containers/Tabs/SendTransaction/components/ConfirmationModal.scss b/common/containers/Tabs/SendTransaction/components/ConfirmationModal.scss
index f9a05437..d59c1f64 100644
--- a/common/containers/Tabs/SendTransaction/components/ConfirmationModal.scss
+++ b/common/containers/Tabs/SendTransaction/components/ConfirmationModal.scss
@@ -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
+ }
+
}
diff --git a/common/containers/Tabs/SendTransaction/index.jsx b/common/containers/Tabs/SendTransaction/index.jsx
index f6985dbb..e20a8b53 100644
--- a/common/containers/Tabs/SendTransaction/index.jsx
+++ b/common/containers/Tabs/SendTransaction/index.jsx
@@ -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
+};
+
+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 &&
- {'' /* */}
+ {/* */}
@@ -180,19 +212,6 @@ export class SendTransaction extends React.Component {
- {readOnly &&
- !hasEnoughBalance &&
-
-
-
- Warning! You do not have enough funds to complete this
- swap.
-
-
- Please add more funds or access a different wallet.
-
-
}
-
{translate('SEND_trans')}
@@ -225,12 +244,13 @@ export class SendTransaction extends React.Component {
@@ -262,12 +282,12 @@ export class SendTransaction extends React.Component {
}
@@ -279,8 +299,8 @@ export class SendTransaction extends React.Component {
}
@@ -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
+);
diff --git a/common/containers/Tabs/SendTransaction/ref.js b/common/containers/Tabs/SendTransaction/ref.js
deleted file mode 100644
index 50775dd4..00000000
--- a/common/containers/Tabs/SendTransaction/ref.js
+++ /dev/null
@@ -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
- ? "View TX
"
- : '';
- var emailLink =
- 'Confused? Email Us.';
- $scope.notifier.success(
- globalFuncs.successMsgs[2] +
- resp.data +
- '' +
- bExStr +
- '
' +
- emailLink +
- '
'
- );
- $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;
diff --git a/common/libs/nodes/base.js b/common/libs/nodes/base.js
index 6e869a19..78d08c17 100644
--- a/common/libs/nodes/base.js
+++ b/common/libs/nodes/base.js
@@ -23,4 +23,8 @@ export default class BaseNode {
async getTransactionCount(_address: string): Promise {
throw new Error('Implement me');
}
+
+ async sendRawTx(_tx: string): Promise {
+ throw new Error('Implement me');
+ }
}
diff --git a/common/libs/nodes/rpc/client.js b/common/libs/nodes/rpc/client.js
index 0d19af86..87930d74 100644
--- a/common/libs/nodes/rpc/client.js
+++ b/common/libs/nodes/rpc/client.js
@@ -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(transaction: T): EstimateGasRequest {
return {
id: id(),
diff --git a/common/libs/nodes/rpc/index.js b/common/libs/nodes/rpc/index.js
index 5c4a2de2..432ce972 100644
--- a/common/libs/nodes/rpc/index.js
+++ b/common/libs/nodes/rpc/index.js
@@ -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 {
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 {
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 {
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 {
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 {
+ 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;
});
diff --git a/common/libs/nodes/rpc/types.js b/common/libs/nodes/rpc/types.js
index 9208021a..edcdc90e 100644
--- a/common/libs/nodes/rpc/types.js
+++ b/common/libs/nodes/rpc/types.js
@@ -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 & {
diff --git a/common/libs/transaction.js b/common/libs/transaction.js
index 0a87b372..9b649c82 100644
--- a/common/libs/transaction.js
+++ b/common/libs/transaction.js
@@ -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');
+}
diff --git a/common/libs/validators.js b/common/libs/validators.js
index 0798a70b..e4b4cdb9 100644
--- a/common/libs/validators.js
+++ b/common/libs/validators.js
@@ -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;
}
diff --git a/common/reducers/wallet.js b/common/reducers/wallet.js
index 842b8dbe..d131d766 100644
--- a/common/reducers/wallet.js
+++ b/common/reducers/wallet.js
@@ -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
};
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,
+ broadcastStatusTx: BroadcastStatusTransaction,
+ isBroadcasting: boolean,
+ successfullyBroadcast: boolean
+): Array {
+ 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 but can't.
+): Array {
+ 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;
}
diff --git a/common/sagas/index.js b/common/sagas/index.js
index 7266e2f4..3ada3f1c 100644
--- a/common/sagas/index.js
+++ b/common/sagas/index.js
@@ -12,7 +12,6 @@ import wallet from './wallet';
import handleConfigChanges from './config';
import deterministicWallets from './deterministicWallets';
-
export default {
bityTimeRemaining,
handleConfigChanges,
diff --git a/common/sagas/wallet.js b/common/sagas/wallet.js
index 317274af..cbb50850 100644
--- a/common/sagas/wallet.js
+++ b/common/sagas/wallet.js
@@ -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 {
+ const signedTx = action.payload.signedTx;
+ try {
+ const node: BaseNode = yield select(getNodeLib);
+ const txHash = yield apply(node, node.sendRawTx, [signedTx]);
+ yield put(
+ showNotification('success', , 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 {
// useful for development
yield call(updateBalances);
@@ -132,6 +172,8 @@ export default function* walletSaga(): Generator {
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)
];
}
diff --git a/common/selectors/wallet.js b/common/selectors/wallet.js
index bacc474d..096e0260 100644
--- a/common/selectors/wallet.js
+++ b/common/selectors/wallet.js
@@ -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,
+ signedTx: string
+): ?BroadcastStatusTransaction {
+ return transactions.find(transaction => {
+ return transaction.signedTx === signedTx;
+ });
+}
diff --git a/common/vendor/trezor-connect.js b/common/vendor/trezor-connect.js
index 2cd3f46b..5317ef9e 100644
--- a/common/vendor/trezor-connect.js
+++ b/common/vendor/trezor-connect.js
@@ -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);
}
diff --git a/spec/libs/validators.spec.js b/spec/libs/validators.spec.js
index 3b8663bb..4637b147 100644
--- a/spec/libs/validators.spec.js
+++ b/spec/libs/validators.spec.js
@@ -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();