From 283af403569b7c0af66989a2afcc3bfe32542c9c Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Fri, 25 May 2018 11:26:36 +0200 Subject: [PATCH] Refactoring: SelectedAccountReducer synchronized with other reducers thru WalletService + implementation new data model in components --- src/js/actions/SelectedAccountActions.js | 124 ++- src/js/actions/SendFormActions.js | 838 +++++++++--------- src/js/actions/WalletActions.js | 47 +- src/js/actions/constants/account.js | 4 +- src/js/actions/constants/send.js | 1 + src/js/components/modal/ConfirmAddress.js | 22 +- src/js/components/modal/ConfirmSignTx.js | 6 +- .../wallet/account/SelectedAccount.js | 210 ++--- .../wallet/account/receive/Receive.js | 32 +- .../wallet/account/receive/index.js | 13 +- .../wallet/account/send/AdvancedForm.js | 19 +- .../wallet/account/send/SendForm.js | 64 +- .../components/wallet/account/send/index.js | 12 +- .../wallet/account/summary/Summary.js | 68 +- .../wallet/account/summary/index.js | 15 +- src/js/reducers/DevicesReducer.js | 10 - src/js/reducers/ReceiveReducer.js | 3 +- src/js/reducers/SelectedAccountReducer.js | 43 +- src/js/reducers/SendFormReducer.js | 65 +- src/js/reducers/SummaryReducer.js | 7 +- src/js/reducers/utils/index.js | 22 +- src/js/services/WalletService.js | 56 +- 22 files changed, 792 insertions(+), 889 deletions(-) diff --git a/src/js/actions/SelectedAccountActions.js b/src/js/actions/SelectedAccountActions.js index 08597181..b45d891a 100644 --- a/src/js/actions/SelectedAccountActions.js +++ b/src/js/actions/SelectedAccountActions.js @@ -1,66 +1,98 @@ /* @flow */ 'use strict'; +import { LOCATION_CHANGE } from 'react-router-redux'; import * as ACCOUNT from './constants/account'; +import * as SEND from './constants/send'; + +import * as SendFormActions from './SendFormActions'; +import * as SessionStorageActions from './SessionStorageActions'; +import * as stateUtils from '../reducers/utils'; import { initialState } from '../reducers/SelectedAccountReducer'; -import type { AsyncAction, ThunkAction, Action, GetState, Dispatch, TrezorDevice } from '~/flowtype'; -import type { State } from '../reducers/SelectedAccountReducer'; -import type { Coin } from '../reducers/LocalStorageReducer'; +import type { + Coin, + TrezorDevice, + AsyncAction, + ThunkAction, + Action, + GetState, + Dispatch, + State, +} from '~/flowtype'; + export type SelectedAccountAction = { - type: typeof ACCOUNT.INIT, - state: State -} | { type: typeof ACCOUNT.DISPOSE, +} | { + type: typeof ACCOUNT.UPDATE_SELECTED_ACCOUNT, + payload: $ElementType }; -export const init = (): ThunkAction => { - return (dispatch: Dispatch, getState: GetState): void => { +export const updateSelectedValues = (prevState: State, action: Action): AsyncAction => { + return async (dispatch: Dispatch, getState: GetState): Promise => { - const { location } = getState().router; - const urlParams = location.state; + const locationChange: boolean = action.type === LOCATION_CHANGE; + const state: State = getState(); + const location = state.router.location; - const selected: ?TrezorDevice = getState().wallet.selectedDevice;; - if (!selected) return; - if (!selected.state || !selected.features) return; + let needUpdate: boolean = false; - const { config } = getState().localStorage; - const coin: ?Coin = config.coins.find(c => c.network === urlParams.network); - if (!coin) return; + // reset form to default + if (action.type === SEND.TX_COMPLETE) { + dispatch( SendFormActions.init() ); + // linear action + SessionStorageActions.clear(location.pathname); + } - const state: State = { - index: parseInt(urlParams.account), - deviceState: selected.state || '0', - deviceId: selected.features ? selected.features.device_id : '0', - deviceInstance: selected.instance, - network: urlParams.network, - coin, - location: location.pathname, - }; + // handle devices state change (from trezor-connect events or location change) + if (locationChange + || prevState.accounts !== state.accounts + || prevState.discovery !== state.discovery + || prevState.tokens !== state.tokens + || prevState.web3 !== state.web3) { + + const account = stateUtils.getSelectedAccount(state); + const network = stateUtils.getSelectedNetwork(state); + const discovery = stateUtils.getDiscoveryProcess(state); + const tokens = stateUtils.getTokens(state, account); + const web3 = stateUtils.getWeb3(state); - dispatch({ - type: ACCOUNT.INIT, - state: state - }); + const payload: $ElementType = { + // location: location.pathname, + account, + network, + discovery, + tokens, + web3 + } - } -} + let needUpdate: boolean = false; + Object.keys(payload).forEach((key) => { + if (payload[key] !== state.selectedAccount[key]) { + needUpdate = true; + } + }) -export const update = (initAccountAction: () => ThunkAction): ThunkAction => { - return (dispatch: Dispatch, getState: GetState): void => { - const { - selectedAccount, - router - } = getState(); + if (needUpdate) { + dispatch({ + type: ACCOUNT.UPDATE_SELECTED_ACCOUNT, + payload, + }) + } - const shouldReload: boolean = (!selectedAccount || router.location.pathname !== selectedAccount.location); - if (shouldReload) { - dispatch( dispose() ); - dispatch( init() ); - if (selectedAccount !== null) - initAccountAction(); + if (locationChange) { + + if (prevState.router.location && prevState.router.location.state.send) { + SessionStorageActions.save(prevState.router.location.pathname, state.sendForm); + } + + dispatch( dispose() ); + if (location.state.send) { + dispatch( SendFormActions.init( SessionStorageActions.load(location.pathname) ) ); + } + } } } } @@ -69,10 +101,4 @@ export const dispose = (): Action => { return { type: ACCOUNT.DISPOSE } -} - -export default { - init, - update, - dispose } \ No newline at end of file diff --git a/src/js/actions/SendFormActions.js b/src/js/actions/SendFormActions.js index 17e17015..acc625f1 100644 --- a/src/js/actions/SendFormActions.js +++ b/src/js/actions/SendFormActions.js @@ -17,7 +17,7 @@ import BigNumber from 'bignumber.js'; import { initialState } from '../reducers/SendFormReducer'; import { findAccount } from '../reducers/AccountsReducer'; import { findToken } from '../reducers/TokensReducer'; -import { findDevice } from '../reducers/DevicesReducer'; +import { findDevice } from '../reducers/utils'; import type { Dispatch, @@ -34,6 +34,7 @@ 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'; +import type { Props } from '../components/wallet/account/send'; export type SendTxAction = { type: typeof SEND.TX_COMPLETE, @@ -56,6 +57,9 @@ export type SendFormAction = SendTxAction | { errors: {[k: string]: string}, warnings: {[k: string]: string}, infos: {[k: string]: string} +} | { + type: typeof SEND.ADDRESS_VALIDATION, + state: State } | { type: typeof SEND.ADDRESS_CHANGE, state: State @@ -111,21 +115,28 @@ export type SendFormAction = SendTxAction | { //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 => { +export const calculateFee = (gasPrice: string, gasLimit: string): string => { try { - return new BigNumber(amount).plus( calculateFee(gasPrice, gasLimit) ).toString(); + return EthereumjsUnits.convert( new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether'); } catch (error) { return '0'; } } -const calculateMaxAmount = (balance: string, gasPrice: string, gasLimit: string): string => { +export const calculateTotal = (amount: string, gasPrice: string, gasLimit: string): string => { try { - const fee = EthereumjsUnits.convert( new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether'); + // return new BigNumber(amount).plus( calculateFee(gasPrice, gasLimit) ).toString(); + const fee = calculateFee(gasPrice, gasLimit); + return new BigNumber(amount).plus( calculateFee(gasPrice, gasLimit) ).toFixed(16); + } catch (error) { + return '0'; + } +} + +export const calculateMaxAmount = (balance: string, gasPrice: string, gasLimit: string): string => { + try { + // TODO - minus pendings + const fee = calculateFee(gasPrice, gasLimit); const b = new BigNumber(balance); const max = b.minus(fee); if (max.lessThan(0)) return '0'; @@ -136,25 +147,56 @@ const calculateMaxAmount = (balance: string, gasPrice: string, gasLimit: string) } -const setAmountAndTotal = (state: State): State => { +export const calculate = (prevProps: Props, props: Props) => { - // if (state.setMax) { - // const account: ?Account = findAccount(getState().accounts, accountState.index, accountState.deviceState, accountState.network); - // if (!account) return; + const { + account, + } = props.selectedAccount; + 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); + console.warn("CALCULATE!", props) - return state; + const prevState = prevProps.sendForm; + const state = props.sendForm; + const isToken: boolean = state.currency !== state.networkSymbol; + + // account balance + // token balance + // gasLimit, gasPrice changed + + // const shouldRecalculateAmount = + // (prevProps.selectedAccount.account !== account) + // || (prevProps.) + + + if (state.setMax) { + + const pendingAmount = props.pending.reduce((value, p) => { + //if (p.tx.amount) + console.warn("PENDING AMOUNT!", p, value); + }, 0); + + if (isToken) { + const token: ?Token = findToken(props.tokens, account.address, state.currency, account.deviceState); + if (token) { + state.amount = token.balance; + } + } else { + state.amount = calculateMaxAmount(account.balance, state.gasPrice, state.gasLimit); + } + } + + // amount changed + // fee changed + state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); + + if (state.selectedFeeLevel.value === 'Custom') { + state.selectedFeeLevel.label = `${ calculateFee(state.gasPrice, state.gasLimit) } ${ state.networkSymbol }`; + state.selectedFeeLevel.gasPrice = state.gasPrice; + } } + 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); @@ -194,45 +236,44 @@ export const getFeeLevels = (symbol: string, gasPrice: BigNumber | string, gasLi // initialize component -export const init = (): ThunkAction => { +export const init = (stateFromStorage: ?State): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { - const accountState: ?AccountState = getState().selectedAccount; - if (!accountState) return; + const { + account, + network, + web3 + } = getState().selectedAccount; - const { location } = getState().router; - const urlParams: RouterLocationState = location.state; + if (!account || !network || !web3) return; - const selected: ?TrezorDevice = getState().wallet.selectedDevice; - 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; + if (stateFromStorage) { + dispatch({ + type: SEND.INIT, + state: stateFromStorage + }); + 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); + const gasPrice: BigNumber = new BigNumber( EthereumjsUnits.convert(web3.gasPrice, 'wei', 'gwei') ) || new BigNumber(network.defaultGasPrice); + const gasLimit: string = network.defaultGasLimit.toString(); + const feeLevels: Array = getFeeLevels(network.symbol, gasPrice, gasLimit); // TODO: get nonce + // TODO: LOAD DATA FROM SESSION STORAGE const state: State = { ...initialState, - network: coin.network, - coinSymbol: coin.symbol, - selectedCurrency: coin.symbol, + networkName: network.network, + networkSymbol: network.symbol, + currency: network.symbol, feeLevels, selectedFeeLevel: feeLevels.find(f => f.value === 'Normal'), recommendedGasPrice: gasPrice.toString(), gasLimit, gasPrice: gasPrice.toString(), - nonce: account.nonce.toString(), // TODO!!! }; dispatch({ @@ -242,269 +283,272 @@ export const init = (): ThunkAction => { } } -export const dispose = (): Action => { - return { - type: SEND.DISPOSE - } -} - export const toggleAdvanced = (address: string): Action => { return { type: SEND.TOGGLE_ADVANCED } } -export const validation = (): ThunkAction => { + +const addressValidation = (): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { - - const accountState: ?AccountState = getState().selectedAccount; - if (!accountState) return; + const { + account, + network, + tokens, + } = getState().selectedAccount; + if (!account || !network) return; const state: State = getState().sendForm; + const infos = { ...state.infos }; + const warnings = { ...state.warnings }; - 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) { + if (state.untouched || !state.touched.address) return; - 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().devices, currentNetworkAccount.deviceID, currentNetworkAccount.deviceState); - if (!device) return; + const savedAccounts = getState().accounts.filter(a => a.address.toLowerCase() === state.address.toLowerCase()); + if (savedAccounts.length > 0) { + // check if found account belongs to this network + // corner-case: when same derivation path is used on different networks + const currentNetworkAccount = savedAccounts.find(a => a.network === network.network); + if (currentNetworkAccount) { + const device: ?TrezorDevice = findDevice(getState().devices, currentNetworkAccount.deviceID, currentNetworkAccount.deviceState); + if (device) { infos.address = `${ device.instanceLabel } Account #${ (currentNetworkAccount.index + 1) }`; - } else { - const device: ?TrezorDevice = findDevice(getState().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'; - } + const otherNetworkAccount = savedAccounts[0]; + const device: ?TrezorDevice = findDevice(getState().devices, otherNetworkAccount.deviceID, otherNetworkAccount.deviceState); + const coins = getState().localStorage.config.coins; + const otherNetwork: ?Coin = coins.find(c => c.network === otherNetworkAccount.network) + if (device && otherNetwork) { + warnings.address = `Looks like it's ${ device.instanceLabel } Account #${ (otherNetworkAccount.index + 1) } address of ${ otherNetwork.name } network`; } } + } else { + delete warnings.address; + delete infos.address; } - - // 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 - }); + type: SEND.ADDRESS_VALIDATION, + state: { + ...state, + infos, + warnings + } + }) } } +export const validation = (props: Props): void => { + + const { + account, + network, + tokens, + } = props.selectedAccount; + if (!account || !network) return; + + + const state: State = props.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) { + if (state.address.length < 1) { + errors.address = 'Address is not set'; + } else if (!EthereumjsUtil.isValidAddress(state.address)) { + errors.address = 'Address is not valid'; + } else { + // address warning or info are set in addressValidation ThunkAction + // do not override this + if (state.warnings.address) { + warnings.address = state.warnings.address; + } else if (state.infos.address) { + infos.address = state.infos.address; + } + } + } + + // 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 { + + let decimalRegExp: RegExp; + + if (state.currency !== state.networkSymbol) { + const token = findToken(tokens, account.address, state.currency, 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.networkSymbol } 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.currency !== state.networkSymbol ? network.defaultGasLimitTokens : network.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 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? + + state.errors = errors; + state.warnings = warnings; + state.infos = infos; + +} + + + + export const onAddressChange = (address: string): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { - - const currentState: State = getState().sendForm; - const touched = { ...currentState.touched }; + const state: State = getState().sendForm; + const touched = { ...state.touched }; touched.address = true; - const state: State = { - ...currentState, - untouched: false, - touched, - address - }; - dispatch({ type: SEND.ADDRESS_CHANGE, - state + state: { + ...state, + untouched: false, + touched, + address + } }); - dispatch( validation() ); + + dispatch( addressValidation() ); } } export const onAmountChange = (amount: string): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { - - const accountState: ?AccountState = getState().selectedAccount; - const currentState: State = getState().sendForm; - const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; - const touched = { ...currentState.touched }; + const state = getState().sendForm; + const touched = { ...state.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 + state: { + ...state, + untouched: false, + touched, + setMax: false, + amount, + } }); - dispatch( validation() ); } } export const onCurrencyChange = (currency: { value: string, label: string }): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { - const accountState: ?AccountState = getState().selectedAccount; - if (!accountState) return; + const { + account, + network + } = getState().selectedAccount; + if (!account || !network) return; + const currentState: State = getState().sendForm; - const isToken: boolean = currency.value !== currentState.coinSymbol; + const isToken: boolean = currency.value !== currentState.networkSymbol; + const gasLimit: string = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString(); - 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 feeLevels: Array = getFeeLevels(network.symbol, currentState.recommendedGasPrice, 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, + currency: currency.value, + // amount, + // total, feeLevels, selectedFeeLevel, gasLimit, @@ -514,66 +558,36 @@ export const onCurrencyChange = (currency: { value: string, label: string }): Th type: SEND.CURRENCY_CHANGE, state }); - dispatch( validation() ); } } - - export const onSetMax = (): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { - const accountState: ?AccountState = getState().selectedAccount; - if (!accountState) return; - const currentState: State = getState().sendForm; - const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; - const touched = { ...currentState.touched }; + const state = getState().sendForm; + const touched = { ...state.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 + state: { + ...state, + untouched: false, + touched, + setMax: !state.setMax, + } }); - dispatch( validation() ); } } export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { - const accountState: ?AccountState = getState().selectedAccount; - if (!accountState) return; - const currentState: State = getState().sendForm; - const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; + const { + network + } = getState().selectedAccount; + if (!network) return; - const coin: Coin = accountState.coin; + const currentState: State = getState().sendForm; + const isToken: boolean = currentState.currency !== currentState.networkSymbol; const state: State = { ...currentState, @@ -584,38 +598,23 @@ export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => { if (feeLevel.value === 'Custom') { state.advanced = true; feeLevel.gasPrice = state.gasPrice; - feeLevel.label = `${ calculateFee(state.gasPrice, state.gasLimit) } ${ state.coinSymbol }`; + feeLevel.label = `${ calculateFee(state.gasPrice, state.gasLimit) } ${ state.networkSymbol }`; } else { const customLevel: ?FeeLevel = state.feeLevels.find(f => f.value === 'Custom'); if (customLevel) customLevel.label = ''; state.gasPrice = feeLevel.gasPrice; if (isToken) { - state.gasLimit = coin.defaultGasLimitTokens.toString() + state.gasLimit = network.defaultGasLimitTokens.toString() } else { - state.gasLimit = state.data.length > 0 ? state.gasLimit : coin.defaultGasLimit.toString(); + state.gasLimit = state.data.length > 0 ? state.gasLimit : network.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() ); } } @@ -624,12 +623,25 @@ export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => { export const updateFeeLevels = (): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { - const accountState: ?AccountState = getState().selectedAccount; - if (!accountState) return; - const currentState: State = getState().sendForm; - const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; + const { + account, + network + } = getState().selectedAccount; + if (!account || !network) return; - const feeLevels: Array = getFeeLevels(currentState.coinSymbol, currentState.recommendedGasPrice, currentState.gasLimit, currentState.selectedFeeLevel); + const currentState: State = getState().sendForm; + const isToken: boolean = currentState.currency !== currentState.networkSymbol; + let gasLimit: string = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString(); + + // override custom settings + if (currentState.selectedFeeLevel.value === 'Custom') { + // update only gasPrice + currentState.selectedFeeLevel.gasPrice = currentState.recommendedGasPrice; + // leave gas limit as it was + gasLimit = currentState.gasLimit; + } + + const feeLevels: Array = getFeeLevels(network.symbol, currentState.recommendedGasPrice, gasLimit, currentState.selectedFeeLevel); const selectedFeeLevel: ?FeeLevel = feeLevels.find(f => f.value === currentState.selectedFeeLevel.value); if (!selectedFeeLevel) return; @@ -641,34 +653,18 @@ export const updateFeeLevels = (): ThunkAction => { 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().selectedAccount; - if (!accountState) return; + const currentState: State = getState().sendForm; - const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; + const isToken: boolean = currentState.currency !== currentState.networkSymbol; const touched = { ...currentState.touched }; touched.gasPrice = true; @@ -680,99 +676,46 @@ export const onGasPriceChange = (gasPrice: string): ThunkAction => { gasPrice: gasPrice, }; - if (gasPrice.match(numberRegExp) && state.gasLimit.match(numberRegExp)) { + if (currentState.selectedFeeLevel.value !== 'Custom') { 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, updateFeeLevels: boolean = false): ThunkAction => { return (dispatch: Dispatch, getState: GetState): void => { - const accountState: ?AccountState = getState().selectedAccount; - if (!accountState) return; + const currentState: State = getState().sendForm; - const isToken: boolean = currentState.selectedCurrency !== currentState.coinSymbol; + const isToken: boolean = currentState.currency !== currentState.networkSymbol; const touched = { ...currentState.touched }; touched.gasLimit = true; const state: State = { ...currentState, + calculatingGasLimit: false, untouched: false, touched, gasLimit, }; - if (gasLimit.match(numberRegExp) && state.gasPrice.match(numberRegExp)) { - - if (updateFeeLevels) { - const feeLevels: Array = getFeeLevels(state.coinSymbol, state.gasPrice, gasLimit, state.selectedFeeLevel); - const selectedFeeLevel: ?FeeLevel = feeLevels.find(f => f.value === state.selectedFeeLevel.value); - if (!selectedFeeLevel) return; - state.selectedFeeLevel = selectedFeeLevel; - state.feeLevels = feeLevels; - - } else { - 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); - } - } + if (currentState.selectedFeeLevel.value !== 'Custom') { + const customLevel = currentState.feeLevels.find(f => f.value === 'Custom'); + if (!customLevel) return; + state.selectedFeeLevel = customLevel; } - state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); - dispatch({ type: SEND.GAS_LIMIT_CHANGE, state }); - dispatch( validation() ); } } @@ -793,7 +736,6 @@ export const onNonceChange = (nonce: string): AsyncAction => { type: SEND.NONCE_CHANGE, state }); - dispatch( validation() ); } } @@ -805,16 +747,22 @@ export const onDataChange = (data: string): AsyncAction => { const state: State = { ...currentState, + calculatingGasLimit: true, untouched: false, touched, data, }; + if (currentState.selectedFeeLevel.value !== 'Custom') { + const customLevel = currentState.feeLevels.find(f => f.value === 'Custom'); + if (!customLevel) return; + state.selectedFeeLevel = customLevel; + } + dispatch({ type: SEND.DATA_CHANGE, state }); - dispatch( validation() ); dispatch( estimateGasPrice() ); } @@ -824,58 +772,71 @@ export const onDataChange = (data: string): AsyncAction => { const estimateGasPrice = (): AsyncAction => { return async (dispatch: Dispatch, getState: GetState): Promise => { - const accountState: ?AccountState = getState().selectedAccount; - if (!accountState) return; - const web3instance: ?Web3Instance = getState().web3.filter(w3 => w3.network === accountState.network)[0]; - if (!web3instance) return; - const web3 = web3instance.web3; - const currentState: State = getState().sendForm; - const requestedData = currentState.data; + const { + web3, + network + } = getState().selectedAccount; + if (!web3 || !network) return; - if (currentState.errors.data) { + const w3 = web3.web3; + + const state: State = getState().sendForm; + const requestedData = state.data; + + const re = /^[0-9A-Fa-f]+$/g; + if (!re.test(requestedData)) { + // to stop calculating + dispatch( onGasLimitChange(state.gasLimit) ); return; } - const data: string = '0x' + (currentState.data.length % 2 === 0 ? currentState.data : '0' + currentState.data); - const gasLimit = await estimateGas(web3instance.web3, { + if (state.data.length < 1) { + // set default + dispatch( onGasLimitChange(network.defaultGasLimit.toString()) ); + return; + } + + // TODO: allow data starting with 0x ... + const data: string = '0x' + (state.data.length % 2 === 0 ? state.data : '0' + state.data); + const gasLimit = await estimateGas(w3, { to: '0x0000000000000000000000000000000000000000', data, - value: web3.toHex(web3.toWei(currentState.amount, 'ether')), - gasPrice: web3.toHex( EthereumjsUnits.convert(currentState.gasPrice, 'gwei', 'wei') ), + value: w3.toHex(w3.toWei(state.amount, 'ether')), + gasPrice: w3.toHex( EthereumjsUnits.convert(state.gasPrice, 'gwei', 'wei') ), }); if (getState().sendForm.data === requestedData) { - dispatch( onGasLimitChange(gasLimit.toString(), true) ); + dispatch( onGasLimitChange(gasLimit.toString()) ); } } } export const onSend = (): AsyncAction => { - //return onSendERC20(); return async (dispatch: Dispatch, getState: GetState): Promise => { - const accountState: ?AccountState = getState().selectedAccount; - if (!accountState) return; + const { + account, + network, + web3 + } = getState().selectedAccount; + if (!account || !web3 || !network) 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 isToken: boolean = currentState.currency !== currentState.networkSymbol; + const w3 = web3.web3; const address_n = account.addressPath; let data: string = '0x' + currentState.data; - let txAmount: string = web3.toHex(web3.toWei(currentState.amount, 'ether')); + let txAmount: string = w3.toHex(w3.toWei(currentState.amount, 'ether')); let txAddress: string = currentState.address; if (isToken) { - const token: ?Token = findToken(getState().tokens, account.address, currentState.selectedCurrency, accountState.deviceState); + const token: ?Token = findToken(getState().tokens, account.address, currentState.currency, account.deviceState); if (!token) return; - const contract = web3instance.erc20.at(token.address); + const contract = web3.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, { @@ -893,11 +854,10 @@ export const onSend = (): AsyncAction => { 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') ), + chainId: web3.chainId, + nonce: w3.toHex(account.nonce), + gasLimit: w3.toHex(currentState.gasLimit), + gasPrice: w3.toHex( EthereumjsUnits.convert(currentState.gasPrice, 'gwei', 'wei') ), r: '', s: '', v: '' @@ -940,20 +900,19 @@ export const onSend = (): AsyncAction => { txData.r = '0x' + signedTransaction.payload.r; txData.s = '0x' + signedTransaction.payload.s; - txData.v = web3.toHex(signedTransaction.payload.v); + txData.v = w3.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; + const txid: string = await pushTx(w3, serializedTx); dispatch({ type: SEND.TX_COMPLETE, account: account, - selectedCurrency: currentState.selectedCurrency, + selectedCurrency: currentState.currency, amount: currentState.amount, txid, txData, @@ -964,7 +923,7 @@ export const onSend = (): AsyncAction => { payload: { type: 'success', title: 'Transaction success', - message: `See transaction detail`, + message: `See transaction detail`, cancelable: true, actions: [] } @@ -987,9 +946,6 @@ export const onSend = (): AsyncAction => { } export default { - init, - dispose, - toggleAdvanced, onAddressChange, onAmountChange, @@ -1002,4 +958,4 @@ export default { onNonceChange, onDataChange, onSend, -} +} \ No newline at end of file diff --git a/src/js/actions/WalletActions.js b/src/js/actions/WalletActions.js index 154fbd07..b670e52e 100644 --- a/src/js/actions/WalletActions.js +++ b/src/js/actions/WalletActions.js @@ -1,9 +1,27 @@ /* @flow */ 'use strict'; +import { LOCATION_CHANGE } from 'react-router-redux'; import * as WALLET from './constants/wallet'; +import * as stateUtils from '../reducers/utils'; -import type { TrezorDevice, RouterLocationState, ThunkAction, Dispatch, GetState } from '~/flowtype'; +import type + { + Account, + Coin, + Discovery, + Token, + Web3Instance, + + TrezorDevice, + RouterLocationState, + ThunkAction, + AsyncAction, + Action, + Dispatch, + GetState, + State +} from '~/flowtype'; export type WalletAction = { type: typeof WALLET.SET_INITIAL_URL, @@ -51,3 +69,30 @@ export const toggleDeviceDropdown = (opened: boolean): WalletAction => { opened } } + +export const updateSelectedValues = (prevState: State, action: Action): AsyncAction => { + return async (dispatch: Dispatch, getState: GetState): Promise => { + + const locationChange: boolean = action.type === LOCATION_CHANGE; + const state: State = getState(); + + // handle devices state change (from trezor-connect events or location change) + if (locationChange || prevState.devices !== state.devices) { + const device = stateUtils.getSelectedDevice(state); + if (state.wallet.selectedDevice !== device) { + if (device && stateUtils.isSelectedDevice(state.wallet.selectedDevice, device)) { + dispatch({ + type: WALLET.UPDATE_SELECTED_DEVICE, + device + }); + } else { + dispatch({ + type: WALLET.SET_SELECTED_DEVICE, + device + }); + } + } + } + + } +} \ No newline at end of file diff --git a/src/js/actions/constants/account.js b/src/js/actions/constants/account.js index 284d4504..11c72a4d 100644 --- a/src/js/actions/constants/account.js +++ b/src/js/actions/constants/account.js @@ -8,4 +8,6 @@ export const CREATE: 'account__create' = 'account__create'; export const REMOVE: 'account__remove' = 'account__remove'; export const SET_BALANCE: 'account__set_balance' = 'account__set_balance'; export const SET_NONCE: 'account__set_nonce' = 'account__set_nonce'; -export const FROM_STORAGE: 'account__from_storage' = 'account__from_storage'; \ No newline at end of file +export const FROM_STORAGE: 'account__from_storage' = 'account__from_storage'; + +export const UPDATE_SELECTED_ACCOUNT: 'account__update_selected_account' = 'account__update_selected_account'; \ No newline at end of file diff --git a/src/js/actions/constants/send.js b/src/js/actions/constants/send.js index 6fdcef85..35c70969 100644 --- a/src/js/actions/constants/send.js +++ b/src/js/actions/constants/send.js @@ -4,6 +4,7 @@ export const INIT: 'send__init' = 'send__init'; export const DISPOSE: 'send__dispose' = 'send__dispose'; export const VALIDATION: 'send__validation' = 'send__validation'; +export const ADDRESS_VALIDATION: 'send__address_validation' = 'send__address_validation'; 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'; diff --git a/src/js/components/modal/ConfirmAddress.js b/src/js/components/modal/ConfirmAddress.js index ff83a247..34518bb5 100644 --- a/src/js/components/modal/ConfirmAddress.js +++ b/src/js/components/modal/ConfirmAddress.js @@ -8,10 +8,11 @@ import type { Props } from './index'; const ConfirmAddress = (props: Props) => { - const { accounts, selectedAccount } = props; - if (!selectedAccount) return null; - const account = findAccount(accounts, selectedAccount.index, selectedAccount.deviceState, selectedAccount.network); - if (!account) return null; + const { + account, + network + } = props.selectedAccount; + if (!account || !network) return null; return (
@@ -21,7 +22,7 @@ const ConfirmAddress = (props: Props) => {

{ account.address }

- +
); @@ -50,14 +51,9 @@ export class ConfirmUnverifiedAddress extends Component { verifyAddress() { if (!this.props.modal.opened) return; - - const { - accounts, - selectedAccount - } = this.props; - - if(!selectedAccount) return null; - const account = findAccount(accounts, selectedAccount.index, selectedAccount.deviceState, selectedAccount.network); + const { + account + } = this.props.selectedAccount; if (!account) return null; this.props.modalActions.onCancel(); diff --git a/src/js/components/modal/ConfirmSignTx.js b/src/js/components/modal/ConfirmSignTx.js index f6fd0e59..2550f810 100644 --- a/src/js/components/modal/ConfirmSignTx.js +++ b/src/js/components/modal/ConfirmSignTx.js @@ -12,9 +12,7 @@ const Confirmation = (props: Props) => { const { amount, address, - network, - coinSymbol, - selectedCurrency, + currency, total, selectedFeeLevel } = props.sendForm; @@ -27,7 +25,7 @@ const Confirmation = (props: Props) => {
-

{ `${amount} ${ selectedCurrency }` }

+

{ `${amount} ${ currency }` }

{ address }

diff --git a/src/js/components/wallet/account/SelectedAccount.js b/src/js/components/wallet/account/SelectedAccount.js index 747aa27e..6b65226c 100644 --- a/src/js/components/wallet/account/SelectedAccount.js +++ b/src/js/components/wallet/account/SelectedAccount.js @@ -1,171 +1,117 @@ /* @flow */ 'use strict'; -import React, { Component } from 'react'; +import * as React from 'react'; import { Notification } from '~/js/components/common/Notification'; -import { findDevice } from '~/js/reducers/DevicesReducer'; - -// import * as SelectedAccountActions from '~/js/actions/SelectedAccountActions'; -import { default as SelectedAccountActions } from '~/js/actions/SelectedAccountActions'; import type { State, TrezorDevice, Action, ThunkAction } from '~/flowtype'; import type { Account } from '~/js/reducers/AccountsReducer'; import type { Discovery } from '~/js/reducers/DiscoveryReducer'; export type StateProps = { + className: string; selectedAccount: $ElementType, - devices: $ElementType, - discovery: $ElementType, - accounts: $ElementType, + wallet: $ElementType, + children?: React.Node } export type DispatchProps = { - selectedAccountActions: typeof SelectedAccountActions, - initAccount: () => ThunkAction, - disposeAccount: () => Action, + } export type Props = StateProps & DispatchProps; -export type ComponentState = { - device: ?TrezorDevice; - account: ?Account; - discovery: ?Discovery; - deviceStatusNotification: ?React$Element; -} - -export default class SelectedAccount

extends Component { - - state: ComponentState = { - device: null, - account: null, - discovery: null, - deviceStatusNotification: null - }; - - componentDidMount() { - this.props.selectedAccountActions.init(); - this.props.initAccount(); +const SelectedAccount = (props: Props) => { + const device = props.wallet.selectedDevice; + if (!device || !device.state) { + return (

); } - componentWillReceiveProps(props: Props & P) { - - this.props.selectedAccountActions.update( this.props.initAccount ); + const accountState = props.selectedAccount; - const accountState = props.selectedAccount; - if (!accountState) return; + const { + account, + discovery + } = accountState; - const device = findDevice(props.devices, accountState.deviceId, accountState.deviceState, accountState.deviceInstance); - if (!device) return; - const discovery = props.discovery.find(d => d.deviceState === device.state && d.network === accountState.network); - // if (!discovery) return; - const account = props.accounts.find(a => a.deviceState === accountState.deviceState && a.index === accountState.index && a.network === accountState.network); - - let deviceStatusNotification: ?React$Element = null; - if (account) { - if (!device.connected) { - deviceStatusNotification = ; - } else if (!device.available) { - deviceStatusNotification = ; - } - } - - if (discovery && !discovery.completed && !deviceStatusNotification) { - deviceStatusNotification = ; - } - - this.setState({ - device, - discovery, - account, - deviceStatusNotification - }) - } - - componentWillUnmount() { - this.props.selectedAccountActions.dispose(); - this.props.disposeAccount(); - } - - render(): ?React$Element { - - const props = this.props; - const accountState = props.selectedAccount; - - if (!accountState) { - return (
); - } - - const { - device, - account, - discovery - } = this.state; - - if (!device) { - return (
); - } - - // account not found. checking why... - if (!account) { - if (!discovery || discovery.waitingForDevice) { - - if (device.connected) { - // case 1: device is connected but discovery not started yet (probably waiting for auth) - if (device.available) { - return ( -
- -
- ); - } else { - // case 2: device is unavailable (created with different passphrase settings) account cannot be accessed - return ( -
- -
- ); - } + // account not found (yet). checking why... + if (!account) { + if (!discovery || discovery.waitingForDevice) { + if (device.connected) { + // case 1: device is connected but discovery not started yet (probably waiting for auth) + if (device.available) { + return ( +
+ +
+ ); } else { - // case 3: device is disconnected + // case 2: device is unavailable (created with different passphrase settings) account cannot be accessed return (
+ title={ `Device ${ device.instanceLabel } is unavailable` } + message="Change passphrase settings to use this device" + />
); } - } else if (discovery.waitingForBackend) { - // case 4: backend is not working - return ( -
- -
- ); - } else if (discovery.completed) { - // case 5: account not found and discovery is completed - return ( -
- -
- ); } else { - // case 6: discovery is not completed yet + // case 3: device is disconnected return (
- +
); } + } else if (discovery.waitingForBackend) { + // case 4: backend is not working + return ( +
+ +
+ ); + } else if (discovery.completed) { + // case 5: account not found and discovery is completed + return ( +
+ +
+ ); + } else { + // case 6: discovery is not completed yet + return ( +
+ +
+ ); + } + } else { + + let notification: ?React$Element = null; + if (!device.connected) { + notification = ; + } else if (!device.available) { + notification = ; } - return null; + if (discovery && !discovery.completed && !notification) { + notification = ; + } + + return ( +
+ { notification } + { props.children } +
+ ) } -} \ No newline at end of file + +} + +export default SelectedAccount; \ No newline at end of file diff --git a/src/js/components/wallet/account/receive/Receive.js b/src/js/components/wallet/account/receive/Receive.js index 642e8f75..8e8d103a 100644 --- a/src/js/components/wallet/account/receive/Receive.js +++ b/src/js/components/wallet/account/receive/Receive.js @@ -9,31 +9,26 @@ import { QRCode } from 'react-qr-svg'; import SelectedAccount from '../SelectedAccount'; import { Notification } from '~/js/components/common/Notification'; -import type { ComponentState } from '../SelectedAccount'; import type { Props } from './index'; -export default class Receive extends SelectedAccount { - render() { - return super.render() || _render(this.props, this.state); - } -} -const _render = (props: Props, state: ComponentState): React$Element => { +const Receive = (props: Props) => { + + const device = props.wallet.selectedDevice; const { - device, account, + network, discovery, - deviceStatusNotification - } = state; + } = props.selectedAccount; + + if (!device || !account || !discovery) return null; const { addressVerified, addressUnverified, } = props.receive; - if (!device || !account || !discovery) return
; - let qrCode = null; let address = `${account.address.substring(0, 20)}...`; let className = 'address hidden'; @@ -83,8 +78,7 @@ const _render = (props: Props, state: ComponentState): React$Element => } return ( -
- { deviceStatusNotification } +

Receive Ethereum or tokens

@@ -95,6 +89,14 @@ const _render = (props: Props, state: ComponentState): React$Element => { button }
{ qrCode } -
+
+ ); +} + +export default (props: Props) => { + return ( + + + ); } \ No newline at end of file diff --git a/src/js/components/wallet/account/receive/index.js b/src/js/components/wallet/account/receive/index.js index 509ecf2b..18ebe06b 100644 --- a/src/js/components/wallet/account/receive/index.js +++ b/src/js/components/wallet/account/receive/index.js @@ -6,7 +6,6 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { default as ReceiveActions } from '~/js/actions/ReceiveActions'; -import { default as SelectedAccountActions } from '~/js/actions/SelectedAccountActions'; import * as TokenActions from '~/js/actions/TokenActions'; import Receive from './Receive'; @@ -28,14 +27,14 @@ type DispatchProps = BaseDispatchProps & { showAddress: typeof ReceiveActions.showAddress }; -export type Props = StateProps & DispatchProps; +export type Props = StateProps & BaseStateProps & DispatchProps & BaseDispatchProps; const mapStateToProps: MapStateToProps = (state: State, own: OwnProps): StateProps => { return { + className: "receive", selectedAccount: state.selectedAccount, - devices: state.devices, - accounts: state.accounts, - discovery: state.discovery, + wallet: state.wallet, + receive: state.receive, modal: state.modal, }; @@ -43,10 +42,6 @@ const mapStateToProps: MapStateToProps = (state: St const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => { return { - selectedAccountActions: bindActionCreators(SelectedAccountActions, dispatch), - - initAccount: bindActionCreators(ReceiveActions.init, dispatch), - disposeAccount: bindActionCreators(ReceiveActions.dispose, dispatch), showAddress: bindActionCreators(ReceiveActions.showAddress, dispatch), }; } diff --git a/src/js/components/wallet/account/send/AdvancedForm.js b/src/js/components/wallet/account/send/AdvancedForm.js index 33e99bf7..c46e4a2a 100644 --- a/src/js/components/wallet/account/send/AdvancedForm.js +++ b/src/js/components/wallet/account/send/AdvancedForm.js @@ -14,15 +14,17 @@ type Props = { const AdvancedForm = (props: Props) => { - const selectedAccount = props.selectedAccount; - if (!selectedAccount) return null; - - const { network } = selectedAccount; const { - coinSymbol, - selectedCurrency, + account, + network + } = props.selectedAccount; + + const { + networkSymbol, + currency, gasPrice, gasLimit, + calculatingGasLimit, nonce, data, errors, @@ -120,10 +122,11 @@ const AdvancedForm = (props: Props) => { autoCapitalize="off" spellCheck="false" value={ gasLimit } - disabled={ coinSymbol === selectedCurrency && data.length > 0 } + disabled={ networkSymbol === currency && data.length > 0 } onChange={ event => onGasLimitChange(event.target.value) } /> { errors.gasLimit ? ({ errors.gasLimit }) : null } { warnings.gasLimit ? ({ warnings.gasLimit }) : null } + { calculatingGasLimit ? (Calculating...) : null }
- + { errors.data ? ({ errors.data }) : null }
diff --git a/src/js/components/wallet/account/send/SendForm.js b/src/js/components/wallet/account/send/SendForm.js index ffc72cb0..bdb781d9 100644 --- a/src/js/components/wallet/account/send/SendForm.js +++ b/src/js/components/wallet/account/send/SendForm.js @@ -6,40 +6,52 @@ import Select from 'react-select'; import AdvancedForm from './AdvancedForm'; import PendingTransactions from './PendingTransactions'; import { FeeSelectValue, FeeSelectOption } from './FeeSelect'; -import { Notification } from '~/js/components/common/Notification'; import SelectedAccount from '../SelectedAccount'; import { findAccountTokens } from '~/js/reducers/TokensReducer'; +import { calculate, validation } from '~/js/actions/SendFormActions'; + +import { findToken } from '~/js/reducers/TokensReducer'; import type { Props } from './index'; -import type { ComponentState } from '../SelectedAccount'; +import type { Token } from '~/flowtype'; + + +export default class SendContainer extends Component { + + componentWillReceiveProps(newProps: Props) { + calculate(this.props, newProps); + validation(newProps); + } -export default class Send extends SelectedAccount { render() { - return super.render() || _render(this.props, this.state); + return ( + + + + ); } } -const _render = (props: Props, state: ComponentState): React$Element => { +const Send = (props: Props) => { + + const device = props.wallet.selectedDevice; const { - device, account, + network, discovery, - deviceStatusNotification - } = state; - const selectedAccount = props.selectedAccount; + tokens + } = props.selectedAccount; - if (!device || !account || !discovery || !selectedAccount) return
; + if (!device || !account || !discovery || !network) return null; - const tokens = findAccountTokens(props.tokens, account); - const { network } = selectedAccount; const { address, amount, setMax, - coinSymbol, - selectedCurrency, + networkSymbol, + currency, feeLevels, selectedFeeLevel, gasPriceNeedsUpdate, @@ -48,6 +60,7 @@ const _render = (props: Props, state: ComponentState): React$Element => warnings, infos, advanced, + data, sending, } = props.sendForm; @@ -61,13 +74,12 @@ const _render = (props: Props, state: ComponentState): React$Element => onSend, } = props.sendFormActions; - const selectedCoin = selectedAccount.coin; const fiatRate = props.fiat.find(f => f.network === network); const tokensSelectData = tokens.map(t => { return { value: t.symbol, label: t.symbol }; }); - tokensSelectData.unshift({ value: selectedCoin.symbol, label: selectedCoin.symbol }); + tokensSelectData.unshift({ value: network.symbol, label: network.symbol }); const setMaxClassName: string = setMax ? 'set-max enabled' : 'set-max'; @@ -89,10 +101,10 @@ const _render = (props: Props, state: ComponentState): React$Element => let buttonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending; let buttonLabel: string = 'Send'; - if (coinSymbol !== selectedCurrency && amount.length > 0 && !errors.amount) { - buttonLabel += ` ${amount} ${ selectedCurrency.toUpperCase() }` - } else if (coinSymbol === selectedCurrency && total !== '0') { - buttonLabel += ` ${total} ${ selectedCoin.symbol }`; + if (networkSymbol !== currency && amount.length > 0 && !errors.amount) { + buttonLabel += ` ${amount} ${ currency.toUpperCase() }` + } else if (networkSymbol === currency && total !== '0') { + buttonLabel += ` ${total} ${ network.symbol }`; } if (!device.connected){ @@ -105,14 +117,9 @@ const _render = (props: Props, state: ComponentState): React$Element => buttonLabel = 'Loading accounts'; buttonDisabled = true; } - - let notification = null; - + return (
- - { deviceStatusNotification } -

Send Ethereum or tokens

@@ -152,7 +159,7 @@ const _render = (props: Props, state: ComponentState): React$Element => searchable={ false } clearable= { false } multi={ false } - value={ selectedCurrency } + value={ currency } disabled={ tokensSelectData.length < 2 } onChange={ onCurrencyChange } options={ tokensSelectData } /> @@ -172,6 +179,7 @@ const _render = (props: Props, state: ComponentState): React$Element => onChange={ onFeeLevelChange } valueComponent={ FeeSelectValue } optionComponent={ FeeSelectOption } + disabled={ networkSymbol === currency && data.length > 0 } optionClassName="fee-option" options={ feeLevels } />
@@ -187,7 +195,7 @@ const _render = (props: Props, state: ComponentState): React$Element => pending={ props.pending } tokens={ props.tokens } account={ account } - selectedCoin={ selectedCoin } /> + selectedCoin={ network } />
); diff --git a/src/js/components/wallet/account/send/index.js b/src/js/components/wallet/account/send/index.js index 63b30752..b8415ff6 100644 --- a/src/js/components/wallet/account/send/index.js +++ b/src/js/components/wallet/account/send/index.js @@ -6,7 +6,6 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { default as SendFormActions } from '~/js/actions/SendFormActions'; -import { default as SelectedAccountActions } from '~/js/actions/SelectedAccountActions'; import SendForm from './SendForm'; import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; @@ -28,14 +27,14 @@ export type DispatchProps = BaseDispatchProps & { sendFormActions: typeof SendFormActions } -export type Props = StateProps & DispatchProps; +export type Props = StateProps & BaseStateProps & DispatchProps & BaseDispatchProps; const mapStateToProps: MapStateToProps = (state: State, own: OwnProps): StateProps => { return { + className: "send-from", selectedAccount: state.selectedAccount, - devices: state.devices, - accounts: state.accounts, - discovery: state.discovery, + wallet: state.wallet, + tokens: state.tokens, pending: state.pending, sendForm: state.sendForm, @@ -46,10 +45,7 @@ const mapStateToProps: MapStateToProps = (state: St const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => { return { - selectedAccountActions: bindActionCreators(SelectedAccountActions, dispatch), sendFormActions: bindActionCreators(SendFormActions, dispatch), - initAccount: bindActionCreators(SendFormActions.init, dispatch), - disposeAccount: bindActionCreators(SendFormActions.dispose, dispatch), }; } diff --git a/src/js/components/wallet/account/summary/Summary.js b/src/js/components/wallet/account/summary/Summary.js index 28d2d1ee..4d053439 100644 --- a/src/js/components/wallet/account/summary/Summary.js +++ b/src/js/components/wallet/account/summary/Summary.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import BigNumber from 'bignumber.js'; -import { Async } from 'react-select'; +import { Async as AsyncSelect } from 'react-select'; import Tooltip from 'rc-tooltip'; import { resolveAfter } from '~/js/utils/promiseUtils'; @@ -13,56 +13,38 @@ import SummaryDetails from './SummaryDetails.js'; import SummaryTokens from './SummaryTokens.js'; import type { Props } from './index'; -import type { ComponentState } from '../SelectedAccount'; - -import type { TrezorDevice } from '~/flowtype'; import type { NetworkToken } from '~/js/reducers/LocalStorageReducer'; -import type { Account } from '~/js/reducers/AccountsReducer'; -import type { Discovery } from '~/js/reducers/DiscoveryReducer'; -import { findAccountTokens } from '~/js/reducers/TokensReducer'; - -export default class Summary extends SelectedAccount { - render() { - return super.render() || _render(this.props, this.state); - } -} - -const _render = (props: Props, state: ComponentState): React$Element => { +const Summary = (props: Props) => { + const device = props.wallet.selectedDevice; const { - device, account, - deviceStatusNotification - } = state; - const selectedAccount = props.selectedAccount; + network, + tokens, + } = props.selectedAccount; - if (!device || !account || !selectedAccount) return
; - - - const tokens = findAccountTokens(props.tokens, account); - const explorerLink: string = `${selectedAccount.coin.explorer.address}${account.address}`; + // flow + if (!device || !account || !network) return null; const tokensTooltip = (
Insert token name, symbol or address to be able to send it.
); + const explorerLink: string = `${network.explorer.address}${account.address}`; return ( - -
- { deviceStatusNotification } - -

- Account #{ parseInt(selectedAccount.index) + 1 } +
+

+ Account #{ parseInt(account.index) + 1 } See full transaction history

@@ -79,7 +61,7 @@ const _render = (props: Props, state: ComponentState): React$Element => {/* 0x58cda554935e4a1f2acbe15f8757400af275e084 Lahod */} {/* 0x58cda554935e4a1f2acbe15f8757400af275e084 T01 */}
- => return o; } }); - - // return options.filter(o => { - // return !tokens.find(t => t.symbol === o.symbol); - // }); } } valueKey="symbol" labelKey="name" placeholder="Search for token" searchPromptText="Type token name or address" - noResultsText="Token not found" - - /> + noResultsText="Token not found" />
- + -

+ + ) +} + +export default (props: Props) => { + return ( + + + ); } \ No newline at end of file diff --git a/src/js/components/wallet/account/summary/index.js b/src/js/components/wallet/account/summary/index.js index b1cfb366..c113e85b 100644 --- a/src/js/components/wallet/account/summary/index.js +++ b/src/js/components/wallet/account/summary/index.js @@ -6,7 +6,6 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import Summary from './Summary'; -import { default as SelectedAccountActions } from '~/js/actions/SelectedAccountActions'; import * as SummaryActions from '~/js/actions/SummaryActions'; import * as TokenActions from '~/js/actions/TokenActions'; @@ -21,7 +20,7 @@ type StateProps = BaseStateProps & { summary: $ElementType, fiat: $ElementType, localStorage: $ElementType, -} +}; type DispatchProps = BaseDispatchProps & { onDetailsToggle: typeof SummaryActions.onDetailsToggle, @@ -30,14 +29,13 @@ type DispatchProps = BaseDispatchProps & { removeToken: typeof TokenActions.remove, } -export type Props = StateProps & DispatchProps; +export type Props = StateProps & BaseStateProps & DispatchProps & BaseDispatchProps; const mapStateToProps: MapStateToProps = (state: State, own: OwnProps): StateProps => { return { + className: "summary", selectedAccount: state.selectedAccount, - devices: state.devices, - accounts: state.accounts, - discovery: state.discovery, + wallet: state.wallet, tokens: state.tokens, summary: state.summary, @@ -48,11 +46,6 @@ const mapStateToProps: MapStateToProps = (state: St const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => { return { - selectedAccountActions: bindActionCreators(SelectedAccountActions, dispatch), - - initAccount: bindActionCreators(SummaryActions.init, dispatch), - disposeAccount: bindActionCreators(SummaryActions.dispose, dispatch), - onDetailsToggle: bindActionCreators(SummaryActions.onDetailsToggle, dispatch), addToken: bindActionCreators(TokenActions.add, dispatch), loadTokens: bindActionCreators(TokenActions.load, dispatch), diff --git a/src/js/reducers/DevicesReducer.js b/src/js/reducers/DevicesReducer.js index 76682f30..87b2cdf6 100644 --- a/src/js/reducers/DevicesReducer.js +++ b/src/js/reducers/DevicesReducer.js @@ -297,14 +297,4 @@ export const getNewInstance = (devices: State, device: Device | TrezorDevice): n }, 0); return instance; -} - -export const findDevice = (devices: State, deviceId: string, deviceState: string, instance: ?number): ?TrezorDevice => { - return devices.find(d => { - // TODO: && (instance && d.instance === instance) - if (d.features && d.features.device_id === deviceId && d.state === deviceState) { - return true; - } - return false; - }); } \ No newline at end of file diff --git a/src/js/reducers/ReceiveReducer.js b/src/js/reducers/ReceiveReducer.js index 4a09e219..8f35a5ae 100644 --- a/src/js/reducers/ReceiveReducer.js +++ b/src/js/reducers/ReceiveReducer.js @@ -3,6 +3,7 @@ import { UI } from 'trezor-connect'; import * as RECEIVE from '../actions/constants/receive'; +import * as ACCOUNT from '../actions/constants/account'; import type { Action } from '~/flowtype'; @@ -23,7 +24,7 @@ export default (state: State = initialState, action: Action): State => { case RECEIVE.INIT : return action.state; - case RECEIVE.DISPOSE : + case ACCOUNT.DISPOSE : return initialState; case RECEIVE.SHOW_ADDRESS : diff --git a/src/js/reducers/SelectedAccountReducer.js b/src/js/reducers/SelectedAccountReducer.js index a1e5ba92..665f0822 100644 --- a/src/js/reducers/SelectedAccountReducer.js +++ b/src/js/reducers/SelectedAccountReducer.js @@ -2,32 +2,41 @@ 'use strict'; import * as ACCOUNT from '../actions/constants/account'; -import * as CONNECT from '../actions/constants/TrezorConnect'; -import type { Action } from '~/flowtype'; -import type { Coin } from './LocalStorageReducer'; +import type { + Action, + Account, + Coin, + Token, + Discovery, + Web3Instance +} from '~/flowtype'; export type State = { - +index: number; - +deviceState: string; - +deviceId: string; - +deviceInstance: ?number; - +network: string; - +coin: Coin; - +location: string; + location?: string; + + account: ?Account; + network: ?Coin; + tokens: Array, + web3: ?Web3Instance, + discovery: ?Discovery }; -export const initialState: ?State = null; +export const initialState: State = { + location: '/', + account: null, + network: null, + tokens: [], + web3: null, + discovery: null +}; -export default (state: ?State = initialState, action: Action): ?State => { +export default (state: State = initialState, action: Action): State => { switch (action.type) { - case ACCOUNT.INIT : - return action.state; - - case ACCOUNT.DISPOSE : - return initialState; + case ACCOUNT.UPDATE_SELECTED_ACCOUNT : + return action.payload; default: return state; diff --git a/src/js/reducers/SendFormReducer.js b/src/js/reducers/SendFormReducer.js index 210a33c5..47cccd4a 100644 --- a/src/js/reducers/SendFormReducer.js +++ b/src/js/reducers/SendFormReducer.js @@ -4,6 +4,8 @@ import * as SEND from '../actions/constants/send'; import * as WEB3 from '../actions/constants/web3'; import * as ACCOUNT from '../actions/constants/account'; +import * as WALLET from '../actions/constants/wallet'; + import EthereumjsUnits from 'ethereumjs-units'; import BigNumber from 'bignumber.js'; import { getFeeLevels } from '../actions/SendFormActions'; @@ -14,10 +16,9 @@ import type { } from '../actions/Web3Actions'; export type State = { - +network: string; - +coinSymbol: string; - selectedCurrency: string; - balanceNeedUpdate: boolean; + +networkName: string; + +networkSymbol: string; + currency: string; // form fields advanced: boolean; @@ -31,14 +32,17 @@ export type State = { recommendedGasPrice: string; gasPriceNeedsUpdate: boolean; gasLimit: string; + calculatingGasLimit: boolean; gasPrice: string; data: string; nonce: string; total: string; - sending: boolean; + errors: {[k: string]: string}; warnings: {[k: string]: string}; infos: {[k: string]: string}; + + sending: boolean; } export type FeeLevel = { @@ -49,14 +53,13 @@ export type FeeLevel = { export const initialState: State = { - network: '', - coinSymbol: '', - selectedCurrency: '', + networkName: '', + networkSymbol: '', + currency: '', advanced: false, untouched: true, touched: {}, - balanceNeedUpdate: false, address: '', //address: '0x574BbB36871bA6b78E27f4B4dCFb76eA0091880B', amount: '', @@ -70,6 +73,7 @@ export const initialState: State = { recommendedGasPrice: '0', gasPriceNeedsUpdate: false, gasLimit: '0', + calculatingGasLimit: false, gasPrice: '0', data: '', nonce: '0', @@ -88,13 +92,13 @@ const onGasPriceUpdated = (state: State, action: Web3UpdateGasPriceAction): Stat // } // const newPrice = getRandomInt(10, 50).toString(); const newPrice: string = EthereumjsUnits.convert(action.gasPrice, 'wei', 'gwei'); - if (action.network === state.network && newPrice !== state.recommendedGasPrice) { + 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.coinSymbol, newPrice, state.gasLimit); + const newFeeLevels = getFeeLevels(state.networkSymbol, newPrice, state.gasLimit); const selectedFeeLevel: ?FeeLevel = newFeeLevels.find(f => f.value === 'Normal'); if (!selectedFeeLevel) return state; newState.recommendedGasPrice = newPrice; @@ -107,19 +111,6 @@ const onGasPriceUpdated = (state: State, action: Web3UpdateGasPriceAction): Stat return state; } -const onBalanceUpdated = (state: State, action: any): State => { - // balanceNeedUpdate - // if (state.senderAddress === action.address) { - // return { - // ...state, - // balance: '1' - // } - // } - - // TODO: handle balance update during send form life cycle - return state; -} - export default (state: State = initialState, action: Action): State => { @@ -128,7 +119,7 @@ export default (state: State = initialState, action: Action): State => { case SEND.INIT : return action.state; - case SEND.DISPOSE : + case ACCOUNT.DISPOSE : return initialState; // this will be called right after Web3 instance initialization before any view is shown @@ -136,8 +127,6 @@ export default (state: State = initialState, action: Action): State => { case WEB3.GAS_PRICE_UPDATED : return onGasPriceUpdated(state, action); - case ACCOUNT.SET_BALANCE : - return onBalanceUpdated(state, action); case SEND.TOGGLE_ADVANCED : return { @@ -148,6 +137,7 @@ export default (state: State = initialState, action: Action): State => { // user actions case SEND.ADDRESS_CHANGE : + case SEND.ADDRESS_VALIDATION : case SEND.AMOUNT_CHANGE : case SEND.SET_MAX : case SEND.CURRENCY_CHANGE : @@ -165,33 +155,12 @@ export default (state: State = initialState, action: Action): State => { sending: true, } - case SEND.TX_COMPLETE : - return { - ...state, - - sending: false, - untouched: true, - touched: {}, - address: '', - amount: '', - setMax: false, - gasPriceNeedsUpdate: false, - gasLimit: state.gasLimit, - gasPrice: state.recommendedGasPrice, - data: '', - nonce: new BigNumber(state.nonce).plus(1).toString(), - total: '0', - errors: {}, - warnings: {}, - infos: {}, - } case SEND.TX_ERROR : return { ...state, sending: false, } - case SEND.VALIDATION : return { ...state, diff --git a/src/js/reducers/SummaryReducer.js b/src/js/reducers/SummaryReducer.js index 17303048..1d93796f 100644 --- a/src/js/reducers/SummaryReducer.js +++ b/src/js/reducers/SummaryReducer.js @@ -1,6 +1,7 @@ /* @flow */ 'use strict'; +import * as ACCOUNT from '../actions/constants/account'; import * as SUMMARY from '../actions/constants/summary'; import type { Action } from '~/flowtype'; import type { NetworkToken } from './LocalStorageReducer'; @@ -13,13 +14,15 @@ export type State = { export const initialState: State = { details: true, selectedToken: null -}; - +} export default (state: State = initialState, action: Action): State => { switch (action.type) { + case ACCOUNT.DISPOSE : + return initialState; + case SUMMARY.INIT : return action.state; diff --git a/src/js/reducers/utils/index.js b/src/js/reducers/utils/index.js index 9b8f0794..97a7b9ac 100644 --- a/src/js/reducers/utils/index.js +++ b/src/js/reducers/utils/index.js @@ -40,8 +40,20 @@ export const getSelectedDevice = (state: State): ?TrezorDevice => { }); } +// export const isSelectedDevice = (current: ?TrezorDevice, device: ?TrezorDevice): boolean => { - return (current && device && (current.path === device.path || current.instance === device.instance)) ? true : false; + return (current && device && (current.path === device.path && current.instance === device.instance)) ? true : false; +} + +// find device by id and state +export const findDevice = (devices: Array, deviceId: string, deviceState: string, instance: ?number): ?TrezorDevice => { + return devices.find(d => { + // TODO: && (instance && d.instance === instance) + if (d.features && d.features.device_id === deviceId && d.state === deviceState) { + return true; + } + return false; + }); } export const getSelectedAccount = (state: State): ?Account => { @@ -71,10 +83,10 @@ export const getDiscoveryProcess = (state: State): ?Discovery => { return state.discovery.find(d => d.deviceState === device.state && d.network === locationState.network); } -export const getTokens = (state: State): Array => { - const account = state.selectedAccount.account; - if (!account) return state.selectedAccount.tokens; - return state.tokens.filter(t => t.ethAddress === account.address && t.network === account.network && t.deviceState === account.deviceState); +export const getTokens = (state: State, account: ?Account): Array => { + const a = account; + if (!a) return state.selectedAccount.tokens; + return state.tokens.filter(t => t.ethAddress === a.address && t.network === a.network && t.deviceState === a.deviceState); } export const getWeb3 = (state: State): ?Web3Instance => { diff --git a/src/js/services/WalletService.js b/src/js/services/WalletService.js index 4a826161..233c06eb 100644 --- a/src/js/services/WalletService.js +++ b/src/js/services/WalletService.js @@ -3,10 +3,12 @@ import { LOCATION_CHANGE } from 'react-router-redux'; import * as WALLET from '../actions/constants/wallet'; +import * as SEND from '../actions/constants/wallet'; import * as WalletActions from '../actions/WalletActions'; import * as LocalStorageActions from '../actions/LocalStorageActions'; import * as TrezorConnectActions from '../actions/TrezorConnectActions'; +import * as SelectedAccountActions from '../actions/SelectedAccountActions'; import type { Middleware, @@ -19,24 +21,6 @@ import type { TrezorDevice, } from '~/flowtype'; - -const getSelectedDevice = (state: State): ?TrezorDevice => { - const locationState = state.router.location.state; - if (!locationState.device) return undefined; - - const instance: ?number = locationState.deviceInstance ? parseInt(locationState.deviceInstance) : undefined; - return state.devices.find(d => { - if (d.unacquired && d.path === locationState.device) { - return true; - } else if (d.features && d.features.bootloader_mode && d.path === locationState.device) { - return true; - } else if (d.features && d.features.device_id === locationState.device && d.instance === instance) { - return true; - } - return false; - }); -} - /** * Middleware */ @@ -58,38 +42,22 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa // pass action next(action); - const state = api.getState(); + // update common values in WallerReducer + api.dispatch( WalletActions.updateSelectedValues(prevState, action) ); - // handle devices state change - if (locationChange || prevState.devices !== state.devices) { - const device = getSelectedDevice(state); - const currentDevice = state.wallet.selectedDevice; + // update common values in SelectedAccountReducer + api.dispatch( SelectedAccountActions.updateSelectedValues(prevState, action) ); - if (state.wallet.selectedDevice !== device) { - if (!locationChange && currentDevice && device && (currentDevice.path === device.path || currentDevice.instance === device.instance)) { - api.dispatch({ - type: WALLET.UPDATE_SELECTED_DEVICE, - device - }) - } else { - api.dispatch({ - type: WALLET.SET_SELECTED_DEVICE, - device - }); - - if (device) { - api.dispatch( TrezorConnectActions.getSelectedDeviceState() ); - } else { - api.dispatch( TrezorConnectActions.switchToFirstAvailableDevice() ); - } - - } + // selected device changed + if (action.type === WALLET.SET_SELECTED_DEVICE) { + if (action.device) { + api.dispatch( TrezorConnectActions.getSelectedDeviceState() ); + } else { + api.dispatch( TrezorConnectActions.switchToFirstAvailableDevice() ); } } return action; }; - - export default WalletService; \ No newline at end of file