/* @flow */ 'use strict'; import * as SEND from './constants/send'; import * as NOTIFICATION from './constants/notification'; import { estimateGas, getGasPrice, pushTx } from './Web3Actions'; import EthereumjsUtil from 'ethereumjs-util'; import EthereumjsUnits from 'ethereumjs-units'; import EthereumjsTx from 'ethereumjs-tx'; import TrezorConnect from 'trezor-connect'; import { strip } from '../utils/ethUtils'; import { push } from 'react-router-redux'; import BigNumber from 'bignumber.js'; import { initialState } from '../reducers/SendFormReducer'; import { findAccount } from '../reducers/AccountsReducer'; import { findToken } from '../reducers/TokensReducer'; import { findSelectedDevice, findDevice } from '../reducers/TrezorConnectReducer'; import type { Dispatch, GetState, Action, ThunkAction, AsyncAction, RouterLocationState, TrezorDevice } from '~/flowtype'; import type { State as AccountState } from '../reducers/AbstractAccountReducer'; import type { Web3Instance } from '../reducers/Web3Reducer'; import type { Config, Coin } from '../reducers/LocalStorageReducer'; import type { Token } from '../reducers/TokensReducer'; import type { State, FeeLevel } from '../reducers/SendFormReducer'; import type { Account } from '../reducers/AccountsReducer'; export type SendTxAction = { type: typeof SEND.TX_COMPLETE, account: Account, selectedCurrency: string, amount: string, txid: string, txData: any, }; export type SendFormAction = SendTxAction | { type: typeof SEND.INIT, state: State } | { type: typeof SEND.DISPOSE } | { type: typeof SEND.TOGGLE_ADVANCED } | { type: typeof SEND.VALIDATION, errors: {[k: string]: string}, warnings: {[k: string]: string}, infos: {[k: string]: string} } | { type: typeof SEND.ADDRESS_CHANGE, state: State } | { type: typeof SEND.AMOUNT_CHANGE, state: State } | { type: typeof SEND.CURRENCY_CHANGE, state: State } | { type: typeof SEND.SET_MAX, state: State } | { type: typeof SEND.FEE_LEVEL_CHANGE, state: State } | { type: typeof SEND.UPDATE_FEE_LEVELS, state: State } | { type: typeof SEND.FEE_LEVEL_CHANGE, state: State } | { type: typeof SEND.GAS_PRICE_CHANGE, state: State } | { type: typeof SEND.GAS_LIMIT_CHANGE, state: State } | { type: typeof SEND.NONCE_CHANGE, state: State } | { type: typeof SEND.DATA_CHANGE, state: State } | { type: typeof SEND.SEND, } | { type: typeof SEND.TX_ERROR, } | { type: typeof SEND.FROM_SESSION_STORAGE, address: string, amount: string, setMax: boolean, selectedCurrency: string, selectedFeeLevel: any, advanced: boolean, gasLimit: string, gasPrice: string, data: string, nonce: string, touched: any, } //const numberRegExp = new RegExp('^([0-9]{0,10}\\.)?[0-9]{1,18}$'); const numberRegExp: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$'); const calculateFee = (gasPrice: string, gasLimit: string): string => { return EthereumjsUnits.convert( new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether'); } const calculateTotal = (amount: string, gasPrice: string, gasLimit: string): string => { try { return new BigNumber(amount).plus( calculateFee(gasPrice, gasLimit) ).toString(); } catch (error) { return '0'; } } const calculateMaxAmount = (balance: string, gasPrice: string, gasLimit: string): string => { try { const fee = EthereumjsUnits.convert( new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether'); const b = new BigNumber(balance); const max = b.minus(fee); if (max.lessThan(0)) return '0'; return max.toString(); } catch (error) { return '0'; } } const setAmountAndTotal = (state: State): State => { // if (state.setMax) { // const account: ?Account = findAccount(getState().accounts, accountState.index, accountState.deviceState, accountState.network); // if (!account) return; // if (isToken) { // const token: ?Token = findToken(getState().tokens, account.address, state.selectedCurrency, account.deviceState); // if (!token) return; // state.amount = token.balance; // } else { // state.amount = calculateMaxAmount(account.balance, state.gasPrice, state.gasLimit); // } // } // state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); return state; } export const getFeeLevels = (symbol: string, gasPrice: BigNumber | string, gasLimit: string, selected?: FeeLevel): Array => { 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(); const low: string = price.minus(quarter.times(2)).toString(); 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(), gasLimit) } ${ symbol }` }, { value: 'Low', gasPrice: low, label: `${ calculateFee(low, gasLimit) } ${ symbol }` }, customLevel ] } // initialize component export const init = (): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { const accountState: ?AccountState = getState().abstractAccount; if (!accountState) return; const { location } = getState().router; const urlParams: RouterLocationState = location.state; const selected: ?TrezorDevice = findSelectedDevice( getState().connect ); if (!selected) return; const web3instance: ?Web3Instance = getState().web3.find(w3 => w3.network === urlParams.network); if (!web3instance) return; const account = getState().accounts.find(a => a.deviceState === accountState.deviceState && a.index === accountState.index && a.network === accountState.network); if (!account) return; // TODO: check if there are some unfinished tx in localStorage const coin: Coin = accountState.coin; const gasPrice: BigNumber = new BigNumber( EthereumjsUnits.convert(web3instance.gasPrice, 'wei', 'gwei') ) || new BigNumber(coin.defaultGasPrice); const gasLimit: string = coin.defaultGasLimit.toString(); const feeLevels: Array = getFeeLevels(coin.symbol, gasPrice, gasLimit); // TODO: get nonce const state: State = { ...initialState, network: coin.network, coinSymbol: coin.symbol, selectedCurrency: coin.symbol, feeLevels, selectedFeeLevel: feeLevels.find(f => f.value === 'Normal'), recommendedGasPrice: gasPrice.toString(), gasLimit, gasPrice: gasPrice.toString(), nonce: account.nonce.toString(), // TODO!!! }; dispatch({ type: SEND.INIT, state }); } } export const dispose = (): Action => { return { type: SEND.DISPOSE } } export const toggleAdvanced = (address: string): Action => { return { type: SEND.TOGGLE_ADVANCED } } export const validation = (): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { const accountState: ?AccountState = getState().abstractAccount; if (!accountState) return; const state: State = getState().sendForm; const errors: {[k: string]: string} = {}; const warnings: {[k: string]: string} = {}; const infos: {[k: string]: string} = {}; if (state.untouched) return; // valid address if (state.touched.address) { const accounts = getState().accounts; const savedAccounts = accounts.filter(a => a.address.toLowerCase() === state.address.toLowerCase()); if (state.address.length < 1) { errors.address = 'Address is not set'; } else if (!EthereumjsUtil.isValidAddress(state.address)) { errors.address = 'Address is not valid'; } else if (savedAccounts.length > 0) { // check if founded account belongs to this network // corner-case: when same derivation path is used on different networks const currentNetworkAccount = savedAccounts.find(a => a.network === accountState.network); if (currentNetworkAccount) { const device: ?TrezorDevice = findDevice(getState().connect.devices, currentNetworkAccount.deviceID, currentNetworkAccount.deviceState); if (!device) return; infos.address = `${ device.instanceLabel } Account #${ (currentNetworkAccount.index + 1) }`; } else { const device: ?TrezorDevice = findDevice(getState().connect.devices, savedAccounts[0].deviceID, savedAccounts[0].deviceState); if (!device) return; warnings.address = `Looks like it's ${ device.instanceLabel } Account #${ (savedAccounts[0].index + 1) } address of ${ savedAccounts[0].network.toUpperCase() } network`; } } } // valid amount // https://stackoverflow.com/a/42701461 //const regexp = new RegExp('^(?:[0-9]{0,10}\\.)?[0-9]{1,18}$'); if (state.touched.amount) { if (state.amount.length < 1) { errors.amount = 'Amount is not set'; } else if (state.amount.length > 0 && !state.amount.match(numberRegExp)) { errors.amount = 'Amount is not a number'; } else { const account: ?Account = findAccount(getState().accounts, accountState.index, accountState.deviceState, accountState.network); if (!account) return; let decimalRegExp: RegExp; if (state.selectedCurrency !== state.coinSymbol) { const token = findToken(getState().tokens, account.address, state.selectedCurrency, account.deviceState); if (token) { if (parseInt(token.decimals) > 0) { //decimalRegExp = new RegExp('^(0|0\\.([0-9]{0,' + token.decimals + '})?|[1-9]+\\.?([0-9]{0,' + token.decimals + '})?|\\.[0-9]{1,' + token.decimals + '})$'); decimalRegExp = new RegExp('^(0|0\\.([0-9]{0,' + token.decimals + '})?|[1-9][0-9]*\\.?([0-9]{0,' + token.decimals + '})?|\\.[0-9]{1,' + token.decimals + '})$'); } else { // decimalRegExp = new RegExp('^(0|0\\.?|[1-9]+\\.?)$'); decimalRegExp = new RegExp('^[0-9]+$'); } if (!state.amount.match(decimalRegExp)) { errors.amount = `Maximum ${ token.decimals} decimals allowed`; } else if (new BigNumber(state.total).greaterThan(account.balance)) { errors.amount = `Not enough ${ state.coinSymbol } to cover transaction fee`; } else if (new BigNumber(state.amount).greaterThan(token.balance)) { errors.amount = 'Not enough funds'; } else if (new BigNumber(state.amount).lessThanOrEqualTo('0')) { errors.amount = 'Amount is too low'; } } } else { decimalRegExp = new RegExp('^(0|0\\.([0-9]{0,18})?|[1-9][0-9]*\\.?([0-9]{0,18})?|\\.[0-9]{0,18})$'); if (!state.amount.match(decimalRegExp)) { errors.amount = `Maximum 18 decimals allowed`; } else if (new BigNumber(state.total).greaterThan(account.balance)) { errors.amount = 'Not enough funds'; } } } } // valid gas limit if (state.touched.gasLimit) { if (state.gasLimit.length < 1) { errors.gasLimit = 'Gas limit is not set'; } else if (state.gasLimit.length > 0 && !state.gasLimit.match(numberRegExp)) { errors.gasLimit = 'Gas limit is not a number'; } else { const gl: BigNumber = new BigNumber(state.gasLimit); if (gl.lessThan(1)) { errors.gasLimit = 'Gas limit is too low'; } else if (gl.lessThan( state.selectedCurrency !== state.coinSymbol ? accountState.coin.defaultGasLimitTokens : accountState.coin.defaultGasLimit )) { warnings.gasLimit = 'Gas limit is below recommended'; } } } // valid gas price if (state.touched.gasPrice) { if (state.gasPrice.length < 1) { errors.gasPrice = 'Gas price is not set'; } else if (state.gasPrice.length > 0 && !state.gasPrice.match(numberRegExp)) { errors.gasPrice = 'Gas price is not a number'; } else { const gp: BigNumber = new BigNumber(state.gasPrice); if (gp.greaterThan(1000)) { warnings.gasPrice = 'Gas price is too high'; } else if (gp.lessThanOrEqualTo('0')) { errors.gasPrice = 'Gas price is too low'; } } } // valid nonce if (state.touched.nonce) { const re = new RegExp('^[0-9]+$'); if (state.nonce.length < 1) { errors.nonce = 'Nonce is not set'; } else if (!state.nonce.match(re)) { errors.nonce = 'Nonce is not a valid number'; } else { const account: ?Account = findAccount(getState().accounts, accountState.index, accountState.deviceState, accountState.network); if (!account) return; const n: BigNumber = new BigNumber(state.nonce); if (n.lessThan(account.nonce)) { warnings.nonce = 'Nonce is lower than recommended'; } else if (n.greaterThan(account.nonce)) { warnings.nonce = 'Nonce is greater than recommended'; } } } // valid data if (state.touched.data && state.data.length > 0) { const re = /^[0-9A-Fa-f]+$/g; if (!re.test(state.data)) { errors.data = 'Data is not valid hexadecimal'; } } // valid nonce? dispatch({ type: SEND.VALIDATION, errors, warnings, infos }); } } export const onAddressChange = (address: string): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { const currentState: State = getState().sendForm; const touched = { ...currentState.touched }; touched.address = true; const state: State = { ...currentState, untouched: false, touched, address }; dispatch({ type: SEND.ADDRESS_CHANGE, state }); dispatch( validation() ); } } export const onAmountChange = (amount: string): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { const accountState: ?AccountState = getState().abstractAccount; const currentState: State = getState().sendForm; const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; const touched = { ...currentState.touched }; touched.amount = true; const total: string = calculateTotal(isToken ? '0' : amount, currentState.gasPrice, currentState.gasLimit); const state: State = { ...currentState, untouched: false, touched, setMax: false, amount, total }; dispatch({ type: SEND.AMOUNT_CHANGE, state }); dispatch( validation() ); } } export const onCurrencyChange = (currency: { value: string, label: string }): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { const accountState: ?AccountState = getState().abstractAccount; if (!accountState) return; const currentState: State = getState().sendForm; const isToken: boolean = currency.value !== currentState.coinSymbol; const account: ?Account = findAccount(getState().accounts, accountState.index, accountState.deviceState, accountState.network); if (!account) { // account not found return; } const coin: Coin = accountState.coin; let gasLimit: string = ''; let amount: string = currentState.amount; let total: string; if (isToken) { gasLimit = coin.defaultGasLimitTokens.toString(); if (currentState.setMax) { const token: ?Token = findToken(getState().tokens, account.address, currency.value, accountState.deviceState); if (!token) return; amount = token.balance; } total = calculateTotal('0', currentState.gasPrice, currentState.gasLimit); } else { gasLimit = coin.defaultGasLimit.toString(); if (currentState.setMax) { amount = calculateMaxAmount(account.balance, currentState.gasPrice, currentState.gasLimit); } total = calculateTotal(amount, currentState.gasPrice, currentState.gasLimit); } const feeLevels: Array = getFeeLevels(currentState.coinSymbol, currentState.gasPrice, gasLimit, currentState.selectedFeeLevel); const selectedFeeLevel: ?FeeLevel = feeLevels.find(f => f.value === currentState.selectedFeeLevel.value); if (!selectedFeeLevel) return; const state: State = { ...currentState, selectedCurrency: currency.value, amount, total, feeLevels, selectedFeeLevel, gasLimit, }; dispatch({ type: SEND.CURRENCY_CHANGE, state }); dispatch( validation() ); } } export const onSetMax = (): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { const accountState: ?AccountState = getState().abstractAccount; if (!accountState) return; const currentState: State = getState().sendForm; const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; const touched = { ...currentState.touched }; touched.amount = true; const account: ?Account = findAccount(getState().accounts, accountState.index, accountState.deviceState, accountState.network); if (!account) { // account not found return; } let amount: string = currentState.amount; let total: string = currentState.total; if (!currentState.setMax) { if (isToken) { const token: ?Token = findToken(getState().tokens, account.address, currentState.selectedCurrency, accountState.deviceState); if (!token) return; amount = token.balance; total = calculateTotal('0', currentState.gasPrice, currentState.gasLimit); } else { amount = calculateMaxAmount(account.balance, currentState.gasPrice, currentState.gasLimit); total = calculateTotal(amount, currentState.gasPrice, currentState.gasLimit); } } const state: State = { ...currentState, untouched: false, touched, setMax: !currentState.setMax, amount, total }; dispatch({ type: SEND.SET_MAX, state }); dispatch( validation() ); } } export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { const accountState: ?AccountState = getState().abstractAccount; if (!accountState) return; const currentState: State = getState().sendForm; const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; const coin: Coin = accountState.coin; const state: State = { ...currentState, untouched: false, selectedFeeLevel: feeLevel, }; if (feeLevel.value === 'Custom') { state.advanced = true; feeLevel.gasPrice = state.gasPrice; feeLevel.label = `${ calculateFee(state.gasPrice, state.gasLimit) } ${ state.coinSymbol }`; } else { const customLevel: ?FeeLevel = state.feeLevels.find(f => f.value === 'Custom'); if (customLevel) customLevel.label = ''; state.gasPrice = feeLevel.gasPrice; state.gasLimit = isToken ? coin.defaultGasLimitTokens.toString() : coin.defaultGasLimit.toString(); } if (currentState.setMax) { const account: ?Account = findAccount(getState().accounts, accountState.index, accountState.deviceState, accountState.network); if (!account) return; if (isToken) { const token: ?Token = findToken(getState().tokens, account.address, currentState.selectedCurrency, accountState.deviceState); if (!token) return; state.amount = token.balance; } else { state.amount = calculateMaxAmount(account.balance, state.gasPrice, state.gasLimit); } } state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); dispatch({ type: SEND.FEE_LEVEL_CHANGE, state }); dispatch( validation() ); } } // Manually triggered from user // Update gasPrice to recommended value export const updateFeeLevels = (): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { const accountState: ?AccountState = getState().abstractAccount; if (!accountState) return; const currentState: State = getState().sendForm; const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; const feeLevels: Array = getFeeLevels(currentState.coinSymbol, currentState.recommendedGasPrice, currentState.gasLimit, currentState.selectedFeeLevel); const selectedFeeLevel: ?FeeLevel = feeLevels.find(f => f.value === currentState.selectedFeeLevel.value); if (!selectedFeeLevel) return; const state: State = { ...currentState, feeLevels, selectedFeeLevel, gasPrice: selectedFeeLevel.gasPrice, gasPriceNeedsUpdate: false, }; if (currentState.setMax) { const account: ?Account = findAccount(getState().accounts, accountState.index, accountState.deviceState, accountState.network); if (!account) return; if (isToken) { const token: ?Token = findToken(getState().tokens, account.address, state.selectedCurrency, accountState.deviceState); if (!token) return; const tokenBalance: string = token.balance; state.amount = tokenBalance; } else { state.amount = calculateMaxAmount(account.balance, state.gasPrice, state.gasLimit); } } state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); dispatch({ type: SEND.UPDATE_FEE_LEVELS, state }); dispatch( validation() ); } } export const onGasPriceChange = (gasPrice: string): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { const accountState: ?AccountState = getState().abstractAccount; if (!accountState) return; const currentState: State = getState().sendForm; const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; const touched = { ...currentState.touched }; touched.gasPrice = true; const state: State = { ...currentState, untouched: false, touched, gasPrice: gasPrice, }; if (gasPrice.match(numberRegExp) && state.gasLimit.match(numberRegExp)) { const customLevel = currentState.feeLevels.find(f => f.value === 'Custom'); if (!customLevel) return; customLevel.gasPrice = gasPrice; customLevel.label = `${ calculateFee(gasPrice, state.gasLimit) } ${ state.coinSymbol }`; state.selectedFeeLevel = customLevel; if (currentState.setMax) { const account: ?Account = findAccount(getState().accounts, accountState.index, accountState.deviceState, accountState.network); if (!account) return; if (isToken) { const token: ?Token = findToken(getState().tokens, account.address, state.selectedCurrency, accountState.deviceState); if (!token) return; const tokenBalance: string = token.balance; state.amount = tokenBalance; } else { state.amount = calculateMaxAmount(account.balance, state.gasPrice, state.gasLimit); } } state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); } else { // state.gasPrice = currentState.gasPrice; } dispatch({ type: SEND.GAS_PRICE_CHANGE, state }); dispatch( validation() ); } } export const onGasLimitChange = (gasLimit: string): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { const accountState: ?AccountState = getState().abstractAccount; if (!accountState) return; const currentState: State = getState().sendForm; const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; const touched = { ...currentState.touched }; touched.gasLimit = true; const state: State = { ...currentState, untouched: false, touched, gasLimit, }; if (gasLimit.match(numberRegExp) && state.gasPrice.match(numberRegExp)) { const customLevel: ?FeeLevel = state.feeLevels.find(f => f.value === 'Custom'); if (!customLevel) return; customLevel.label = `${ calculateFee(currentState.gasPrice, gasLimit) } ${ state.coinSymbol }`; state.selectedFeeLevel = customLevel; if (state.setMax) { const account: ?Account = findAccount(getState().accounts, accountState.index, accountState.deviceState, accountState.network); if (!account) return; if (isToken) { const token: ?Token = findToken(getState().tokens, account.address, state.selectedCurrency, accountState.deviceState); if (!token) return; const tokenBalance: string = token.balance; state.amount = tokenBalance; } else { state.amount = calculateMaxAmount(account.balance, state.gasPrice, state.gasLimit); } } } state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); dispatch({ type: SEND.GAS_LIMIT_CHANGE, state }); dispatch( validation() ); } } export const onNonceChange = (nonce: string): AsyncAction => { return async (dispatch: Dispatch, getState: GetState): Promise => { const currentState: State = getState().sendForm; const touched = { ...currentState.touched }; touched.nonce = true; const state: State = { ...currentState, untouched: false, touched, nonce, }; dispatch({ type: SEND.NONCE_CHANGE, state }); dispatch( validation() ); } } export const onDataChange = (data: string): AsyncAction => { return async (dispatch: Dispatch, getState: GetState): Promise => { const currentState: State = getState().sendForm; const touched = { ...currentState.touched }; touched.data = true; const state: State = { ...currentState, untouched: false, touched, data, }; dispatch({ type: SEND.DATA_CHANGE, state }); dispatch( validation() ); dispatch( estimateGasPrice() ); } } const estimateGasPrice = (): AsyncAction => { return async (dispatch: Dispatch, getState: GetState): Promise => { const accountState: ?AccountState = getState().abstractAccount; if (!accountState) return; const web3instance: ?Web3Instance = getState().web3.filter(w3 => w3.network === accountState.network)[0]; if (!web3instance) return; const web3 = web3instance.web3; let currentState: State = getState().sendForm; const data: string = '0x' + (currentState.data.length % 2 === 0 ? currentState.data : '0' + currentState.data); const gasLimit = await estimateGas(web3instance.web3, { to: '0xdb6e09ddca62d0959dc4725697e66b8152222aee', // TODO: real adress data, value: web3.toHex(web3.toWei(currentState.amount, 'ether')), gasPrice: web3.toHex( EthereumjsUnits.convert(currentState.gasPrice, 'gwei', 'wei') ), }); currentState = getState().sendForm; dispatch({ type: SEND.GAS_LIMIT_CHANGE, state: { ...currentState, gasLimit: gasLimit.toString() } }); } } export const onSend = (): AsyncAction => { //return onSendERC20(); return async (dispatch: Dispatch, getState: GetState): Promise => { const accountState: ?AccountState = getState().abstractAccount; if (!accountState) return; const currentState: State = getState().sendForm; const web3instance: ?Web3Instance = getState().web3.filter(w3 => w3.network === accountState.network)[0]; const account: ?Account = findAccount(getState().accounts, accountState.index, accountState.deviceState, accountState.network); if (!account || !web3instance) return; const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; const web3 = web3instance.web3; const address_n = account.addressPath; let data: string = '0x' + currentState.data; let txAmount: string = web3.toHex(web3.toWei(currentState.amount, 'ether')); let txAddress: string = currentState.address; if (isToken) { const token: ?Token = findToken(getState().tokens, account.address, currentState.selectedCurrency, accountState.deviceState); if (!token) return; const contract = web3instance.erc20.at(token.address); const amountValue: string = new BigNumber(currentState.amount).times( Math.pow(10, token.decimals) ).toString(); data = contract.transfer.getData(currentState.address, amountValue, { from: account.address, gasLimit: currentState.gasLimit, gasPrice: currentState.gasPrice }); txAmount = '0x00'; txAddress = token.address; } const txData = { address_n, // from: currentAddress.address to: txAddress, value: txAmount, data, //chainId: 3 // ropsten chainId: web3instance.chainId, nonce: web3.toHex(account.nonce), gasLimit: web3.toHex(currentState.gasLimit), gasPrice: web3.toHex( EthereumjsUnits.convert(currentState.gasPrice, 'gwei', 'wei') ), r: '', s: '', v: '' } const selected: ?TrezorDevice = findSelectedDevice(getState().connect); if (!selected) return; let signedTransaction = await TrezorConnect.ethereumSignTransaction({ device: { path: selected.path, instance: selected.instance, state: selected.state }, //path: "m/44'/60'/0'/0/0", path: txData.address_n, nonce: strip(txData.nonce), gasPrice: strip(txData.gasPrice), gasLimit: strip(txData.gasLimit), to: strip(txData.to), value: strip(txData.value), data: strip(txData.data), chainId: txData.chainId }); if (!signedTransaction || !signedTransaction.success) { dispatch({ type: NOTIFICATION.ADD, payload: { type: 'error', title: 'Transaction error', message: signedTransaction.payload.error, cancelable: true, actions: [ ] } }) return; } txData.r = '0x' + signedTransaction.payload.r; txData.s = '0x' + signedTransaction.payload.s; txData.v = web3.toHex(signedTransaction.payload.v); try { const tx = new EthereumjsTx(txData); const serializedTx = '0x' + tx.serialize().toString('hex'); const txid: string = await pushTx(web3, serializedTx); const coin: Coin = accountState.coin; dispatch({ type: SEND.TX_COMPLETE, account: account, selectedCurrency: currentState.selectedCurrency, amount: currentState.amount, txid, txData, }); dispatch({ type: NOTIFICATION.ADD, payload: { type: 'success', title: 'Transaction success', message: `See transaction detail`, cancelable: true, actions: [] } }); } catch (error) { dispatch({ type: NOTIFICATION.ADD, payload: { type: 'error', title: 'Transaction error', message: error.message || error, cancelable: true, actions: [ ] } }); } } } export default { init, dispose, toggleAdvanced, onAddressChange, onAmountChange, onCurrencyChange, onSetMax, onFeeLevelChange, updateFeeLevels, onGasPriceChange, onGasLimitChange, onNonceChange, onDataChange, onSend, }