mirror of
https://github.com/trezor/trezor-wallet
synced 2024-11-24 09:18:09 +00:00
commit
1f7a4eca5e
@ -1,42 +1,26 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import Web3 from 'web3';
|
|
||||||
import HDKey from 'hdkey';
|
|
||||||
|
|
||||||
import EthereumjsUtil from 'ethereumjs-util';
|
|
||||||
import EthereumjsUnits from 'ethereumjs-units';
|
|
||||||
import EthereumjsTx from 'ethereumjs-tx';
|
|
||||||
import TrezorConnect from 'trezor-connect';
|
import TrezorConnect from 'trezor-connect';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
import type { EstimateGasOptions } from 'web3';
|
|
||||||
import type { TransactionStatus, TransactionReceipt } from 'web3';
|
|
||||||
import { strip } from 'utils/ethUtils';
|
|
||||||
import * as BLOCKCHAIN from 'actions/constants/blockchain';
|
import * as BLOCKCHAIN from 'actions/constants/blockchain';
|
||||||
import * as WEB3 from 'actions/constants/web3';
|
|
||||||
import * as PENDING from 'actions/constants/pendingTx';
|
|
||||||
|
|
||||||
import * as AccountsActions from './AccountsActions';
|
|
||||||
import * as Web3Actions from './Web3Actions';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
TrezorDevice,
|
TrezorDevice,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
GetState,
|
GetState,
|
||||||
Action,
|
|
||||||
AsyncAction,
|
|
||||||
PromiseAction,
|
PromiseAction,
|
||||||
ThunkAction,
|
|
||||||
} from 'flowtype';
|
} from 'flowtype';
|
||||||
import type { EthereumAccount } from 'trezor-connect';
|
import type { EthereumAccount } from 'trezor-connect';
|
||||||
import type { Token } from 'reducers/TokensReducer';
|
import type { Token } from 'reducers/TokensReducer';
|
||||||
import type { NetworkToken } from 'reducers/LocalStorageReducer';
|
import type { NetworkToken } from 'reducers/LocalStorageReducer';
|
||||||
|
import * as Web3Actions from './Web3Actions';
|
||||||
|
import * as AccountsActions from './AccountsActions';
|
||||||
|
|
||||||
export type BlockchainAction = {
|
export type BlockchainAction = {
|
||||||
type: typeof BLOCKCHAIN.READY,
|
type: typeof BLOCKCHAIN.READY,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const discoverAccount = (device: TrezorDevice, address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch, getState: GetState): Promise<EthereumAccount> => {
|
export const discoverAccount = (device: TrezorDevice, address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
|
||||||
// get data from connect
|
// get data from connect
|
||||||
// Temporary disabled, enable after trezor-connect@5.0.32 release
|
// Temporary disabled, enable after trezor-connect@5.0.32 release
|
||||||
const txs = await TrezorConnect.ethereumGetAccountInfo({
|
const txs = await TrezorConnect.ethereumGetAccountInfo({
|
||||||
@ -44,8 +28,8 @@ export const discoverAccount = (device: TrezorDevice, address: string, network:
|
|||||||
address,
|
address,
|
||||||
block: 0,
|
block: 0,
|
||||||
transactions: 0,
|
transactions: 0,
|
||||||
balance: "0",
|
balance: '0',
|
||||||
nonce: 0
|
nonce: 0,
|
||||||
},
|
},
|
||||||
coin: network,
|
coin: network,
|
||||||
});
|
});
|
||||||
@ -55,7 +39,7 @@ export const discoverAccount = (device: TrezorDevice, address: string, network:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// blockbook web3 fallback
|
// blockbook web3 fallback
|
||||||
const web3account = await dispatch( Web3Actions.discoverAccount(address, network) );
|
const web3account = await dispatch(Web3Actions.discoverAccount(address, network));
|
||||||
// return { transactions: txs.payload, ...web3account };
|
// return { transactions: txs.payload, ...web3account };
|
||||||
return {
|
return {
|
||||||
address,
|
address,
|
||||||
@ -66,35 +50,53 @@ export const discoverAccount = (device: TrezorDevice, address: string, network:
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTokenInfo = (input: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch, getState: GetState): Promise<NetworkToken> => {
|
export const getTokenInfo = (input: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch): Promise<NetworkToken> => dispatch(Web3Actions.getTokenInfo(input, network));
|
||||||
return await dispatch( Web3Actions.getTokenInfo(input, network) );
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
|
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => dispatch(Web3Actions.getTokenBalance(token));
|
||||||
return await dispatch( Web3Actions.getTokenBalance(token) );
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getGasPrice = (network: string, defaultGasPrice: number): PromiseAction<BigNumber> => async (dispatch: Dispatch, getState: GetState): Promise<BigNumber> => {
|
export const getGasPrice = (network: string, defaultGasPrice: number): PromiseAction<BigNumber> => async (dispatch: Dispatch): Promise<BigNumber> => {
|
||||||
try {
|
try {
|
||||||
const gasPrice = await dispatch( Web3Actions.getCurrentGasPrice(network) );
|
const gasPrice = await dispatch(Web3Actions.getCurrentGasPrice(network));
|
||||||
return new BigNumber(gasPrice);
|
return new BigNumber(gasPrice);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return new BigNumber(defaultGasPrice);
|
return new BigNumber(defaultGasPrice);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const estimateGasLimit = (network: string, data: string, value: string, gasPrice: string): PromiseAction<number> => async (dispatch: Dispatch, getState: GetState): Promise<number> => {
|
const estimateProxy: Array<Promise<string>> = [];
|
||||||
return await dispatch( Web3Actions.estimateGasLimit(network, { to: '', data, value, gasPrice }) );
|
export const estimateGasLimit = (network: string, data: string, value: string, gasPrice: string): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => {
|
||||||
}
|
// Since this method could be called multiple times in short period of time
|
||||||
|
// check for pending calls in proxy and if there more than two (first is current running and the second is waiting for result of first)
|
||||||
|
// TODO: should reject second call immediately?
|
||||||
|
if (estimateProxy.length > 0) {
|
||||||
|
// wait for proxy result (but do not process it)
|
||||||
|
await estimateProxy[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const call = dispatch(Web3Actions.estimateGasLimit(network, {
|
||||||
|
to: '',
|
||||||
|
data,
|
||||||
|
value,
|
||||||
|
gasPrice,
|
||||||
|
}));
|
||||||
|
// add current call to proxy
|
||||||
|
estimateProxy.push(call);
|
||||||
|
// wait for result
|
||||||
|
const result = await call;
|
||||||
|
// remove current call from proxy
|
||||||
|
estimateProxy.splice(0, 1);
|
||||||
|
// return result
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
export const onBlockMined = (coinInfo: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
export const onBlockMined = (coinInfo: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
// incoming "coinInfo" from TrezorConnect is CoinInfo | EthereumNetwork type
|
// incoming "coinInfo" from TrezorConnect is CoinInfo | EthereumNetwork type
|
||||||
const network: string = coinInfo.shortcut.toLowerCase();
|
const network: string = coinInfo.shortcut.toLowerCase();
|
||||||
|
|
||||||
// try to resolve pending transactions
|
// try to resolve pending transactions
|
||||||
await dispatch( Web3Actions.resolvePendingTransactions(network) );
|
await dispatch(Web3Actions.resolvePendingTransactions(network));
|
||||||
|
|
||||||
await dispatch( Web3Actions.updateGasPrice(network) );
|
await dispatch(Web3Actions.updateGasPrice(network));
|
||||||
|
|
||||||
const accounts: Array<any> = getState().accounts.filter(a => a.network === network);
|
const accounts: Array<any> = getState().accounts.filter(a => a.network === network);
|
||||||
if (accounts.length > 0) {
|
if (accounts.length > 0) {
|
||||||
@ -108,34 +110,32 @@ export const onBlockMined = (coinInfo: any): PromiseAction<void> => async (dispa
|
|||||||
response.payload.forEach((a, i) => {
|
response.payload.forEach((a, i) => {
|
||||||
if (a.transactions > 0) {
|
if (a.transactions > 0) {
|
||||||
// load additional data from Web3 (balance, nonce, tokens)
|
// load additional data from Web3 (balance, nonce, tokens)
|
||||||
dispatch( Web3Actions.updateAccount(accounts[i], a, network) )
|
dispatch(Web3Actions.updateAccount(accounts[i], a, network));
|
||||||
} else {
|
} else {
|
||||||
// there are no new txs, just update block
|
// there are no new txs, just update block
|
||||||
dispatch( AccountsActions.update( { ...accounts[i], block: a.block }) );
|
dispatch(AccountsActions.update({ ...accounts[i], block: a.block }));
|
||||||
|
|
||||||
// HACK: since blockbook can't work with smart contracts for now
|
// HACK: since blockbook can't work with smart contracts for now
|
||||||
// try to update tokens balances added to this account using Web3
|
// try to update tokens balances added to this account using Web3
|
||||||
dispatch( Web3Actions.updateAccountTokens(accounts[i]) );
|
dispatch(Web3Actions.updateAccountTokens(accounts[i]));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
// not used for now, waiting for fix in blockbook
|
// not used for now, waiting for fix in blockbook
|
||||||
|
/*
|
||||||
export const onNotification = (payload: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
export const onNotification = (payload: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
|
|
||||||
// this event can be triggered multiple times
|
// this event can be triggered multiple times
|
||||||
// 1. check if pair [txid + address] is already in reducer
|
// 1. check if pair [txid + address] is already in reducer
|
||||||
const network: string = payload.coin.shortcut.toLowerCase();
|
const network: string = payload.coin.shortcut.toLowerCase();
|
||||||
const address: string = EthereumjsUtil.toChecksumAddress(payload.tx.address);
|
const address: string = EthereumjsUtil.toChecksumAddress(payload.tx.address);
|
||||||
const txInfo = await dispatch( Web3Actions.getPendingInfo(network, payload.tx.txid) );
|
const txInfo = await dispatch(Web3Actions.getPendingInfo(network, payload.tx.txid));
|
||||||
|
|
||||||
// const exists = getState().pending.filter(p => p.id === payload.tx.txid && p.address === address);
|
// const exists = getState().pending.filter(p => p.id === payload.tx.txid && p.address === address);
|
||||||
const exists = getState().pending.filter(p => {
|
const exists = getState().pending.filter(p => p.address === address);
|
||||||
return p.address === address;
|
|
||||||
});
|
|
||||||
if (exists.length < 1) {
|
if (exists.length < 1) {
|
||||||
if (txInfo) {
|
if (txInfo) {
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -144,14 +144,14 @@ export const onNotification = (payload: any): PromiseAction<void> => async (disp
|
|||||||
type: 'send',
|
type: 'send',
|
||||||
id: payload.tx.txid,
|
id: payload.tx.txid,
|
||||||
network,
|
network,
|
||||||
currency: "tETH",
|
currency: 'tETH',
|
||||||
amount: txInfo.value,
|
amount: txInfo.value,
|
||||||
total: "0",
|
total: '0',
|
||||||
tx: {},
|
tx: {},
|
||||||
nonce: txInfo.nonce,
|
nonce: txInfo.nonce,
|
||||||
address,
|
address,
|
||||||
rejected: false
|
rejected: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// tx info not found (yet?)
|
// tx info not found (yet?)
|
||||||
@ -164,18 +164,18 @@ export const onNotification = (payload: any): PromiseAction<void> => async (disp
|
|||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
export const subscribe = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
export const subscribe = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
const addresses: Array<string> = getState().accounts.filter(a => a.network === network).map(a => a.address);
|
const addresses: Array<string> = getState().accounts.filter(a => a.network === network).map(a => a.address); // eslint-disable-line no-unused-vars
|
||||||
// $FlowIssue: trezor-connect@5.0.32
|
await TrezorConnect.blockchainSubscribe({
|
||||||
return await TrezorConnect.blockchainSubscribe({
|
|
||||||
// accounts: addresses,
|
// accounts: addresses,
|
||||||
accounts: [],
|
accounts: [],
|
||||||
coin: network
|
coin: network,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// Conditionally subscribe to blockchain backend
|
// Conditionally subscribe to blockchain backend
|
||||||
// called after TrezorConnect.init successfully emits TRANSPORT.START event
|
// called after TrezorConnect.init successfully emits TRANSPORT.START event
|
||||||
@ -185,26 +185,26 @@ export const init = (): PromiseAction<void> => async (dispatch: Dispatch, getSta
|
|||||||
if (getState().discovery.length > 0) {
|
if (getState().discovery.length > 0) {
|
||||||
// get unique networks
|
// get unique networks
|
||||||
const networks: Array<string> = [];
|
const networks: Array<string> = [];
|
||||||
getState().discovery.forEach(discovery => {
|
getState().discovery.forEach((discovery) => {
|
||||||
if (networks.indexOf(discovery.network) < 0) {
|
if (networks.indexOf(discovery.network) < 0) {
|
||||||
networks.push(discovery.network);
|
networks.push(discovery.network);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// subscribe
|
// subscribe
|
||||||
for (let i = 0; i < networks.length; i++) {
|
const results = networks.map(n => dispatch(subscribe(n)));
|
||||||
await dispatch( subscribe(networks[i]) );
|
// wait for all subscriptions
|
||||||
}
|
await Promise.all(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
// continue wallet initialization
|
// continue wallet initialization
|
||||||
dispatch({
|
dispatch({
|
||||||
type: BLOCKCHAIN.READY
|
type: BLOCKCHAIN.READY,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// Handle BLOCKCHAIN.ERROR event from TrezorConnect
|
// Handle BLOCKCHAIN.ERROR event from TrezorConnect
|
||||||
// disconnect and remove Web3 webscocket instance if exists
|
// disconnect and remove Web3 webscocket instance if exists
|
||||||
export const error = (payload: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
export const error = (payload: any): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||||
dispatch( Web3Actions.disconnect(payload.coin) );
|
dispatch(Web3Actions.disconnect(payload.coin));
|
||||||
}
|
};
|
@ -3,13 +3,9 @@
|
|||||||
|
|
||||||
import { LOCATION_CHANGE } from 'react-router-redux';
|
import { LOCATION_CHANGE } from 'react-router-redux';
|
||||||
import * as ACCOUNT from 'actions/constants/account';
|
import * as ACCOUNT from 'actions/constants/account';
|
||||||
import * as SEND from 'actions/constants/send';
|
|
||||||
import * as NOTIFICATION from 'actions/constants/notification';
|
import * as NOTIFICATION from 'actions/constants/notification';
|
||||||
import * as PENDING from 'actions/constants/pendingTx';
|
import * as PENDING from 'actions/constants/pendingTx';
|
||||||
|
|
||||||
import * as SendFormActions from 'actions/SendFormActions';
|
|
||||||
import * as SessionStorageActions from 'actions/SessionStorageActions';
|
|
||||||
|
|
||||||
import * as stateUtils from 'reducers/utils';
|
import * as stateUtils from 'reducers/utils';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -36,17 +32,6 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct
|
|||||||
const state: State = getState();
|
const state: State = getState();
|
||||||
const { location } = state.router;
|
const { location } = state.router;
|
||||||
|
|
||||||
// reset form to default
|
|
||||||
if (action.type === SEND.TX_COMPLETE) {
|
|
||||||
// dispatch( SendFormActions.init() );
|
|
||||||
// linear action
|
|
||||||
// SessionStorageActions.clear(location.pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevState.sendForm !== state.sendForm) {
|
|
||||||
dispatch(SessionStorageActions.save());
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle devices state change (from trezor-connect events or location change)
|
// handle devices state change (from trezor-connect events or location change)
|
||||||
if (locationChange
|
if (locationChange
|
||||||
|| prevState.accounts !== state.accounts
|
|| prevState.accounts !== state.accounts
|
||||||
@ -85,11 +70,6 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct
|
|||||||
payload,
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
// initialize SendFormReducer
|
|
||||||
if (location.state.send && getState().sendForm.currency === '') {
|
|
||||||
dispatch(SendFormActions.init());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (location.state.send) {
|
if (location.state.send) {
|
||||||
const rejectedTxs = pending.filter(tx => tx.rejected);
|
const rejectedTxs = pending.filter(tx => tx.rejected);
|
||||||
rejectedTxs.forEach((tx) => {
|
rejectedTxs.forEach((tx) => {
|
||||||
|
File diff suppressed because it is too large
Load Diff
413
src/actions/SendFormValidationActions.js
Normal file
413
src/actions/SendFormValidationActions.js
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
/* @flow */
|
||||||
|
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import EthereumjsUtil from 'ethereumjs-util';
|
||||||
|
import EthereumjsUnits from 'ethereumjs-units';
|
||||||
|
import { findToken } from 'reducers/TokensReducer';
|
||||||
|
import { findDevice, getPendingAmount } from 'reducers/utils';
|
||||||
|
import * as SEND from 'actions/constants/send';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Dispatch,
|
||||||
|
GetState,
|
||||||
|
PayloadAction,
|
||||||
|
} from 'flowtype';
|
||||||
|
import type { State, FeeLevel } from 'reducers/SendFormReducer';
|
||||||
|
|
||||||
|
// general regular expressions
|
||||||
|
const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$');
|
||||||
|
const UPPERCASE_RE = new RegExp('^(.*[A-Z].*)$');
|
||||||
|
const ABS_RE = new RegExp('^[0-9]+$');
|
||||||
|
const ETH_18_RE = new RegExp('^(0|0\\.([0-9]{0,18})?|[1-9][0-9]*\\.?([0-9]{0,18})?|\\.[0-9]{0,18})$');
|
||||||
|
const HEX_RE = new RegExp('^[0-9A-Fa-f]+$');
|
||||||
|
const dynamicRegexp = (decimals: number): RegExp => {
|
||||||
|
if (decimals > 0) {
|
||||||
|
return new RegExp(`^(0|0\\.([0-9]{0,${decimals}})?|[1-9][0-9]*\\.?([0-9]{0,${decimals}})?|\\.[0-9]{1,${decimals}})$`);
|
||||||
|
}
|
||||||
|
return ABS_RE;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Called from SendFormActions.observe
|
||||||
|
* Reaction for WEB3.GAS_PRICE_UPDATED action
|
||||||
|
*/
|
||||||
|
export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAction<void> => (dispatch: Dispatch, getState: GetState): void => {
|
||||||
|
// testing random data
|
||||||
|
// function getRandomInt(min, max) {
|
||||||
|
// return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
// }
|
||||||
|
// const newPrice = getRandomInt(10, 50).toString();
|
||||||
|
|
||||||
|
const state = getState().sendForm;
|
||||||
|
if (network === state.networkSymbol) return;
|
||||||
|
|
||||||
|
// check if new price is different then currently recommended
|
||||||
|
const newPrice: string = EthereumjsUnits.convert(gasPrice, 'wei', 'gwei');
|
||||||
|
|
||||||
|
if (newPrice !== state.recommendedGasPrice) {
|
||||||
|
if (!state.untouched) {
|
||||||
|
// if there is a transaction draft let the user know
|
||||||
|
// and let him update manually
|
||||||
|
dispatch({
|
||||||
|
type: SEND.CHANGE,
|
||||||
|
state: {
|
||||||
|
...state,
|
||||||
|
gasPriceNeedsUpdate: true,
|
||||||
|
recommendedGasPrice: newPrice,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// automatically update feeLevels and gasPrice
|
||||||
|
const feeLevels = getFeeLevels(state.networkSymbol, newPrice, state.gasLimit);
|
||||||
|
const selectedFeeLevel = getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
|
||||||
|
dispatch({
|
||||||
|
type: SEND.CHANGE,
|
||||||
|
state: {
|
||||||
|
...state,
|
||||||
|
gasPriceNeedsUpdate: false,
|
||||||
|
recommendedGasPrice: newPrice,
|
||||||
|
gasPrice: selectedFeeLevel.gasPrice,
|
||||||
|
feeLevels,
|
||||||
|
selectedFeeLevel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Recalculate amount, total and fees
|
||||||
|
*/
|
||||||
|
export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||||
|
// clone deep nested object
|
||||||
|
// to avoid overrides across state history
|
||||||
|
let state: State = JSON.parse(JSON.stringify(getState().sendForm));
|
||||||
|
// reset errors
|
||||||
|
state.errors = {};
|
||||||
|
state.warnings = {};
|
||||||
|
state.infos = {};
|
||||||
|
state = dispatch(recalculateTotalAmount(state));
|
||||||
|
state = dispatch(updateCustomFeeLabel(state));
|
||||||
|
state = dispatch(addressValidation(state));
|
||||||
|
state = dispatch(addressLabel(state));
|
||||||
|
state = dispatch(amountValidation(state));
|
||||||
|
state = dispatch(gasLimitValidation(state));
|
||||||
|
state = dispatch(gasPriceValidation(state));
|
||||||
|
state = dispatch(nonceValidation(state));
|
||||||
|
state = dispatch(dataValidation(state));
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||||
|
const {
|
||||||
|
account,
|
||||||
|
tokens,
|
||||||
|
pending,
|
||||||
|
} = getState().selectedAccount;
|
||||||
|
if (!account) return $state;
|
||||||
|
|
||||||
|
const state = { ...$state };
|
||||||
|
const isToken = state.currency !== state.networkSymbol;
|
||||||
|
|
||||||
|
if (state.setMax) {
|
||||||
|
const pendingAmount = getPendingAmount(pending, state.currency, isToken);
|
||||||
|
if (isToken) {
|
||||||
|
const token = findToken(tokens, account.address, state.currency, account.deviceState);
|
||||||
|
if (token) {
|
||||||
|
state.amount = new BigNumber(token.balance).minus(pendingAmount).toString(10);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const b = new BigNumber(account.balance).minus(pendingAmount);
|
||||||
|
state.amount = calculateMaxAmount(b, state.gasPrice, state.gasLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit);
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateCustomFeeLabel = ($state: State): PayloadAction<State> => (): State => {
|
||||||
|
const state = { ...$state };
|
||||||
|
if ($state.selectedFeeLevel.value === 'Custom') {
|
||||||
|
state.selectedFeeLevel = {
|
||||||
|
...state.selectedFeeLevel,
|
||||||
|
gasPrice: state.gasPrice,
|
||||||
|
label: `${calculateFee(state.gasPrice, state.gasLimit)} ${state.networkSymbol}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Address value validation
|
||||||
|
*/
|
||||||
|
export const addressValidation = ($state: State): PayloadAction<State> => (): State => {
|
||||||
|
const state = { ...$state };
|
||||||
|
if (!state.touched.address) return state;
|
||||||
|
|
||||||
|
const { address } = state;
|
||||||
|
|
||||||
|
if (address.length < 1) {
|
||||||
|
state.errors.address = 'Address is not set';
|
||||||
|
} else if (!EthereumjsUtil.isValidAddress(address)) {
|
||||||
|
state.errors.address = 'Address is not valid';
|
||||||
|
} else if (address.match(UPPERCASE_RE) && !EthereumjsUtil.isValidChecksumAddress(address)) {
|
||||||
|
state.errors.address = 'Address is not a valid checksum';
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Address label assignation
|
||||||
|
*/
|
||||||
|
export const addressLabel = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||||
|
const state = { ...$state };
|
||||||
|
if (!state.touched.address || state.errors.address) return state;
|
||||||
|
|
||||||
|
const {
|
||||||
|
account,
|
||||||
|
network,
|
||||||
|
} = getState().selectedAccount;
|
||||||
|
if (!account || !network) return state;
|
||||||
|
const { address } = state;
|
||||||
|
|
||||||
|
const savedAccounts = getState().accounts.filter(a => a.address.toLowerCase() === address.toLowerCase());
|
||||||
|
if (savedAccounts.length > 0) {
|
||||||
|
// check if found account belongs to this network
|
||||||
|
const currentNetworkAccount = savedAccounts.find(a => a.network === network.network);
|
||||||
|
if (currentNetworkAccount) {
|
||||||
|
const device = findDevice(getState().devices, currentNetworkAccount.deviceID, currentNetworkAccount.deviceState);
|
||||||
|
if (device) {
|
||||||
|
state.infos.address = `${device.instanceLabel} Account #${(currentNetworkAccount.index + 1)}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// corner-case: the same derivation path is used on different networks
|
||||||
|
const otherNetworkAccount = savedAccounts[0];
|
||||||
|
const device = findDevice(getState().devices, otherNetworkAccount.deviceID, otherNetworkAccount.deviceState);
|
||||||
|
const { coins } = getState().localStorage.config;
|
||||||
|
const otherNetwork = coins.find(c => c.network === otherNetworkAccount.network);
|
||||||
|
if (device && otherNetwork) {
|
||||||
|
state.warnings.address = `Looks like it's ${device.instanceLabel} Account #${(otherNetworkAccount.index + 1)} address of ${otherNetwork.name} network`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Amount value validation
|
||||||
|
*/
|
||||||
|
export const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||||
|
const state = { ...$state };
|
||||||
|
if (!state.touched.amount) return state;
|
||||||
|
|
||||||
|
const {
|
||||||
|
account,
|
||||||
|
tokens,
|
||||||
|
pending,
|
||||||
|
} = getState().selectedAccount;
|
||||||
|
if (!account) return state;
|
||||||
|
|
||||||
|
const { amount } = state;
|
||||||
|
if (amount.length < 1) {
|
||||||
|
state.errors.amount = 'Amount is not set';
|
||||||
|
} else if (amount.length > 0 && !amount.match(NUMBER_RE)) {
|
||||||
|
state.errors.amount = 'Amount is not a number';
|
||||||
|
} else {
|
||||||
|
const isToken: boolean = state.currency !== state.networkSymbol;
|
||||||
|
const pendingAmount: BigNumber = getPendingAmount(pending, state.currency, isToken);
|
||||||
|
|
||||||
|
if (isToken) {
|
||||||
|
const token = findToken(tokens, account.address, state.currency, account.deviceState);
|
||||||
|
if (!token) return state;
|
||||||
|
const decimalRegExp = dynamicRegexp(parseInt(token.decimals, 0));
|
||||||
|
|
||||||
|
if (!state.amount.match(decimalRegExp)) {
|
||||||
|
state.errors.amount = `Maximum ${token.decimals} decimals allowed`;
|
||||||
|
} else if (new BigNumber(state.total).greaterThan(account.balance)) {
|
||||||
|
state.errors.amount = `Not enough ${state.networkSymbol} to cover transaction fee`;
|
||||||
|
} else if (new BigNumber(state.amount).greaterThan(new BigNumber(token.balance).minus(pendingAmount))) {
|
||||||
|
state.errors.amount = 'Not enough funds';
|
||||||
|
} else if (new BigNumber(state.amount).lessThanOrEqualTo('0')) {
|
||||||
|
state.errors.amount = 'Amount is too low';
|
||||||
|
}
|
||||||
|
} else if (!state.amount.match(ETH_18_RE)) {
|
||||||
|
state.errors.amount = 'Maximum 18 decimals allowed';
|
||||||
|
} else if (new BigNumber(state.total).greaterThan(new BigNumber(account.balance).minus(pendingAmount))) {
|
||||||
|
state.errors.amount = 'Not enough funds';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Gas limit value validation
|
||||||
|
*/
|
||||||
|
export const gasLimitValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||||
|
const state = { ...$state };
|
||||||
|
if (!state.touched.gasLimit) return state;
|
||||||
|
|
||||||
|
const {
|
||||||
|
network,
|
||||||
|
} = getState().selectedAccount;
|
||||||
|
if (!network) return state;
|
||||||
|
|
||||||
|
const { gasLimit } = state;
|
||||||
|
if (gasLimit.length < 1) {
|
||||||
|
state.errors.gasLimit = 'Gas limit is not set';
|
||||||
|
} else if (gasLimit.length > 0 && !gasLimit.match(NUMBER_RE)) {
|
||||||
|
state.errors.gasLimit = 'Gas limit is not a number';
|
||||||
|
} else {
|
||||||
|
const gl: BigNumber = new BigNumber(gasLimit);
|
||||||
|
if (gl.lessThan(1)) {
|
||||||
|
state.errors.gasLimit = 'Gas limit is too low';
|
||||||
|
} else if (gl.lessThan(state.currency !== state.networkSymbol ? network.defaultGasLimitTokens : network.defaultGasLimit)) {
|
||||||
|
state.warnings.gasLimit = 'Gas limit is below recommended';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Gas price value validation
|
||||||
|
*/
|
||||||
|
export const gasPriceValidation = ($state: State): PayloadAction<State> => (): State => {
|
||||||
|
const state = { ...$state };
|
||||||
|
if (!state.touched.gasPrice) return state;
|
||||||
|
|
||||||
|
const { gasPrice } = state;
|
||||||
|
if (gasPrice.length < 1) {
|
||||||
|
state.errors.gasPrice = 'Gas price is not set';
|
||||||
|
} else if (gasPrice.length > 0 && !gasPrice.match(NUMBER_RE)) {
|
||||||
|
state.errors.gasPrice = 'Gas price is not a number';
|
||||||
|
} else {
|
||||||
|
const gp: BigNumber = new BigNumber(gasPrice);
|
||||||
|
if (gp.greaterThan(1000)) {
|
||||||
|
state.warnings.gasPrice = 'Gas price is too high';
|
||||||
|
} else if (gp.lessThanOrEqualTo('0')) {
|
||||||
|
state.errors.gasPrice = 'Gas price is too low';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Nonce value validation
|
||||||
|
*/
|
||||||
|
export const nonceValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||||
|
const state = { ...$state };
|
||||||
|
if (!state.touched.nonce) return state;
|
||||||
|
|
||||||
|
const {
|
||||||
|
account,
|
||||||
|
} = getState().selectedAccount;
|
||||||
|
if (!account) return state;
|
||||||
|
|
||||||
|
const { nonce } = state;
|
||||||
|
if (nonce.length < 1) {
|
||||||
|
state.errors.nonce = 'Nonce is not set';
|
||||||
|
} else if (!nonce.match(ABS_RE)) {
|
||||||
|
state.errors.nonce = 'Nonce is not a valid number';
|
||||||
|
} else {
|
||||||
|
const n: BigNumber = new BigNumber(nonce);
|
||||||
|
if (n.lessThan(account.nonce)) {
|
||||||
|
state.warnings.nonce = 'Nonce is lower than recommended';
|
||||||
|
} else if (n.greaterThan(account.nonce)) {
|
||||||
|
state.warnings.nonce = 'Nonce is greater than recommended';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Gas price value validation
|
||||||
|
*/
|
||||||
|
export const dataValidation = ($state: State): PayloadAction<State> => (): State => {
|
||||||
|
const state = { ...$state };
|
||||||
|
if (!state.touched.data || state.data.length === 0) return state;
|
||||||
|
if (!HEX_RE.test(state.data)) {
|
||||||
|
state.errors.data = 'Data is not valid hexadecimal';
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* UTILITIES
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const calculateFee = (gasPrice: string, gasLimit: string): string => {
|
||||||
|
try {
|
||||||
|
return EthereumjsUnits.convert(new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether');
|
||||||
|
} catch (error) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calculateTotal = (amount: string, gasPrice: string, gasLimit: string): string => {
|
||||||
|
try {
|
||||||
|
return new BigNumber(amount).plus(calculateFee(gasPrice, gasLimit)).toString(10);
|
||||||
|
} catch (error) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calculateMaxAmount = (balance: BigNumber, gasPrice: string, gasLimit: string): string => {
|
||||||
|
try {
|
||||||
|
// TODO - minus pendings
|
||||||
|
const fee = calculateFee(gasPrice, gasLimit);
|
||||||
|
const max = balance.minus(fee);
|
||||||
|
if (max.lessThan(0)) return '0';
|
||||||
|
return max.toString(10);
|
||||||
|
} catch (error) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFeeLevels = (symbol: string, gasPrice: BigNumber | string, gasLimit: string, selected?: FeeLevel): Array<FeeLevel> => {
|
||||||
|
const price: BigNumber = typeof gasPrice === 'string' ? new BigNumber(gasPrice) : gasPrice;
|
||||||
|
const quarter: BigNumber = price.dividedBy(4);
|
||||||
|
const high: string = price.plus(quarter.times(2)).toString(10);
|
||||||
|
const low: string = price.minus(quarter.times(2)).toString(10);
|
||||||
|
|
||||||
|
const customLevel: FeeLevel = selected && selected.value === 'Custom' ? {
|
||||||
|
value: 'Custom',
|
||||||
|
gasPrice: selected.gasPrice,
|
||||||
|
// label: `${ calculateFee(gasPrice, gasLimit) } ${ symbol }`
|
||||||
|
label: `${calculateFee(selected.gasPrice, gasLimit)} ${symbol}`,
|
||||||
|
} : {
|
||||||
|
value: 'Custom',
|
||||||
|
gasPrice: low,
|
||||||
|
label: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: 'High',
|
||||||
|
gasPrice: high,
|
||||||
|
label: `${calculateFee(high, gasLimit)} ${symbol}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Normal',
|
||||||
|
gasPrice: gasPrice.toString(),
|
||||||
|
label: `${calculateFee(price.toString(10), gasLimit)} ${symbol}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Low',
|
||||||
|
gasPrice: low,
|
||||||
|
label: `${calculateFee(low, gasLimit)} ${symbol}`,
|
||||||
|
},
|
||||||
|
customLevel,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSelectedFeeLevel = (feeLevels: Array<FeeLevel>, selected: FeeLevel): FeeLevel => {
|
||||||
|
const { value } = selected;
|
||||||
|
let selectedFeeLevel: ?FeeLevel;
|
||||||
|
selectedFeeLevel = feeLevels.find(f => f.value === value);
|
||||||
|
if (!selectedFeeLevel) {
|
||||||
|
// fallback to default
|
||||||
|
selectedFeeLevel = feeLevels.find(f => f.value === 'Normal');
|
||||||
|
}
|
||||||
|
return selectedFeeLevel || selected;
|
||||||
|
};
|
@ -4,47 +4,56 @@
|
|||||||
import type { State as SendFormState } from 'reducers/SendFormReducer';
|
import type { State as SendFormState } from 'reducers/SendFormReducer';
|
||||||
import type {
|
import type {
|
||||||
ThunkAction,
|
ThunkAction,
|
||||||
|
PayloadAction,
|
||||||
GetState,
|
GetState,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
} from 'flowtype';
|
} from 'flowtype';
|
||||||
|
|
||||||
const PREFIX: string = 'trezor:draft-tx:';
|
const TX_PREFIX: string = 'trezor:draft-tx:';
|
||||||
|
|
||||||
export const save = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
export const saveDraftTransaction = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||||
if (typeof window.localStorage === 'undefined') return;
|
if (typeof window.localStorage === 'undefined') return;
|
||||||
|
|
||||||
const location = getState().router.location.pathname;
|
|
||||||
const state = getState().sendForm;
|
const state = getState().sendForm;
|
||||||
if (!state.untouched) {
|
if (state.untouched) return;
|
||||||
|
|
||||||
|
const location = getState().router.location.pathname;
|
||||||
try {
|
try {
|
||||||
window.sessionStorage.setItem(`${PREFIX}${location}`, JSON.stringify(state));
|
// save state as it is
|
||||||
|
// "loadDraftTransaction" will do the validation
|
||||||
|
window.sessionStorage.setItem(`${TX_PREFIX}${location}`, JSON.stringify(state));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Saving sessionStorage error: ${error}`);
|
console.error(`Saving sessionStorage error: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const load = (location: string): ?SendFormState => {
|
export const loadDraftTransaction = (): PayloadAction<?SendFormState> => (dispatch: Dispatch, getState: GetState): ?SendFormState => {
|
||||||
if (typeof window.localStorage === 'undefined') return;
|
if (typeof window.localStorage === 'undefined') return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const value: string = window.sessionStorage.getItem(`${PREFIX}${location}`);
|
const location = getState().router.location.pathname;
|
||||||
|
const value: string = window.sessionStorage.getItem(`${TX_PREFIX}${location}`);
|
||||||
const state: ?SendFormState = JSON.parse(value);
|
const state: ?SendFormState = JSON.parse(value);
|
||||||
if (state && state.address === '' && (state.amount === '' || state.amount === '0')) {
|
if (state) {
|
||||||
window.sessionStorage.removeItem(`${PREFIX}${location}`);
|
// decide if draft is valid and should be returned
|
||||||
return;
|
// ignore this draft if has any error
|
||||||
|
if (Object.keys(state.errors).length > 0) {
|
||||||
|
window.sessionStorage.removeItem(`${TX_PREFIX}${location}`);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Loading sessionStorage error: ${error}`);
|
console.error(`Loading sessionStorage error: ${error}`);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clear = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
export const clear = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||||
if (typeof window.localStorage === 'undefined') return;
|
if (typeof window.localStorage === 'undefined') return;
|
||||||
const location = getState().router.location.pathname;
|
const location = getState().router.location.pathname;
|
||||||
try {
|
try {
|
||||||
window.sessionStorage.removeItem(`${PREFIX}${location}`);
|
window.sessionStorage.removeItem(`${TX_PREFIX}${location}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Clearing sessionStorage error: ${error}`);
|
console.error(`Clearing sessionStorage error: ${error}`);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
import Web3 from 'web3';
|
import Web3 from 'web3';
|
||||||
import HDKey from 'hdkey';
|
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
import EthereumjsUtil from 'ethereumjs-util';
|
|
||||||
import EthereumjsUnits from 'ethereumjs-units';
|
import EthereumjsUnits from 'ethereumjs-units';
|
||||||
import EthereumjsTx from 'ethereumjs-tx';
|
|
||||||
// import InputDataDecoder from 'ethereum-input-data-decoder';
|
// import InputDataDecoder from 'ethereum-input-data-decoder';
|
||||||
import TrezorConnect from 'trezor-connect';
|
import type { EstimateGasOptions } from 'web3';
|
||||||
import type { EstimateGasOptions, TransactionStatus, TransactionReceipt } from 'web3';
|
|
||||||
import { strip } from 'utils/ethUtils';
|
|
||||||
import * as WEB3 from 'actions/constants/web3';
|
import * as WEB3 from 'actions/constants/web3';
|
||||||
import * as PENDING from 'actions/constants/pendingTx';
|
import * as PENDING from 'actions/constants/pendingTx';
|
||||||
|
|
||||||
@ -17,13 +12,11 @@ import type {
|
|||||||
Dispatch,
|
Dispatch,
|
||||||
GetState,
|
GetState,
|
||||||
ThunkAction,
|
ThunkAction,
|
||||||
AsyncAction,
|
|
||||||
PromiseAction,
|
PromiseAction,
|
||||||
} from 'flowtype';
|
} from 'flowtype';
|
||||||
|
|
||||||
import type { EthereumAccount } from 'trezor-connect';
|
import type { EthereumAccount } from 'trezor-connect';
|
||||||
import type { Account } from 'reducers/AccountsReducer';
|
import type { Account } from 'reducers/AccountsReducer';
|
||||||
import type { PendingTx } from 'reducers/PendingTxReducer';
|
|
||||||
import type { Web3Instance } from 'reducers/Web3Reducer';
|
import type { Web3Instance } from 'reducers/Web3Reducer';
|
||||||
import type { Token } from 'reducers/TokensReducer';
|
import type { Token } from 'reducers/TokensReducer';
|
||||||
import type { NetworkToken } from 'reducers/LocalStorageReducer';
|
import type { NetworkToken } from 'reducers/LocalStorageReducer';
|
||||||
@ -52,8 +45,7 @@ export type Web3Action = {
|
|||||||
} | Web3UpdateBlockAction
|
} | Web3UpdateBlockAction
|
||||||
| Web3UpdateGasPriceAction;
|
| Web3UpdateGasPriceAction;
|
||||||
|
|
||||||
export const initWeb3 = (network: string, urlIndex: number = 0): PromiseAction<Web3Instance> => async (dispatch: Dispatch, getState: GetState): Promise<Web3Instance> => {
|
export const initWeb3 = (network: string, urlIndex: number = 0): PromiseAction<Web3Instance> => async (dispatch: Dispatch, getState: GetState): Promise<Web3Instance> => new Promise(async (resolve, reject) => {
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
// check if requested web was initialized before
|
// check if requested web was initialized before
|
||||||
const instance = getState().web3.find(w3 => w3.network === network);
|
const instance = getState().web3.find(w3 => w3.network === network);
|
||||||
if (instance && instance.web3.currentProvider.connected) {
|
if (instance && instance.web3.currentProvider.connected) {
|
||||||
@ -67,47 +59,45 @@ export const initWeb3 = (network: string, urlIndex: number = 0): PromiseAction<W
|
|||||||
const coin = config.coins.find(c => c.network === network);
|
const coin = config.coins.find(c => c.network === network);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
// coin not found
|
// coin not found
|
||||||
reject(new Error(`Network ${ network} not found in application config.`));
|
reject(new Error(`Network ${network} not found in application config.`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get first url
|
// get first url
|
||||||
const url = coin.web3[ urlIndex ];
|
const url = coin.web3[urlIndex];
|
||||||
if (!url) {
|
if (!url) {
|
||||||
reject(new Error('Web3 backend is not responding'));
|
reject(new Error('Web3 backend is not responding'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const web3 = new Web3( new Web3.providers.WebsocketProvider(url) );
|
const web3 = new Web3(new Web3.providers.WebsocketProvider(url));
|
||||||
|
|
||||||
const onConnect = async () => {
|
const onConnect = async () => {
|
||||||
|
|
||||||
const latestBlock = await web3.eth.getBlockNumber();
|
const latestBlock = await web3.eth.getBlockNumber();
|
||||||
const gasPrice = await web3.eth.getGasPrice();
|
const gasPrice = await web3.eth.getGasPrice();
|
||||||
|
|
||||||
const instance = {
|
const newInstance = {
|
||||||
network,
|
network,
|
||||||
web3,
|
web3,
|
||||||
chainId: coin.chainId,
|
chainId: coin.chainId,
|
||||||
erc20: new web3.eth.Contract(ERC20Abi),
|
erc20: new web3.eth.Contract(ERC20Abi),
|
||||||
latestBlock,
|
latestBlock,
|
||||||
gasPrice,
|
gasPrice,
|
||||||
}
|
};
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: WEB3.CREATE,
|
type: WEB3.CREATE,
|
||||||
instance,
|
instance: newInstance,
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve(instance);
|
resolve(newInstance);
|
||||||
}
|
};
|
||||||
|
|
||||||
const onEnd = async () => {
|
const onEnd = async () => {
|
||||||
|
|
||||||
web3.currentProvider.reset();
|
web3.currentProvider.reset();
|
||||||
const instance = getState().web3.find(w3 => w3.network === network);
|
const oldInstance = getState().web3.find(w3 => w3.network === network);
|
||||||
|
|
||||||
if (instance && instance.web3.currentProvider.connected) {
|
if (oldInstance && oldInstance.web3.currentProvider.connected) {
|
||||||
// backend disconnects
|
// backend disconnects
|
||||||
// dispatch({
|
// dispatch({
|
||||||
// type: 'WEB3.DISCONNECT',
|
// type: 'WEB3.DISCONNECT',
|
||||||
@ -116,22 +106,21 @@ export const initWeb3 = (network: string, urlIndex: number = 0): PromiseAction<W
|
|||||||
} else {
|
} else {
|
||||||
// backend initialization error for given url, try next one
|
// backend initialization error for given url, try next one
|
||||||
try {
|
try {
|
||||||
const web3 = await dispatch( initWeb3(network, urlIndex + 1) );
|
const otherWeb3 = await dispatch(initWeb3(network, urlIndex + 1));
|
||||||
resolve(web3);
|
resolve(otherWeb3);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
web3.currentProvider.on('connect', onConnect);
|
web3.currentProvider.on('connect', onConnect);
|
||||||
web3.currentProvider.on('end', onEnd);
|
web3.currentProvider.on('end', onEnd);
|
||||||
web3.currentProvider.on('error', onEnd);
|
web3.currentProvider.on('error', onEnd);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export const discoverAccount = (address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch, getState: GetState): Promise<EthereumAccount> => {
|
export const discoverAccount = (address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
|
||||||
const instance: Web3Instance = await dispatch( initWeb3(network) );
|
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||||
const balance = await instance.web3.eth.getBalance(address);
|
const balance = await instance.web3.eth.getBalance(address);
|
||||||
const nonce = await instance.web3.eth.getTransactionCount(address);
|
const nonce = await instance.web3.eth.getTransactionCount(address);
|
||||||
return {
|
return {
|
||||||
@ -139,14 +128,14 @@ export const discoverAccount = (address: string, network: string): PromiseAction
|
|||||||
transactions: 0,
|
transactions: 0,
|
||||||
block: 0,
|
block: 0,
|
||||||
balance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
|
balance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
|
||||||
nonce
|
nonce,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const resolvePendingTransactions = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
export const resolvePendingTransactions = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
const instance: Web3Instance = await dispatch( initWeb3(network) );
|
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||||
const pending = getState().pending.filter(p => p.network === network);
|
const pending = getState().pending.filter(p => p.network === network);
|
||||||
for (const tx of pending) {
|
pending.forEach(async (tx) => {
|
||||||
const status = await instance.web3.eth.getTransaction(tx.id);
|
const status = await instance.web3.eth.getTransaction(tx.id);
|
||||||
if (!status) {
|
if (!status) {
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -169,14 +158,15 @@ export const resolvePendingTransactions = (network: string): PromiseAction<void>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getPendingInfo = (network: string, txid: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
/*
|
||||||
const instance: Web3Instance = await dispatch( initWeb3(network) );
|
export const getPendingInfo = (network: string, txid: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||||
|
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||||
const tx = await instance.web3.eth.getTransaction(txid);
|
const tx = await instance.web3.eth.getTransaction(txid);
|
||||||
|
|
||||||
/*
|
|
||||||
if (tx.input !== "0x") {
|
if (tx.input !== "0x") {
|
||||||
// find token:
|
// find token:
|
||||||
// tx.to <= smart contract address
|
// tx.to <= smart contract address
|
||||||
@ -190,31 +180,33 @@ export const getPendingInfo = (network: string, txid: string): PromiseAction<voi
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
// return tx;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTxInput = (): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
// return tx;
|
||||||
const instance: Web3Instance = await dispatch( initWeb3("ropsten") );
|
};
|
||||||
|
|
||||||
|
export const getTxInput = (): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||||
|
const instance: Web3Instance = await dispatch(initWeb3('ropsten'));
|
||||||
// const inputData = instance.web3.utils.hexToAscii("0xa9059cbb00000000000000000000000073d0385f4d8e00c5e6504c6030f47bf6212736a80000000000000000000000000000000000000000000000000000000000000001");
|
// const inputData = instance.web3.utils.hexToAscii("0xa9059cbb00000000000000000000000073d0385f4d8e00c5e6504c6030f47bf6212736a80000000000000000000000000000000000000000000000000000000000000001");
|
||||||
// console.warn("input data!", inputData);
|
// console.warn("input data!", inputData);
|
||||||
}
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const updateAccount = (account: Account, newAccount: EthereumAccount, network: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||||
export const updateAccount = (account: Account, newAccount: EthereumAccount, network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||||
const instance: Web3Instance = await dispatch( initWeb3(network) );
|
|
||||||
const balance = await instance.web3.eth.getBalance(account.address);
|
const balance = await instance.web3.eth.getBalance(account.address);
|
||||||
const nonce = await instance.web3.eth.getTransactionCount(account.address);
|
const nonce = await instance.web3.eth.getTransactionCount(account.address);
|
||||||
dispatch( AccountsActions.update( { ...account, ...newAccount, balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), nonce }) );
|
dispatch(AccountsActions.update({
|
||||||
|
...account, ...newAccount, balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), nonce,
|
||||||
|
}));
|
||||||
|
|
||||||
// update tokens for this account
|
// update tokens for this account
|
||||||
dispatch( updateAccountTokens(account) );
|
dispatch(updateAccountTokens(account));
|
||||||
}
|
};
|
||||||
|
|
||||||
export const updateAccountTokens = (account: Account): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
export const updateAccountTokens = (account: Account): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
const tokens = getState().tokens.filter(t => t.network === account.network && t.ethAddress === account.address);
|
const tokens = getState().tokens.filter(t => t.network === account.network && t.ethAddress === account.address);
|
||||||
for (const token of tokens) {
|
tokens.forEach(async (token) => {
|
||||||
const balance = await dispatch( getTokenBalance(token) );
|
const balance = await dispatch(getTokenBalance(token));
|
||||||
// const newBalance: string = balance.dividedBy(Math.pow(10, token.decimals)).toString(10);
|
// const newBalance: string = balance.dividedBy(Math.pow(10, token.decimals)).toString(10);
|
||||||
if (balance !== token.balance) {
|
if (balance !== token.balance) {
|
||||||
dispatch(TokenActions.setBalance(
|
dispatch(TokenActions.setBalance(
|
||||||
@ -223,11 +215,11 @@ export const updateAccountTokens = (account: Account): PromiseAction<void> => as
|
|||||||
balance,
|
balance,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getTokenInfo = (address: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch, getState: GetState): Promise<NetworkToken> => {
|
export const getTokenInfo = (address: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch): Promise<NetworkToken> => {
|
||||||
const instance: Web3Instance = await dispatch( initWeb3(network) );
|
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||||
const contract = instance.erc20.clone();
|
const contract = instance.erc20.clone();
|
||||||
contract.options.address = address;
|
contract.options.address = address;
|
||||||
|
|
||||||
@ -243,52 +235,55 @@ export const getTokenInfo = (address: string, network: string): PromiseAction<Ne
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
|
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => {
|
||||||
const instance = await dispatch( initWeb3(token.network) );
|
const instance = await dispatch(initWeb3(token.network));
|
||||||
const contract = instance.erc20.clone();
|
const contract = instance.erc20.clone();
|
||||||
contract.options.address = token.address;
|
contract.options.address = token.address;
|
||||||
|
|
||||||
const balance = await contract.methods.balanceOf(token.ethAddress).call();
|
const balance = await contract.methods.balanceOf(token.ethAddress).call();
|
||||||
return new BigNumber(balance).dividedBy(Math.pow(10, token.decimals)).toString(10);
|
return new BigNumber(balance).dividedBy(10 ** token.decimals).toString(10);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCurrentGasPrice = (network: string): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
|
export const getCurrentGasPrice = (network: string): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
|
||||||
const instance = getState().web3.find(w3 => w3.network === network);
|
const instance = getState().web3.find(w3 => w3.network === network);
|
||||||
if (instance) {
|
if (instance) {
|
||||||
return EthereumjsUnits.convert(instance.gasPrice, 'wei', 'gwei');
|
return EthereumjsUnits.convert(instance.gasPrice, 'wei', 'gwei');
|
||||||
} else {
|
|
||||||
throw "0";
|
|
||||||
}
|
}
|
||||||
}
|
return '0';
|
||||||
|
};
|
||||||
|
|
||||||
export const updateGasPrice = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
export const updateGasPrice = (network: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const instance = await dispatch( initWeb3(network) );
|
const instance = await dispatch(initWeb3(network));
|
||||||
const gasPrice = await instance.web3.eth.getGasPrice();
|
const gasPrice = await instance.web3.eth.getGasPrice();
|
||||||
if (instance.gasPrice !== gasPrice) {
|
if (instance.gasPrice !== gasPrice) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: WEB3.GAS_PRICE_UPDATED,
|
type: WEB3.GAS_PRICE_UPDATED,
|
||||||
network,
|
network,
|
||||||
gasPrice
|
gasPrice,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// silent action
|
// silent action
|
||||||
// nothing happens if this fails
|
// nothing happens if this fails
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export const estimateGasLimit = (network: string, options: EstimateGasOptions): PromiseAction<number> => async (dispatch: Dispatch, getState: GetState): Promise<number> => {
|
export const estimateGasLimit = (network: string, $options: EstimateGasOptions): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => {
|
||||||
const instance = await dispatch( initWeb3(network) );
|
const instance = await dispatch(initWeb3(network));
|
||||||
// TODO: allow data starting with 0x ...
|
// TODO: allow data starting with 0x ...
|
||||||
options.to = '0x0000000000000000000000000000000000000000';
|
const data = `0x${$options.data.length % 2 === 0 ? $options.data : `0${$options.data}`}`;
|
||||||
options.data = `0x${options.data.length % 2 === 0 ? options.data : `0${options.data}`}`;
|
const options = {
|
||||||
options.value = instance.web3.utils.toHex( EthereumjsUnits.convert(options.value || '0', 'ether', 'wei') );
|
...$options,
|
||||||
options.gasPrice = instance.web3.utils.toHex( EthereumjsUnits.convert(options.gasPrice, 'gwei', 'wei') );
|
to: '0x0000000000000000000000000000000000000000',
|
||||||
|
data,
|
||||||
|
value: instance.web3.utils.toHex(EthereumjsUnits.convert($options.value || '0', 'ether', 'wei')),
|
||||||
|
gasPrice: instance.web3.utils.toHex(EthereumjsUnits.convert($options.gasPrice, 'gwei', 'wei')),
|
||||||
|
};
|
||||||
|
|
||||||
const limit = await instance.web3.eth.estimateGas(options);
|
const limit = await instance.web3.eth.estimateGas(options);
|
||||||
return limit;
|
return limit.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const disconnect = (coinInfo: any): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
export const disconnect = (coinInfo: any): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||||
@ -304,7 +299,7 @@ export const disconnect = (coinInfo: any): ThunkAction => (dispatch: Dispatch, g
|
|||||||
// remove instance from reducer
|
// remove instance from reducer
|
||||||
dispatch({
|
dispatch({
|
||||||
type: WEB3.DISCONNECT,
|
type: WEB3.DISCONNECT,
|
||||||
instance
|
instance,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,23 +1,9 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
|
|
||||||
export const INIT: 'send__init' = 'send__init';
|
export const INIT: 'send__init' = 'send__init';
|
||||||
export const DISPOSE: 'send__dispose' = 'send__dispose';
|
export const CHANGE: 'send__change' = 'send__change';
|
||||||
export const VALIDATION: 'send__validation' = 'send__validation';
|
export const VALIDATION: 'send__validation' = 'send__validation';
|
||||||
export const ADDRESS_VALIDATION: 'send__address_validation' = 'send__address_validation';
|
export const TX_SENDING: 'send__tx_sending' = 'send__tx_sending';
|
||||||
export const ADDRESS_CHANGE: 'send__address_change' = 'send__address_change';
|
|
||||||
export const AMOUNT_CHANGE: 'send__amount_change' = 'send__amount_change';
|
|
||||||
export const SET_MAX: 'send__set_max' = 'send__set_max';
|
|
||||||
export const CURRENCY_CHANGE: 'send__currency_change' = 'send__currency_change';
|
|
||||||
export const FEE_LEVEL_CHANGE: 'send__fee_level_change' = 'send__fee_level_change';
|
|
||||||
export const GAS_PRICE_CHANGE: 'send__gas_price_change' = 'send__gas_price_change';
|
|
||||||
export const GAS_LIMIT_CHANGE: 'send__gas_limit_change' = 'send__gas_limit_change';
|
|
||||||
export const NONCE_CHANGE: 'send__nonce_change' = 'send__nonce_change';
|
|
||||||
export const UPDATE_FEE_LEVELS: 'send__update_fee_levels' = 'send__update_fee_levels';
|
|
||||||
export const DATA_CHANGE: 'send__data_change' = 'send__data_change';
|
|
||||||
export const SEND: 'send__submit' = 'send__submit';
|
|
||||||
export const TX_COMPLETE: 'send__tx_complete' = 'send__tx_complete';
|
export const TX_COMPLETE: 'send__tx_complete' = 'send__tx_complete';
|
||||||
export const TX_ERROR: 'send__tx_error' = 'send__tx_error';
|
export const TX_ERROR: 'send__tx_error' = 'send__tx_error';
|
||||||
export const TOGGLE_ADVANCED: 'send__toggle_advanced' = 'send__toggle_advanced';
|
export const TOGGLE_ADVANCED: 'send__toggle_advanced' = 'send__toggle_advanced';
|
||||||
|
|
||||||
export const FROM_SESSION_STORAGE: 'send__from_session_storage' = 'send__from_session_storage';
|
|
@ -1,17 +1,9 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
|
|
||||||
import EthereumjsUnits from 'ethereumjs-units';
|
|
||||||
import * as SEND from 'actions/constants/send';
|
import * as SEND from 'actions/constants/send';
|
||||||
import * as WEB3 from 'actions/constants/web3';
|
|
||||||
import * as ACCOUNT from 'actions/constants/account';
|
import * as ACCOUNT from 'actions/constants/account';
|
||||||
|
|
||||||
import { getFeeLevels } from 'actions/SendFormActions';
|
|
||||||
|
|
||||||
import type { Action } from 'flowtype';
|
import type { Action } from 'flowtype';
|
||||||
import type {
|
|
||||||
Web3UpdateGasPriceAction,
|
|
||||||
} from 'actions/Web3Actions';
|
|
||||||
|
|
||||||
export type FeeLevel = {
|
export type FeeLevel = {
|
||||||
label: string;
|
label: string;
|
||||||
@ -82,46 +74,16 @@ export const initialState: State = {
|
|||||||
infos: {},
|
infos: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const onGasPriceUpdated = (state: State, action: Web3UpdateGasPriceAction): State => {
|
|
||||||
// function getRandomInt(min, max) {
|
|
||||||
// return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
||||||
// }
|
|
||||||
// const newPrice = getRandomInt(10, 50).toString();
|
|
||||||
const newPrice: string = EthereumjsUnits.convert(action.gasPrice, 'wei', 'gwei');
|
|
||||||
if (action.network === state.networkName && newPrice !== state.recommendedGasPrice) {
|
|
||||||
const newState: State = { ...state };
|
|
||||||
if (!state.untouched) {
|
|
||||||
newState.gasPriceNeedsUpdate = true;
|
|
||||||
newState.recommendedGasPrice = newPrice;
|
|
||||||
} else {
|
|
||||||
const newFeeLevels = getFeeLevels(state.networkSymbol, newPrice, state.gasLimit);
|
|
||||||
const selectedFeeLevel: ?FeeLevel = newFeeLevels.find(f => f.value === 'Normal');
|
|
||||||
if (!selectedFeeLevel) return state;
|
|
||||||
newState.recommendedGasPrice = newPrice;
|
|
||||||
newState.feeLevels = newFeeLevels;
|
|
||||||
newState.selectedFeeLevel = selectedFeeLevel;
|
|
||||||
newState.gasPrice = selectedFeeLevel.gasPrice;
|
|
||||||
}
|
|
||||||
return newState;
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default (state: State = initialState, action: Action): State => {
|
export default (state: State = initialState, action: Action): State => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SEND.INIT:
|
case SEND.INIT:
|
||||||
|
case SEND.CHANGE:
|
||||||
|
case SEND.VALIDATION:
|
||||||
return action.state;
|
return action.state;
|
||||||
|
|
||||||
case ACCOUNT.DISPOSE:
|
case ACCOUNT.DISPOSE:
|
||||||
return initialState;
|
return initialState;
|
||||||
|
|
||||||
// this will be called right after Web3 instance initialization before any view is shown
|
|
||||||
// and async during app live time
|
|
||||||
case WEB3.GAS_PRICE_UPDATED:
|
|
||||||
return onGasPriceUpdated(state, action);
|
|
||||||
|
|
||||||
|
|
||||||
case SEND.TOGGLE_ADVANCED:
|
case SEND.TOGGLE_ADVANCED:
|
||||||
return {
|
return {
|
||||||
@ -129,22 +91,7 @@ export default (state: State = initialState, action: Action): State => {
|
|||||||
advanced: !state.advanced,
|
advanced: !state.advanced,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case SEND.TX_SENDING:
|
||||||
// user actions
|
|
||||||
case SEND.ADDRESS_CHANGE:
|
|
||||||
case SEND.ADDRESS_VALIDATION:
|
|
||||||
case SEND.AMOUNT_CHANGE:
|
|
||||||
case SEND.SET_MAX:
|
|
||||||
case SEND.CURRENCY_CHANGE:
|
|
||||||
case SEND.FEE_LEVEL_CHANGE:
|
|
||||||
case SEND.UPDATE_FEE_LEVELS:
|
|
||||||
case SEND.GAS_PRICE_CHANGE:
|
|
||||||
case SEND.GAS_LIMIT_CHANGE:
|
|
||||||
case SEND.NONCE_CHANGE:
|
|
||||||
case SEND.DATA_CHANGE:
|
|
||||||
return action.state;
|
|
||||||
|
|
||||||
case SEND.SEND:
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
sending: true,
|
sending: true,
|
||||||
@ -156,32 +103,6 @@ export default (state: State = initialState, action: Action): State => {
|
|||||||
sending: false,
|
sending: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
case SEND.VALIDATION:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
errors: action.errors,
|
|
||||||
warnings: action.warnings,
|
|
||||||
infos: action.infos,
|
|
||||||
};
|
|
||||||
|
|
||||||
case SEND.FROM_SESSION_STORAGE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
|
|
||||||
address: action.address,
|
|
||||||
amount: action.amount,
|
|
||||||
setMax: action.setMax,
|
|
||||||
selectedCurrency: action.selectedCurrency,
|
|
||||||
selectedFeeLevel: action.selectedFeeLevel,
|
|
||||||
advanced: action.advanced,
|
|
||||||
gasLimit: action.gasLimit,
|
|
||||||
gasPrice: action.gasPrice,
|
|
||||||
data: action.data,
|
|
||||||
nonce: action.nonce,
|
|
||||||
untouched: false,
|
|
||||||
touched: action.touched,
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -115,3 +115,22 @@ export const getWeb3 = (state: State): ?Web3Instance => {
|
|||||||
if (!locationState.network) return null;
|
if (!locationState.network) return null;
|
||||||
return state.web3.find(w3 => w3.network === locationState.network);
|
return state.web3.find(w3 => w3.network === locationState.network);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const observeChanges = (prev: ?(Object | Array<any>), current: ?(Object | Array<any>), fields?: Array<string>): boolean => {
|
||||||
|
if (prev !== current) {
|
||||||
|
// 1. one of the objects is null/undefined
|
||||||
|
if (!prev || !current) return true;
|
||||||
|
// 2. object are Arrays and they have different length
|
||||||
|
if (Array.isArray(prev) && Array.isArray(current)) return prev.length !== current.length;
|
||||||
|
// 3. no nested field to check
|
||||||
|
if (!Array.isArray(fields)) return true;
|
||||||
|
// 4. validate nested field
|
||||||
|
if (prev instanceof Object && current instanceof Object) {
|
||||||
|
for (let i = 0; i < fields.length; i++) {
|
||||||
|
const key = fields[i];
|
||||||
|
if (prev[key] !== current[key]) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
@ -10,6 +10,7 @@ import * as NotificationActions from 'actions/NotificationActions';
|
|||||||
import * as LocalStorageActions from 'actions/LocalStorageActions';
|
import * as LocalStorageActions from 'actions/LocalStorageActions';
|
||||||
import * as TrezorConnectActions from 'actions/TrezorConnectActions';
|
import * as TrezorConnectActions from 'actions/TrezorConnectActions';
|
||||||
import * as SelectedAccountActions from 'actions/SelectedAccountActions';
|
import * as SelectedAccountActions from 'actions/SelectedAccountActions';
|
||||||
|
import * as SendFormActionActions from 'actions/SendFormActions';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Middleware,
|
Middleware,
|
||||||
@ -91,6 +92,8 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
|
|||||||
api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state));
|
api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// observe send form props changes
|
||||||
|
api.dispatch(SendFormActionActions.observe(prevState, action));
|
||||||
|
|
||||||
// update common values in WallerReducer
|
// update common values in WallerReducer
|
||||||
api.dispatch(WalletActions.updateSelectedValues(prevState, action));
|
api.dispatch(WalletActions.updateSelectedValues(prevState, action));
|
||||||
|
@ -0,0 +1,219 @@
|
|||||||
|
/* @flow */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import colors from 'config/colors';
|
||||||
|
|
||||||
|
import Input from 'components/inputs/Input';
|
||||||
|
import Textarea from 'components/Textarea';
|
||||||
|
import Tooltip from 'components/Tooltip';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
import Link from 'components/Link';
|
||||||
|
import ICONS from 'config/icons';
|
||||||
|
|
||||||
|
import type { Props } from '../../Container';
|
||||||
|
|
||||||
|
const InputRow = styled.div`
|
||||||
|
margin-bottom: 20px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InputLabelWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GreenSpan = styled.span`
|
||||||
|
color: ${colors.GREEN_PRIMARY};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AdvancedSettingsWrapper = styled.div`
|
||||||
|
padding: 20px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
border-top: 1px solid ${colors.DIVIDER};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GasInputRow = styled(InputRow)`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GasInput = styled(Input)`
|
||||||
|
&:first-child {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTextarea = styled(Textarea)`
|
||||||
|
margin-bottom: 20px;
|
||||||
|
height: 80px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AdvancedSettingsSendButtonWrapper = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const getGasLimitInputState = (gasLimitErrors: string, gasLimitWarnings: string): string => {
|
||||||
|
let state = '';
|
||||||
|
if (gasLimitWarnings && !gasLimitErrors) {
|
||||||
|
state = 'warning';
|
||||||
|
}
|
||||||
|
if (gasLimitErrors) {
|
||||||
|
state = 'error';
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGasPriceInputState = (gasPriceErrors: string, gasPriceWarnings: string): string => {
|
||||||
|
let state = '';
|
||||||
|
if (gasPriceWarnings && !gasPriceErrors) {
|
||||||
|
state = 'warning';
|
||||||
|
}
|
||||||
|
if (gasPriceErrors) {
|
||||||
|
state = 'error';
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
// stateless component
|
||||||
|
const AdvancedForm = (props: Props) => {
|
||||||
|
const {
|
||||||
|
network,
|
||||||
|
} = props.selectedAccount;
|
||||||
|
if (!network) return null;
|
||||||
|
const {
|
||||||
|
networkSymbol,
|
||||||
|
currency,
|
||||||
|
recommendedGasPrice,
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
infos,
|
||||||
|
data,
|
||||||
|
gasLimit,
|
||||||
|
gasPrice,
|
||||||
|
} = props.sendForm;
|
||||||
|
const {
|
||||||
|
onGasLimitChange,
|
||||||
|
onGasPriceChange,
|
||||||
|
onDataChange,
|
||||||
|
} = props.sendFormActions;
|
||||||
|
|
||||||
|
let gasLimitTooltipCurrency: string;
|
||||||
|
let gasLimitTooltipValue: string;
|
||||||
|
if (networkSymbol !== currency) {
|
||||||
|
gasLimitTooltipCurrency = 'tokens';
|
||||||
|
gasLimitTooltipValue = network.defaultGasLimitTokens.toString(10);
|
||||||
|
} else {
|
||||||
|
gasLimitTooltipCurrency = networkSymbol;
|
||||||
|
gasLimitTooltipValue = network.defaultGasLimit.toString(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdvancedSettingsWrapper>
|
||||||
|
<GasInputRow>
|
||||||
|
<GasInput
|
||||||
|
state={getGasLimitInputState(errors.gasLimit, warnings.gasLimit)}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
topLabel={(
|
||||||
|
<InputLabelWrapper>
|
||||||
|
Gas limit
|
||||||
|
<Tooltip
|
||||||
|
content={(
|
||||||
|
<React.Fragment>
|
||||||
|
Gas limit is the amount of gas to send with your transaction.<br />
|
||||||
|
<GreenSpan>TX fee = gas price * gas limit</GreenSpan> & is paid to miners for including your TX in a block.<br />
|
||||||
|
Increasing this number will not get your TX mined faster.<br />
|
||||||
|
Default value for sending {gasLimitTooltipCurrency} is <GreenSpan>{gasLimitTooltipValue}</GreenSpan>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={ICONS.HELP}
|
||||||
|
color={colors.TEXT_SECONDARY}
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</InputLabelWrapper>
|
||||||
|
)}
|
||||||
|
bottomText={errors.gasLimit || warnings.gasLimit || infos.gasLimit}
|
||||||
|
value={gasLimit}
|
||||||
|
isDisabled={networkSymbol === currency && data.length > 0}
|
||||||
|
onChange={event => onGasLimitChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GasInput
|
||||||
|
state={getGasPriceInputState(errors.gasPrice, warnings.gasPrice)}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
topLabel={(
|
||||||
|
<InputLabelWrapper>
|
||||||
|
Gas price
|
||||||
|
<Tooltip
|
||||||
|
content={(
|
||||||
|
<React.Fragment>
|
||||||
|
Gas Price is the amount you pay per unit of gas.<br />
|
||||||
|
<GreenSpan>TX fee = gas price * gas limit</GreenSpan> & is paid to miners for including your TX in a block.<br />
|
||||||
|
Higher the gas price = faster transaction, but more expensive. Recommended is <GreenSpan>{recommendedGasPrice} GWEI.</GreenSpan><br />
|
||||||
|
<Link href="https://myetherwallet.github.io/knowledge-base/gas/what-is-gas-ethereum.html" target="_blank" rel="noreferrer noopener" isGreen>Read more</Link>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={ICONS.HELP}
|
||||||
|
color={colors.TEXT_SECONDARY}
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</InputLabelWrapper>
|
||||||
|
)}
|
||||||
|
bottomText={errors.gasPrice || warnings.gasPrice || infos.gasPrice}
|
||||||
|
value={gasPrice}
|
||||||
|
onChange={event => onGasPriceChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</GasInputRow>
|
||||||
|
|
||||||
|
<StyledTextarea
|
||||||
|
topLabel={(
|
||||||
|
<InputLabelWrapper>
|
||||||
|
Data
|
||||||
|
<Tooltip
|
||||||
|
content={(
|
||||||
|
<React.Fragment>
|
||||||
|
Data is usually used when you send transactions to contracts.
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={ICONS.HELP}
|
||||||
|
color={colors.TEXT_SECONDARY}
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</InputLabelWrapper>
|
||||||
|
)}
|
||||||
|
bottomText={errors.data || warnings.data || infos.data}
|
||||||
|
disabled={networkSymbol !== currency}
|
||||||
|
value={networkSymbol !== currency ? '' : data}
|
||||||
|
onChange={event => onDataChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AdvancedSettingsSendButtonWrapper>
|
||||||
|
{ props.children }
|
||||||
|
</AdvancedSettingsSendButtonWrapper>
|
||||||
|
</AdvancedSettingsWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdvancedForm;
|
@ -1,6 +1,6 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
import { Select } from 'components/Select';
|
import { Select } from 'components/Select';
|
||||||
import Button from 'components/Button';
|
import Button from 'components/Button';
|
||||||
@ -12,11 +12,9 @@ import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables';
|
|||||||
import colors from 'config/colors';
|
import colors from 'config/colors';
|
||||||
import P from 'components/Paragraph';
|
import P from 'components/Paragraph';
|
||||||
import { H2 } from 'components/Heading';
|
import { H2 } from 'components/Heading';
|
||||||
import Textarea from 'components/Textarea';
|
|
||||||
import Tooltip from 'components/Tooltip';
|
|
||||||
import { calculate, validation } from 'actions/SendFormActions';
|
|
||||||
import SelectedAccount from 'views/Wallet/components/SelectedAccount';
|
import SelectedAccount from 'views/Wallet/components/SelectedAccount';
|
||||||
import type { Token } from 'flowtype';
|
import type { Token } from 'flowtype';
|
||||||
|
import AdvancedForm from './components/Advanced';
|
||||||
import PendingTransactions from './components/PendingTransactions';
|
import PendingTransactions from './components/PendingTransactions';
|
||||||
|
|
||||||
import type { Props } from './Container';
|
import type { Props } from './Container';
|
||||||
@ -25,11 +23,6 @@ import type { Props } from './Container';
|
|||||||
// and put it inside config/variables.js
|
// and put it inside config/variables.js
|
||||||
const SmallScreenWidth = '850px';
|
const SmallScreenWidth = '850px';
|
||||||
|
|
||||||
type State = {
|
|
||||||
isAdvancedSettingsHidden: boolean,
|
|
||||||
shouldAnimateAdvancedSettingsToggle: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Wrapper = styled.section`
|
const Wrapper = styled.section`
|
||||||
padding: 0 48px;
|
padding: 0 48px;
|
||||||
`;
|
`;
|
||||||
@ -143,65 +136,12 @@ const SendButton = styled(Button)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const AdvancedSettingsWrapper = styled.div`
|
|
||||||
padding: 20px 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
border-top: 1px solid ${colors.DIVIDER};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GasInputRow = styled(InputRow)`
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GasInput = styled(Input)`
|
|
||||||
&:first-child {
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AdvancedSettingsSendButtonWrapper = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTextarea = styled(Textarea)`
|
|
||||||
margin-bottom: 20px;
|
|
||||||
height: 80px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AdvancedSettingsIcon = styled(Icon)`
|
const AdvancedSettingsIcon = styled(Icon)`
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const GreenSpan = styled.span`
|
// render helpers
|
||||||
color: ${colors.GREEN_PRIMARY};
|
const getAddressInputState = (address: string, addressErrors: string, addressWarnings: string): string => {
|
||||||
`;
|
|
||||||
|
|
||||||
const InputLabelWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
class AccountSend extends Component<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
isAdvancedSettingsHidden: true,
|
|
||||||
shouldAnimateAdvancedSettingsToggle: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(newProps: Props) {
|
|
||||||
calculate(this.props, newProps);
|
|
||||||
validation(newProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAddressInputState(address: string, addressErrors: string, addressWarnings: string) {
|
|
||||||
let state = '';
|
let state = '';
|
||||||
if (address && !addressErrors) {
|
if (address && !addressErrors) {
|
||||||
state = 'success';
|
state = 'success';
|
||||||
@ -213,9 +153,9 @@ class AccountSend extends Component<Props, State> {
|
|||||||
state = 'error';
|
state = 'error';
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
};
|
||||||
|
|
||||||
getAmountInputState(amountErrors: string, amountWarnings: string) {
|
const getAmountInputState = (amountErrors: string, amountWarnings: string): string => {
|
||||||
let state = '';
|
let state = '';
|
||||||
if (amountWarnings && !amountErrors) {
|
if (amountWarnings && !amountErrors) {
|
||||||
state = 'warning';
|
state = 'warning';
|
||||||
@ -224,56 +164,24 @@ class AccountSend extends Component<Props, State> {
|
|||||||
state = 'error';
|
state = 'error';
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
};
|
||||||
|
|
||||||
getGasLimitInputState(gasLimitErrors: string, gasLimitWarnings: string) {
|
const getTokensSelectData = (tokens: Array<Token>, accountNetwork: any): Array<{ value: string, label: string }> => {
|
||||||
let state = '';
|
|
||||||
if (gasLimitWarnings && !gasLimitErrors) {
|
|
||||||
state = 'warning';
|
|
||||||
}
|
|
||||||
if (gasLimitErrors) {
|
|
||||||
state = 'error';
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
getGasPriceInputState(gasPriceErrors: string, gasPriceWarnings: string) {
|
|
||||||
let state = '';
|
|
||||||
if (gasPriceWarnings && !gasPriceErrors) {
|
|
||||||
state = 'warning';
|
|
||||||
}
|
|
||||||
if (gasPriceErrors) {
|
|
||||||
state = 'error';
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTokensSelectData(tokens: Array<Token>, accountNetwork: any) {
|
|
||||||
const tokensSelectData: Array<{ value: string, label: string }> = tokens.map(t => ({ value: t.symbol, label: t.symbol }));
|
const tokensSelectData: Array<{ value: string, label: string }> = tokens.map(t => ({ value: t.symbol, label: t.symbol }));
|
||||||
tokensSelectData.unshift({ value: accountNetwork.symbol, label: accountNetwork.symbol });
|
tokensSelectData.unshift({ value: accountNetwork.symbol, label: accountNetwork.symbol });
|
||||||
|
|
||||||
return tokensSelectData;
|
return tokensSelectData;
|
||||||
}
|
};
|
||||||
|
|
||||||
handleToggleAdvancedSettingsButton() {
|
// stateless component
|
||||||
this.toggleAdvancedSettings();
|
const AccountSend = (props: Props) => {
|
||||||
}
|
const device = props.wallet.selectedDevice;
|
||||||
|
|
||||||
toggleAdvancedSettings() {
|
|
||||||
this.setState(previousState => ({
|
|
||||||
isAdvancedSettingsHidden: !previousState.isAdvancedSettingsHidden,
|
|
||||||
shouldAnimateAdvancedSettingsToggle: true,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const device = this.props.wallet.selectedDevice;
|
|
||||||
const {
|
const {
|
||||||
account,
|
account,
|
||||||
network,
|
network,
|
||||||
discovery,
|
discovery,
|
||||||
tokens,
|
tokens,
|
||||||
} = this.props.selectedAccount;
|
} = props.selectedAccount;
|
||||||
const {
|
const {
|
||||||
address,
|
address,
|
||||||
amount,
|
amount,
|
||||||
@ -282,19 +190,17 @@ class AccountSend extends Component<Props, State> {
|
|||||||
currency,
|
currency,
|
||||||
feeLevels,
|
feeLevels,
|
||||||
selectedFeeLevel,
|
selectedFeeLevel,
|
||||||
recommendedGasPrice,
|
|
||||||
gasPriceNeedsUpdate,
|
gasPriceNeedsUpdate,
|
||||||
total,
|
total,
|
||||||
errors,
|
errors,
|
||||||
warnings,
|
warnings,
|
||||||
infos,
|
infos,
|
||||||
data,
|
|
||||||
sending,
|
sending,
|
||||||
gasLimit,
|
advanced,
|
||||||
gasPrice,
|
} = props.sendForm;
|
||||||
} = this.props.sendForm;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
toggleAdvanced,
|
||||||
onAddressChange,
|
onAddressChange,
|
||||||
onAmountChange,
|
onAmountChange,
|
||||||
onSetMax,
|
onSetMax,
|
||||||
@ -302,10 +208,7 @@ class AccountSend extends Component<Props, State> {
|
|||||||
onFeeLevelChange,
|
onFeeLevelChange,
|
||||||
updateFeeLevels,
|
updateFeeLevels,
|
||||||
onSend,
|
onSend,
|
||||||
onGasLimitChange,
|
} = props.sendFormActions;
|
||||||
onGasPriceChange,
|
|
||||||
onDataChange,
|
|
||||||
} = this.props.sendFormActions;
|
|
||||||
|
|
||||||
if (!device || !account || !discovery || !network) return null;
|
if (!device || !account || !discovery || !network) return null;
|
||||||
|
|
||||||
@ -328,26 +231,16 @@ class AccountSend extends Component<Props, State> {
|
|||||||
isSendButtonDisabled = true;
|
isSendButtonDisabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokensSelectData = this.getTokensSelectData(tokens, network);
|
const tokensSelectData = getTokensSelectData(tokens, network);
|
||||||
|
const isAdvancedSettingsHidden = !advanced;
|
||||||
let gasLimitTooltipCurrency: string;
|
|
||||||
let gasLimitTooltipValue: string;
|
|
||||||
if (networkSymbol !== currency) {
|
|
||||||
gasLimitTooltipCurrency = 'tokens';
|
|
||||||
gasLimitTooltipValue = network.defaultGasLimitTokens.toString(10);
|
|
||||||
} else {
|
|
||||||
gasLimitTooltipCurrency = networkSymbol;
|
|
||||||
gasLimitTooltipValue = network.defaultGasLimit.toString(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectedAccount {...this.props}>
|
<SelectedAccount {...props}>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<StyledH2>Send Ethereum or tokens</StyledH2>
|
<StyledH2>Send Ethereum or tokens</StyledH2>
|
||||||
<InputRow>
|
<InputRow>
|
||||||
<Input
|
<Input
|
||||||
state={this.getAddressInputState(address, errors.address, warnings.address)}
|
state={getAddressInputState(address, errors.address, warnings.address)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
@ -361,7 +254,7 @@ class AccountSend extends Component<Props, State> {
|
|||||||
|
|
||||||
<InputRow>
|
<InputRow>
|
||||||
<Input
|
<Input
|
||||||
state={this.getAmountInputState(errors.amount, warnings.amount)}
|
state={getAmountInputState(errors.amount, warnings.amount)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
@ -427,12 +320,7 @@ class AccountSend extends Component<Props, State> {
|
|||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
value={selectedFeeLevel}
|
value={selectedFeeLevel}
|
||||||
onChange={(option) => {
|
onChange={onFeeLevelChange}
|
||||||
if (option.value === 'Custom') {
|
|
||||||
this.toggleAdvancedSettings();
|
|
||||||
}
|
|
||||||
onFeeLevelChange(option);
|
|
||||||
}}
|
|
||||||
options={feeLevels}
|
options={feeLevels}
|
||||||
formatOptionLabel={option => (
|
formatOptionLabel={option => (
|
||||||
<FeeOptionWrapper>
|
<FeeOptionWrapper>
|
||||||
@ -444,26 +332,26 @@ class AccountSend extends Component<Props, State> {
|
|||||||
</InputRow>
|
</InputRow>
|
||||||
|
|
||||||
<ToggleAdvancedSettingsWrapper
|
<ToggleAdvancedSettingsWrapper
|
||||||
isAdvancedSettingsHidden={this.state.isAdvancedSettingsHidden}
|
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
|
||||||
>
|
>
|
||||||
<ToggleAdvancedSettingsButton
|
<ToggleAdvancedSettingsButton
|
||||||
isTransparent
|
isTransparent
|
||||||
onClick={() => this.handleToggleAdvancedSettingsButton()}
|
onClick={toggleAdvanced}
|
||||||
>
|
>
|
||||||
Advanced settings
|
Advanced settings
|
||||||
<AdvancedSettingsIcon
|
<AdvancedSettingsIcon
|
||||||
icon={ICONS.ARROW_DOWN}
|
icon={ICONS.ARROW_DOWN}
|
||||||
color={colors.TEXT_SECONDARY}
|
color={colors.TEXT_SECONDARY}
|
||||||
size={24}
|
size={24}
|
||||||
isActive={this.state.isAdvancedSettingsHidden}
|
isActive={advanced}
|
||||||
canAnimate={this.state.shouldAnimateAdvancedSettingsToggle}
|
canAnimate
|
||||||
/>
|
/>
|
||||||
</ToggleAdvancedSettingsButton>
|
</ToggleAdvancedSettingsButton>
|
||||||
|
|
||||||
{this.state.isAdvancedSettingsHidden && (
|
{isAdvancedSettingsHidden && (
|
||||||
<SendButton
|
<SendButton
|
||||||
isDisabled={isSendButtonDisabled}
|
isDisabled={isSendButtonDisabled}
|
||||||
isAdvancedSettingsHidden={this.state.isAdvancedSettingsHidden}
|
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
|
||||||
onClick={() => onSend()}
|
onClick={() => onSend()}
|
||||||
>
|
>
|
||||||
{sendButtonText}
|
{sendButtonText}
|
||||||
@ -471,125 +359,27 @@ class AccountSend extends Component<Props, State> {
|
|||||||
)}
|
)}
|
||||||
</ToggleAdvancedSettingsWrapper>
|
</ToggleAdvancedSettingsWrapper>
|
||||||
|
|
||||||
{!this.state.isAdvancedSettingsHidden && (
|
{advanced && (
|
||||||
<AdvancedSettingsWrapper>
|
<AdvancedForm {...props}>
|
||||||
<GasInputRow>
|
|
||||||
<GasInput
|
|
||||||
state={this.getGasLimitInputState(errors.gasLimit, warnings.gasLimit)}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
spellCheck="false"
|
|
||||||
topLabel={(
|
|
||||||
<InputLabelWrapper>
|
|
||||||
Gas limit
|
|
||||||
<Tooltip
|
|
||||||
content={(
|
|
||||||
<React.Fragment>
|
|
||||||
Gas limit is the amount of gas to send with your transaction.<br />
|
|
||||||
<GreenSpan>TX fee = gas price * gas limit</GreenSpan> & is paid to miners for including your TX in a block.<br />
|
|
||||||
Increasing this number will not get your TX mined faster.<br />
|
|
||||||
Default value for sending {gasLimitTooltipCurrency} is <GreenSpan>{gasLimitTooltipValue}</GreenSpan>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon={ICONS.HELP}
|
|
||||||
color={colors.TEXT_SECONDARY}
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</InputLabelWrapper>
|
|
||||||
)}
|
|
||||||
bottomText={errors.gasLimit || warnings.gasLimit || infos.gasLimit}
|
|
||||||
value={gasLimit}
|
|
||||||
isDisabled={networkSymbol === currency && data.length > 0}
|
|
||||||
onChange={event => onGasLimitChange(event.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<GasInput
|
|
||||||
state={this.getGasPriceInputState(errors.gasPrice, warnings.gasPrice)}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
spellCheck="false"
|
|
||||||
topLabel={(
|
|
||||||
<InputLabelWrapper>
|
|
||||||
Gas price
|
|
||||||
<Tooltip
|
|
||||||
content={(
|
|
||||||
<React.Fragment>
|
|
||||||
Gas Price is the amount you pay per unit of gas.<br />
|
|
||||||
<GreenSpan>TX fee = gas price * gas limit</GreenSpan> & is paid to miners for including your TX in a block.<br />
|
|
||||||
Higher the gas price = faster transaction, but more expensive. Recommended is <GreenSpan>{recommendedGasPrice} GWEI.</GreenSpan><br />
|
|
||||||
<Link href="https://myetherwallet.github.io/knowledge-base/gas/what-is-gas-ethereum.html" target="_blank" rel="noreferrer noopener" isGreen>Read more</Link>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon={ICONS.HELP}
|
|
||||||
color={colors.TEXT_SECONDARY}
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</InputLabelWrapper>
|
|
||||||
)}
|
|
||||||
bottomText={errors.gasPrice || warnings.gasPrice || infos.gasPrice}
|
|
||||||
value={gasPrice}
|
|
||||||
onChange={event => onGasPriceChange(event.target.value)}
|
|
||||||
/>
|
|
||||||
</GasInputRow>
|
|
||||||
|
|
||||||
<StyledTextarea
|
|
||||||
topLabel={(
|
|
||||||
<InputLabelWrapper>
|
|
||||||
Data
|
|
||||||
<Tooltip
|
|
||||||
content={(
|
|
||||||
<React.Fragment>
|
|
||||||
Data is usually used when you send transactions to contracts.
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon={ICONS.HELP}
|
|
||||||
color={colors.TEXT_SECONDARY}
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</InputLabelWrapper>
|
|
||||||
)}
|
|
||||||
disabled={networkSymbol !== currency}
|
|
||||||
value={networkSymbol !== currency ? '' : data}
|
|
||||||
onChange={event => onDataChange(event.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AdvancedSettingsSendButtonWrapper>
|
|
||||||
<SendButton
|
<SendButton
|
||||||
isDisabled={isSendButtonDisabled}
|
isDisabled={isSendButtonDisabled}
|
||||||
onClick={() => onSend()}
|
onClick={() => onSend()}
|
||||||
>
|
>
|
||||||
{sendButtonText}
|
{sendButtonText}
|
||||||
</SendButton>
|
</SendButton>
|
||||||
</AdvancedSettingsSendButtonWrapper>
|
</AdvancedForm>
|
||||||
</AdvancedSettingsWrapper>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.props.selectedAccount.pending.length > 0 && (
|
{props.selectedAccount.pending.length > 0 && (
|
||||||
<PendingTransactions
|
<PendingTransactions
|
||||||
pending={this.props.selectedAccount.pending}
|
pending={props.selectedAccount.pending}
|
||||||
tokens={this.props.selectedAccount.tokens}
|
tokens={props.selectedAccount.tokens}
|
||||||
network={network}
|
network={network}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</SelectedAccount>
|
</SelectedAccount>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default AccountSend;
|
export default AccountSend;
|
||||||
|
Loading…
Reference in New Issue
Block a user