From e34b9141d8f1183c0b6e911c6e4d2c43aea98466 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Thu, 29 Nov 2018 21:02:56 +0100 Subject: [PATCH] change prefix of coin specific actions --- src/actions/DiscoveryActions.js | 4 +- src/actions/SendFormActions.js | 556 +----------------- ...iscoveryActions.js => DiscoveryActions.js} | 0 .../SendFormActions.js} | 246 +++----- .../SendFormValidationActions.js | 8 +- ...iscoveryActions.js => DiscoveryActions.js} | 0 src/actions/ripple/SendFormActions.js | 490 +++++++++++++++ ...ctions.js => SendFormValidationActions.js} | 26 +- 8 files changed, 615 insertions(+), 715 deletions(-) rename src/actions/ethereum/{EthereumDiscoveryActions.js => DiscoveryActions.js} (100%) rename src/actions/{ripple/RippleSendFormActions.js => ethereum/SendFormActions.js} (74%) rename src/actions/{ => ethereum}/SendFormValidationActions.js (98%) rename src/actions/ripple/{RippleDiscoveryActions.js => DiscoveryActions.js} (100%) create mode 100644 src/actions/ripple/SendFormActions.js rename src/actions/ripple/{RippleSendFormValidationActions.js => SendFormValidationActions.js} (92%) diff --git a/src/actions/DiscoveryActions.js b/src/actions/DiscoveryActions.js index 60c99914..fc1636a2 100644 --- a/src/actions/DiscoveryActions.js +++ b/src/actions/DiscoveryActions.js @@ -17,8 +17,8 @@ import type { } from 'flowtype'; import type { Discovery, State } from 'reducers/DiscoveryReducer'; import * as BlockchainActions from './BlockchainActions'; -import * as EthereumDiscoveryActions from './ethereum/EthereumDiscoveryActions'; -import * as RippleDiscoveryActions from './ripple/RippleDiscoveryActions'; +import * as EthereumDiscoveryActions from './ethereum/DiscoveryActions'; +import * as RippleDiscoveryActions from './ripple/DiscoveryActions'; export type DiscoveryStartAction = EthereumDiscoveryActions.DiscoveryStartAction | RippleDiscoveryActions.DiscoveryStartAction; diff --git a/src/actions/SendFormActions.js b/src/actions/SendFormActions.js index 1a052615..bfef42dd 100644 --- a/src/actions/SendFormActions.js +++ b/src/actions/SendFormActions.js @@ -1,18 +1,7 @@ /* @flow */ -import React from 'react'; -import Link from 'components/Link'; -import TrezorConnect from 'trezor-connect'; -import BigNumber from 'bignumber.js'; import * as ACCOUNT from 'actions/constants/account'; -import * as NOTIFICATION from 'actions/constants/notification'; import * as SEND from 'actions/constants/send'; import * as WEB3 from 'actions/constants/web3'; -import * as ValidationActions from 'actions/SendFormValidationActions'; - -import { initialState } from 'reducers/SendFormReducer'; -import { findToken } from 'reducers/TokensReducer'; -import * as reducerUtils from 'reducers/utils'; -import * as ethUtils from 'utils/ethUtils'; import type { Dispatch, @@ -20,17 +9,17 @@ import type { State as ReducersState, Action, ThunkAction, - AsyncAction, - TrezorDevice, } from 'flowtype'; -import type { State, FeeLevel } from 'reducers/SendFormReducer'; +import type { State as EthereumState } from 'reducers/SendFormEthereumReducer'; +import type { State as RippleState } from 'reducers/SendFormRippleReducer'; import type { Account } from 'reducers/AccountsReducer'; -import * as SessionStorageActions from './SessionStorageActions'; -import { prepareEthereumTx, serializeEthereumTx } from './TxActions'; -import * as BlockchainActions from './BlockchainActions'; + +import * as EthereumSendFormActions from './ethereum/SendFormActions'; +import * as RippleSendFormActions from './ripple/SendFormActions'; export type SendTxAction = { type: typeof SEND.TX_COMPLETE, + //networkType: 'ethereum', account: Account, selectedCurrency: string, amount: string, @@ -39,15 +28,21 @@ export type SendTxAction = { nonce: number, txid: string, txData: any, -}; +} export type SendFormAction = { type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE, - state: State, + networkType: 'ethereum', + state: EthereumState, +} | { + type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE, + networkType: 'ripple', + state: RippleState, } | { type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR, } | SendTxAction; + // list of all actions which has influence on "sendForm" reducer // other actions will be ignored const actions = [ @@ -67,521 +62,16 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction = // do not proceed if it's not "send" url if (!currentState.router.location.state.send) return; - // if action type is SEND.VALIDATION which is called as result of this process - // save data to session storage - if (action.type === SEND.VALIDATION) { - dispatch(SessionStorageActions.saveDraftTransaction()); - return; - } - - // if send form was not initialized - if (currentState.sendForm.currency === '') { - dispatch(init()); - return; - } - - // handle gasPrice update from backend - // recalculate fee levels if needed - if (action.type === WEB3.GAS_PRICE_UPDATED) { - dispatch(ValidationActions.onGasPriceUpdated(action.network, action.gasPrice)); - return; - } - - let shouldUpdate: boolean = false; - // check if "selectedAccount" reducer changed - shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, { - account: ['balance', 'nonce', 'tokens'], - }); - if (shouldUpdate && currentState.sendForm.currency !== currentState.sendForm.networkSymbol) { - // make sure that this token is added into account - const { account, tokens } = getState().selectedAccount; - if (!account) return; - const token = findToken(tokens, account.address, currentState.sendForm.currency, account.deviceState); - if (!token) { - // token not found, re-init form - dispatch(init()); - return; - } - } - - - // check if "sendForm" reducer changed - if (!shouldUpdate) { - shouldUpdate = reducerUtils.observeChanges(prevState.sendForm, currentState.sendForm); - } - - if (shouldUpdate) { - const validated = dispatch(ValidationActions.validation()); - dispatch({ - type: SEND.VALIDATION, - state: validated, - }); - } -}; - -/* -* Called from "observe" action -* Initialize "sendForm" reducer data -* Get data either from session storage or "selectedAccount" reducer -*/ -export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const { - account, - network, - } = getState().selectedAccount; - - if (!account || !network) return; - - const stateFromStorage = dispatch(SessionStorageActions.loadDraftTransaction()); - if (stateFromStorage) { - // TODO: consider if current gasPrice should be set here as "recommendedGasPrice" - dispatch({ - type: SEND.INIT, - state: stateFromStorage, - }); - return; - } - - const gasPrice: BigNumber = await dispatch(BlockchainActions.getGasPrice(network.shortcut, network.defaultGasPrice)); - const gasLimit = network.defaultGasLimit.toString(); - const feeLevels = ValidationActions.getFeeLevels(network.symbol, gasPrice, gasLimit); - const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); - - dispatch({ - type: SEND.INIT, - state: { - ...initialState, - networkName: network.shortcut, - networkSymbol: network.symbol, - currency: network.symbol, - feeLevels, - selectedFeeLevel, - recommendedGasPrice: gasPrice.toString(), - gasLimit, - gasPrice: gasPrice.toString(), - }, - }); -}; - -/* -* Called from UI from "advanced" button -*/ -export const toggleAdvanced = (): Action => ({ - type: SEND.TOGGLE_ADVANCED, -}); - -/* -* Called from UI on "address" field change -*/ -export const onAddressChange = (address: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state: State = getState().sendForm; - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - untouched: false, - touched: { ...state.touched, address: true }, - address, - }, - }); -}; - -/* -* Called from UI on "amount" field change -*/ -export const onAmountChange = (amount: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state = getState().sendForm; - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - untouched: false, - touched: { ...state.touched, amount: true }, - setMax: false, - amount, - }, - }); -}; - -/* -* Called from UI on "currency" selection change -*/ -export const onCurrencyChange = (currency: { value: string, label: string }): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const { - account, - network, - } = getState().selectedAccount; - if (!account || !network) return; - - const state = getState().sendForm; - - const isToken = currency.value !== state.networkSymbol; - const gasLimit = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString(); - - const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, gasLimit, state.selectedFeeLevel); - const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); - - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - currency: currency.value, - feeLevels, - selectedFeeLevel, - gasLimit, - }, - }); -}; - -/* -* Called from UI from "set max" button -*/ -export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state = getState().sendForm; - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - untouched: false, - touched: { ...state.touched, amount: true }, - setMax: !state.setMax, - }, - }); -}; - -/* -* Called from UI on "fee" selection change -*/ -export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state = getState().sendForm; - - const isCustom = feeLevel.value === 'Custom'; - let newGasLimit = state.gasLimit; - let newGasPrice = state.gasPrice; - const advanced = isCustom ? true : state.advanced; - - if (!isCustom) { - // if selected fee is not custom - // update gasLimit to default and gasPrice to selected value - const { network } = getState().selectedAccount; - if (!network) return; - const isToken = state.currency !== state.networkSymbol; - if (isToken) { - newGasLimit = network.defaultGasLimitTokens.toString(); - } else { - // corner case: gas limit was changed by user OR by "estimateGasPrice" action - // leave gasLimit as it is - newGasLimit = state.touched.gasLimit ? state.gasLimit : network.defaultGasLimit.toString(); - } - newGasPrice = feeLevel.gasPrice; - } - - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - advanced, - selectedFeeLevel: feeLevel, - gasLimit: newGasLimit, - gasPrice: newGasPrice, - }, - }); -}; - -/* -* Called from UI from "update recommended fees" button -*/ -export const updateFeeLevels = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const { - account, - network, - } = getState().selectedAccount; - if (!account || !network) return; - - const state: State = getState().sendForm; - const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, state.gasLimit, state.selectedFeeLevel); - const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); - - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - feeLevels, - selectedFeeLevel, - gasPrice: selectedFeeLevel.gasPrice, - gasPriceNeedsUpdate: false, - }, - }); -}; - -/* -* Called from UI on "gas price" field change -*/ -export const onGasPriceChange = (gasPrice: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state: State = getState().sendForm; - // switch to custom fee level - let newSelectedFeeLevel = state.selectedFeeLevel; - if (state.selectedFeeLevel.value !== 'Custom') newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom'); - - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - untouched: false, - touched: { ...state.touched, gasPrice: true }, - gasPrice, - selectedFeeLevel: newSelectedFeeLevel, - }, - }); -}; - -/* -* Called from UI on "data" field change -* OR from "estimateGasPrice" action -*/ -export const onGasLimitChange = (gasLimit: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const { network } = getState().selectedAccount; - if (!network) return; - const state: State = getState().sendForm; - // recalculate feeLevels with recommended gasPrice - const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, gasLimit, state.selectedFeeLevel); - const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); - - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - calculatingGasLimit: false, - untouched: false, - touched: { ...state.touched, gasLimit: true }, - gasLimit, - feeLevels, - selectedFeeLevel, - }, - }); -}; - -/* -* Called from UI on "nonce" field change -*/ -export const onNonceChange = (nonce: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state: State = getState().sendForm; - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - untouched: false, - touched: { ...state.touched, nonce: true }, - nonce, - }, - }); -}; - -/* -* Called from UI on "data" field change -*/ -export const onDataChange = (data: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state: State = getState().sendForm; - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - calculatingGasLimit: true, - untouched: false, - touched: { ...state.touched, data: true }, - data, - }, - }); - - dispatch(estimateGasPrice()); -}; - -export const setDefaultGasLimit = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state: State = getState().sendForm; - const { network } = getState().selectedAccount; + const { network } = currentState.selectedAccount; if (!network) return; - const isToken = state.currency !== state.networkSymbol; - const gasLimit = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString(); - - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - calculatingGasLimit: false, - untouched: false, - touched: { ...state.touched, gasLimit: false }, - gasLimit, - }, - }); -}; - -/* -* Internal method -* Called from "onDataChange" action -* try to asynchronously download data from backend -*/ -const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const state: State = getState().sendForm; - const { network } = getState().selectedAccount; - if (!network) { - // stop "calculatingGasLimit" process - dispatch(onGasLimitChange(state.gasLimit)); - return; - } - - const requestedData = state.data; - if (!ethUtils.isHex(requestedData)) { - // stop "calculatingGasLimit" process - dispatch(onGasLimitChange(requestedData.length > 0 ? state.gasLimit : network.defaultGasLimit.toString())); - return; - } - - if (state.data.length < 1) { - // set default - dispatch(onGasLimitChange(network.defaultGasLimit.toString())); - return; - } - - const gasLimit = await dispatch(BlockchainActions.estimateGasLimit(network.shortcut, state.data, state.amount, state.gasPrice)); - - // double check "data" field - // possible race condition when data changed before backend respond - if (getState().sendForm.data === requestedData) { - dispatch(onGasLimitChange(gasLimit)); - } -}; - -/* -* Called from UI from "send" button -*/ -export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const { - account, - network, - pending, - } = getState().selectedAccount; - - if (!account || !network) return; - - const currentState: State = getState().sendForm; - - const isToken: boolean = currentState.currency !== currentState.networkSymbol; - const pendingNonce: number = reducerUtils.getPendingNonce(pending); - const nonce = pendingNonce > 0 && pendingNonce >= account.nonce ? pendingNonce : account.nonce; - - const txData = await dispatch(prepareEthereumTx({ - network: network.shortcut, - token: isToken ? findToken(getState().tokens, account.address, currentState.currency, account.deviceState) : null, - from: account.address, - to: currentState.address, - amount: currentState.amount, - data: currentState.data, - gasLimit: currentState.gasLimit, - gasPrice: currentState.gasPrice, - nonce, - })); - - const selected: ?TrezorDevice = getState().wallet.selectedDevice; - if (!selected) return; - - const signedTransaction = await TrezorConnect.ethereumSignTransaction({ - device: { - path: selected.path, - instance: selected.instance, - state: selected.state, - }, - // useEmptyPassphrase: !selected.instance, - useEmptyPassphrase: selected.useEmptyPassphrase, - path: account.addressPath, - transaction: txData, - }); - - if (!signedTransaction || !signedTransaction.success) { - dispatch({ - type: NOTIFICATION.ADD, - payload: { - type: 'error', - title: 'Transaction error', - message: signedTransaction.payload.error, - cancelable: true, - actions: [], - }, - }); - return; - } - - txData.r = signedTransaction.payload.r; - txData.s = signedTransaction.payload.s; - txData.v = signedTransaction.payload.v; - - try { - const serializedTx: string = await dispatch(serializeEthereumTx(txData)); - const push = await TrezorConnect.pushTransaction({ - tx: serializedTx, - coin: network.shortcut, - }); - - if (!push.success) { - throw new Error(push.payload.error); - } - - const { txid } = push.payload; - - dispatch({ - type: SEND.TX_COMPLETE, - account, - selectedCurrency: currentState.currency, - amount: currentState.amount, - total: currentState.total, - tx: txData, - nonce, - txid, - txData, - }); - - // clear session storage - dispatch(SessionStorageActions.clear()); - - // reset form - dispatch(init()); - - 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: [], - }, - }); + switch (network.type) { + case 'ethereum': + dispatch(EthereumSendFormActions.observe(prevState, action)); + break; + case 'ripple': + dispatch(RippleSendFormActions.observe(prevState, action)); + break; + default: break; } }; - -export default { - toggleAdvanced, - onAddressChange, - onAmountChange, - onCurrencyChange, - onSetMax, - onFeeLevelChange, - updateFeeLevels, - onGasPriceChange, - onGasLimitChange, - setDefaultGasLimit, - onNonceChange, - onDataChange, - onSend, -}; \ No newline at end of file diff --git a/src/actions/ethereum/EthereumDiscoveryActions.js b/src/actions/ethereum/DiscoveryActions.js similarity index 100% rename from src/actions/ethereum/EthereumDiscoveryActions.js rename to src/actions/ethereum/DiscoveryActions.js diff --git a/src/actions/ripple/RippleSendFormActions.js b/src/actions/ethereum/SendFormActions.js similarity index 74% rename from src/actions/ripple/RippleSendFormActions.js rename to src/actions/ethereum/SendFormActions.js index 839c2453..8658d5f6 100644 --- a/src/actions/ripple/RippleSendFormActions.js +++ b/src/actions/ethereum/SendFormActions.js @@ -7,9 +7,7 @@ import * as ACCOUNT from 'actions/constants/account'; import * as NOTIFICATION from 'actions/constants/notification'; import * as SEND from 'actions/constants/send'; import * as WEB3 from 'actions/constants/web3'; -import * as ValidationActions from 'actions/SendFormValidationActions'; - -import { initialState } from 'reducers/SendFormReducer'; +import { initialState } from 'reducers/SendFormEthereumReducer'; import { findToken } from 'reducers/TokensReducer'; import * as reducerUtils from 'reducers/utils'; import * as ethUtils from 'utils/ethUtils'; @@ -23,12 +21,14 @@ import type { AsyncAction, TrezorDevice, } from 'flowtype'; -import type { State, FeeLevel } from 'reducers/SendFormReducer'; +import type { State, FeeLevel } from 'reducers/SendFormEthereumReducer'; import type { Account } from 'reducers/AccountsReducer'; import * as SessionStorageActions from '../SessionStorageActions'; import { prepareEthereumTx, serializeEthereumTx } from '../TxActions'; import * as BlockchainActions from '../BlockchainActions'; +import * as ValidationActions from './SendFormValidationActions'; + export type SendTxAction = { type: typeof SEND.TX_COMPLETE, account: Account, @@ -41,14 +41,14 @@ export type SendTxAction = { txData: any, }; -export type SendFormAction = { +export type sendFormEthereumAction = { type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE, state: State, } | { type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR, } | SendTxAction; -// list of all actions which has influence on "sendForm" reducer +// list of all actions which has influence on "sendFormEthereum" reducer // other actions will be ignored const actions = [ ACCOUNT.UPDATE_SELECTED_ACCOUNT, @@ -75,7 +75,7 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction = } // if send form was not initialized - if (currentState.sendForm.currency === '') { + if (currentState.sendFormEthereum.currency === '') { dispatch(init()); return; } @@ -90,18 +90,30 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction = let shouldUpdate: boolean = false; // check if "selectedAccount" reducer changed shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, { - account: ['balance', 'nonce'], + account: ['balance', 'nonce', 'tokens'], }); + if (shouldUpdate && currentState.sendFormEthereum.currency !== currentState.sendFormEthereum.networkSymbol) { + // make sure that this token is added into account + const { account, tokens } = getState().selectedAccount; + if (!account) return; + const token = findToken(tokens, account.address, currentState.sendFormEthereum.currency, account.deviceState); + if (!token) { + // token not found, re-init form + dispatch(init()); + return; + } + } - // check if "sendForm" reducer changed + // check if "sendFormEthereum" reducer changed if (!shouldUpdate) { - shouldUpdate = reducerUtils.observeChanges(prevState.sendForm, currentState.sendForm); + shouldUpdate = reducerUtils.observeChanges(prevState.sendFormEthereum, currentState.sendFormEthereum); } if (shouldUpdate) { const validated = dispatch(ValidationActions.validation()); dispatch({ type: SEND.VALIDATION, + networkType: 'ethereum', state: validated, }); } @@ -109,7 +121,7 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction = /* * Called from "observe" action -* Initialize "sendForm" reducer data +* Initialize "sendFormEthereum" reducer data * Get data either from session storage or "selectedAccount" reducer */ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { @@ -125,49 +137,32 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS // TODO: consider if current gasPrice should be set here as "recommendedGasPrice" dispatch({ type: SEND.INIT, + networkType: 'ethereum', state: stateFromStorage, }); return; } - if (network.type === 'ethereum') { - const gasPrice: BigNumber = network.type === 'ethereum' ? await dispatch(BlockchainActions.getGasPrice(network.shortcut, network.defaultGasPrice)) : new BigNumber(0); - const gasLimit = network.defaultGasLimit.toString(); - const feeLevels = ValidationActions.getFeeLevels(network.symbol, gasPrice, gasLimit); - const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); + const gasPrice: BigNumber = await dispatch(BlockchainActions.getGasPrice(network.shortcut, network.defaultGasPrice)); + const gasLimit = network.defaultGasLimit.toString(); + const feeLevels = ValidationActions.getFeeLevels(network.symbol, gasPrice, gasLimit); + const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); - dispatch({ - type: SEND.INIT, - state: { - ...initialState, - networkName: network.shortcut, - networkSymbol: network.symbol, - currency: network.symbol, - feeLevels, - selectedFeeLevel, - recommendedGasPrice: gasPrice.toString(), - gasLimit, - gasPrice: gasPrice.toString(), - }, - }); - } else { - const feeLevels = ValidationActions.getFeeLevels(network.symbol, '1', '1'); - const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); - dispatch({ - type: SEND.INIT, - state: { - ...initialState, - networkName: network.shortcut, - networkSymbol: network.symbol, - currency: network.symbol, - feeLevels, - selectedFeeLevel, - recommendedGasPrice: '1', - gasLimit: '1', - gasPrice: '1', - }, - }); - } + dispatch({ + type: SEND.INIT, + networkType: 'ethereum', + state: { + ...initialState, + networkName: network.shortcut, + networkSymbol: network.symbol, + currency: network.symbol, + feeLevels, + selectedFeeLevel, + recommendedGasPrice: gasPrice.toString(), + gasLimit, + gasPrice: gasPrice.toString(), + }, + }); }; /* @@ -181,9 +176,10 @@ export const toggleAdvanced = (): Action => ({ * Called from UI on "address" field change */ export const onAddressChange = (address: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state: State = getState().sendForm; + const state: State = getState().sendFormEthereum; dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, untouched: false, @@ -197,9 +193,10 @@ export const onAddressChange = (address: string): ThunkAction => (dispatch: Disp * Called from UI on "amount" field change */ export const onAmountChange = (amount: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state = getState().sendForm; + const state = getState().sendFormEthereum; dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, untouched: false, @@ -220,7 +217,7 @@ export const onCurrencyChange = (currency: { value: string, label: string }): Th } = getState().selectedAccount; if (!account || !network) return; - const state = getState().sendForm; + const state = getState().sendFormEthereum; const isToken = currency.value !== state.networkSymbol; const gasLimit = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString(); @@ -230,6 +227,7 @@ export const onCurrencyChange = (currency: { value: string, label: string }): Th dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, currency: currency.value, @@ -244,9 +242,10 @@ export const onCurrencyChange = (currency: { value: string, label: string }): Th * Called from UI from "set max" button */ export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state = getState().sendForm; + const state = getState().sendFormEthereum; dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, untouched: false, @@ -260,7 +259,7 @@ export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetSta * Called from UI on "fee" selection change */ export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state = getState().sendForm; + const state = getState().sendFormEthereum; const isCustom = feeLevel.value === 'Custom'; let newGasLimit = state.gasLimit; @@ -285,6 +284,7 @@ export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (dispatch: dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, advanced, @@ -305,12 +305,13 @@ export const updateFeeLevels = (): ThunkAction => (dispatch: Dispatch, getState: } = getState().selectedAccount; if (!account || !network) return; - const state: State = getState().sendForm; + const state: State = getState().sendFormEthereum; const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, state.gasLimit, state.selectedFeeLevel); const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, feeLevels, @@ -325,13 +326,14 @@ export const updateFeeLevels = (): ThunkAction => (dispatch: Dispatch, getState: * Called from UI on "gas price" field change */ export const onGasPriceChange = (gasPrice: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state: State = getState().sendForm; + const state: State = getState().sendFormEthereum; // switch to custom fee level let newSelectedFeeLevel = state.selectedFeeLevel; if (state.selectedFeeLevel.value !== 'Custom') newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom'); dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, untouched: false, @@ -349,13 +351,14 @@ export const onGasPriceChange = (gasPrice: string): ThunkAction => (dispatch: Di export const onGasLimitChange = (gasLimit: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const { network } = getState().selectedAccount; if (!network) return; - const state: State = getState().sendForm; + const state: State = getState().sendFormEthereum; // recalculate feeLevels with recommended gasPrice const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, gasLimit, state.selectedFeeLevel); const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, calculatingGasLimit: false, @@ -372,9 +375,10 @@ export const onGasLimitChange = (gasLimit: string): ThunkAction => (dispatch: Di * Called from UI on "nonce" field change */ export const onNonceChange = (nonce: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state: State = getState().sendForm; + const state: State = getState().sendFormEthereum; dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, untouched: false, @@ -388,9 +392,10 @@ export const onNonceChange = (nonce: string): ThunkAction => (dispatch: Dispatch * Called from UI on "data" field change */ export const onDataChange = (data: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const state: State = getState().sendForm; + const state: State = getState().sendFormEthereum; dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, calculatingGasLimit: true, @@ -403,13 +408,34 @@ export const onDataChange = (data: string): ThunkAction => (dispatch: Dispatch, dispatch(estimateGasPrice()); }; +export const setDefaultGasLimit = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state: State = getState().sendFormEthereum; + const { network } = getState().selectedAccount; + if (!network) return; + + const isToken = state.currency !== state.networkSymbol; + const gasLimit = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString(); + + dispatch({ + type: SEND.CHANGE, + networkType: 'ethereum', + state: { + ...state, + calculatingGasLimit: false, + untouched: false, + touched: { ...state.touched, gasLimit: false }, + gasLimit, + }, + }); +}; + /* * Internal method * Called from "onDataChange" action * try to asynchronously download data from backend */ const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const state: State = getState().sendForm; + const state: State = getState().sendFormEthereum; const { network } = getState().selectedAccount; if (!network) { // stop "calculatingGasLimit" process @@ -434,7 +460,7 @@ const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState: // double check "data" field // possible race condition when data changed before backend respond - if (getState().sendForm.data === requestedData) { + if (getState().sendFormEthereum.data === requestedData) { dispatch(onGasLimitChange(gasLimit)); } }; @@ -442,104 +468,7 @@ const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState: /* * Called from UI from "send" button */ - export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const { - account, - network, - } = getState().selectedAccount; - - const selected: ?TrezorDevice = getState().wallet.selectedDevice; - if (!selected) return; - - if (!account || !network) return; - - const currentState: State = getState().sendForm; - - const signedTransaction = await TrezorConnect.rippleSignTransaction({ - device: { - path: selected.path, - instance: selected.instance, - state: selected.state, - }, - useEmptyPassphrase: selected.useEmptyPassphrase, - path: account.addressPath, - transaction: { - fee: '100000', - flags: 0x80000000, - sequence: account.nonce, - payment: { - amount: currentState.amount, - destination: currentState.address, - }, - }, - }); - - if (!signedTransaction || !signedTransaction.success) { - dispatch({ - type: NOTIFICATION.ADD, - payload: { - type: 'error', - title: 'Transaction error', - message: signedTransaction.payload.error, - cancelable: true, - actions: [], - }, - }); - return; - } - - const push = await TrezorConnect.pushTransaction({ - tx: signedTransaction.payload.serializedTx, - coin: network.shortcut, - }); - - if (!push.success) { - dispatch({ - type: NOTIFICATION.ADD, - payload: { - type: 'error', - title: 'Transaction error', - message: push.payload.error, - cancelable: true, - actions: [], - }, - }); - return; - } - - const { txid } = push.payload; - - dispatch({ - type: SEND.TX_COMPLETE, - account, - selectedCurrency: currentState.currency, - amount: currentState.amount, - total: currentState.total, - tx: {}, - nonce: account.nonce, - txid, - txData: {}, - }); - - // clear session storage - dispatch(SessionStorageActions.clear()); - - // reset form - dispatch(init()); - - dispatch({ - type: NOTIFICATION.ADD, - payload: { - type: 'success', - title: 'Transaction success', - message: See transaction detail, - cancelable: true, - actions: [], - }, - }); -}; -export const onSend1 = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { const { account, network, @@ -548,7 +477,7 @@ export const onSend1 = (): AsyncAction => async (dispatch: Dispatch, getState: G if (!account || !network) return; - const currentState: State = getState().sendForm; + const currentState: State = getState().sendFormEthereum; const isToken: boolean = currentState.currency !== currentState.networkSymbol; const pendingNonce: number = reducerUtils.getPendingNonce(pending); @@ -664,6 +593,7 @@ export default { updateFeeLevels, onGasPriceChange, onGasLimitChange, + setDefaultGasLimit, onNonceChange, onDataChange, onSend, diff --git a/src/actions/SendFormValidationActions.js b/src/actions/ethereum/SendFormValidationActions.js similarity index 98% rename from src/actions/SendFormValidationActions.js rename to src/actions/ethereum/SendFormValidationActions.js index c1a4fe9c..b7985b09 100644 --- a/src/actions/SendFormValidationActions.js +++ b/src/actions/ethereum/SendFormValidationActions.js @@ -13,7 +13,7 @@ import type { GetState, PayloadAction, } from 'flowtype'; -import type { State, FeeLevel } from 'reducers/SendFormReducer'; +import type { State, FeeLevel } from 'reducers/SendFormEthereumReducer'; // general regular expressions const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$'); @@ -38,7 +38,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct // } // const newPrice = getRandomInt(10, 50).toString(); - const state = getState().sendForm; + const state = getState().sendFormEthereum; if (network === state.networkSymbol) return; // check if new price is different then currently recommended @@ -50,6 +50,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct // and let him update manually dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, gasPriceNeedsUpdate: true, @@ -62,6 +63,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct const selectedFeeLevel = getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); dispatch({ type: SEND.CHANGE, + networkType: 'ethereum', state: { ...state, gasPriceNeedsUpdate: false, @@ -81,7 +83,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct export const validation = (): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { // clone deep nested object // to avoid overrides across state history - let state: State = JSON.parse(JSON.stringify(getState().sendForm)); + let state: State = JSON.parse(JSON.stringify(getState().sendFormEthereum)); // reset errors state.errors = {}; state.warnings = {}; diff --git a/src/actions/ripple/RippleDiscoveryActions.js b/src/actions/ripple/DiscoveryActions.js similarity index 100% rename from src/actions/ripple/RippleDiscoveryActions.js rename to src/actions/ripple/DiscoveryActions.js diff --git a/src/actions/ripple/SendFormActions.js b/src/actions/ripple/SendFormActions.js new file mode 100644 index 00000000..7c0dfd56 --- /dev/null +++ b/src/actions/ripple/SendFormActions.js @@ -0,0 +1,490 @@ +/* @flow */ +import React from 'react'; +import Link from 'components/Link'; +import TrezorConnect from 'trezor-connect'; +import * as NOTIFICATION from 'actions/constants/notification'; +import * as SEND from 'actions/constants/send'; +import * as WEB3 from 'actions/constants/web3'; +import { initialState } from 'reducers/SendFormRippleReducer'; +import * as reducerUtils from 'reducers/utils'; +import * as ethUtils from 'utils/ethUtils'; + +import type { + Dispatch, + GetState, + State as ReducersState, + Action, + ThunkAction, + AsyncAction, + TrezorDevice, +} from 'flowtype'; +import type { State, FeeLevel } from 'reducers/SendFormRippleReducer'; +import type { Account } from 'reducers/AccountsReducer'; +import * as SessionStorageActions from '../SessionStorageActions'; +import * as BlockchainActions from '../BlockchainActions'; + +import * as ValidationActions from './SendFormValidationActions'; + +export type SendTxAction = { + type: typeof SEND.TX_COMPLETE, + account: Account, + selectedCurrency: string, + amount: string, + total: string, + tx: any, + nonce: number, + txid: string, + txData: any, +}; + +/* +* Called from WalletService +*/ +export const observe = (prevState: ReducersState, action: Action): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const currentState = getState(); + + // if action type is SEND.VALIDATION which is called as result of this process + // save data to session storage + if (action.type === SEND.VALIDATION) { + dispatch(SessionStorageActions.saveDraftTransaction()); + return; + } + + // if send form was not initialized + if (currentState.sendFormRipple.networkSymbol === '') { + console.warn("i should init myself!") + dispatch(init()); + return; + } + + // handle gasPrice update from backend + // recalculate fee levels if needed + if (action.type === WEB3.GAS_PRICE_UPDATED) { + dispatch(ValidationActions.onGasPriceUpdated(action.network, action.gasPrice)); + return; + } + + let shouldUpdate: boolean = false; + // check if "selectedAccount" reducer changed + shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, { + account: ['balance', 'nonce'], + }); + + // check if "sendForm" reducer changed + if (!shouldUpdate) { + shouldUpdate = reducerUtils.observeChanges(prevState.sendFormRipple, currentState.sendFormRipple); + } + + if (shouldUpdate) { + const validated = dispatch(ValidationActions.validation()); + dispatch({ + type: SEND.VALIDATION, + networkType: 'ripple', + state: validated, + }); + } +}; + +/* +* Called from "observe" action +* Initialize "sendForm" reducer data +* Get data either from session storage or "selectedAccount" reducer +*/ +export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { + const { + account, + network, + } = getState().selectedAccount; + + if (!account || !network) return; + + const stateFromStorage = dispatch(SessionStorageActions.loadDraftTransaction()); + if (stateFromStorage) { + // TODO: consider if current gasPrice should be set here as "recommendedGasPrice" + dispatch({ + type: SEND.INIT, + networkType: 'ripple', + state: stateFromStorage, + }); + return; + } + + const feeLevels = ValidationActions.getFeeLevels(network.symbol, '1', '1'); + const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); + + dispatch({ + type: SEND.INIT, + networkType: 'ripple', + state: { + ...initialState, + networkName: network.shortcut, + networkSymbol: network.symbol, + feeLevels, + selectedFeeLevel, + sequence: '1', + }, + }); +}; + +/* +* Called from UI from "advanced" button +*/ +export const toggleAdvanced = (): Action => ({ + type: SEND.TOGGLE_ADVANCED, +}); + +/* +* Called from UI on "address" field change +*/ +export const onAddressChange = (address: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state: State = getState().sendFormRipple; + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...state, + untouched: false, + touched: { ...state.touched, address: true }, + address, + }, + }); +}; + +/* +* Called from UI on "amount" field change +*/ +export const onAmountChange = (amount: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state = getState().sendFormRipple; + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...state, + untouched: false, + touched: { ...state.touched, amount: true }, + setMax: false, + amount, + }, + }); +}; + +/* +* Called from UI from "set max" button +*/ +export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state = getState().sendFormRipple; + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...state, + untouched: false, + touched: { ...state.touched, amount: true }, + setMax: !state.setMax, + }, + }); +}; + +/* +* Called from UI on "fee" selection change +*/ +export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state = getState().sendFormRipple; + + const isCustom = feeLevel.value === 'Custom'; + let newGasLimit = state.gasLimit; + let newGasPrice = state.gasPrice; + const advanced = isCustom ? true : state.advanced; + + if (!isCustom) { + // if selected fee is not custom + // update gasLimit to default and gasPrice to selected value + const { network } = getState().selectedAccount; + if (!network) return; + const isToken = state.currency !== state.networkSymbol; + if (isToken) { + newGasLimit = network.defaultGasLimitTokens.toString(); + } else { + // corner case: gas limit was changed by user OR by "estimateGasPrice" action + // leave gasLimit as it is + newGasLimit = state.touched.gasLimit ? state.gasLimit : network.defaultGasLimit.toString(); + } + newGasPrice = feeLevel.gasPrice; + } + + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...state, + advanced, + selectedFeeLevel: feeLevel, + gasLimit: newGasLimit, + gasPrice: newGasPrice, + }, + }); +}; + +/* +* Called from UI from "update recommended fees" button +*/ +export const updateFeeLevels = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const { + account, + network, + } = getState().selectedAccount; + if (!account || !network) return; + + const state: State = getState().sendFormRipple; + const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, state.gasLimit, state.selectedFeeLevel); + const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); + + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...state, + feeLevels, + selectedFeeLevel, + gasPrice: selectedFeeLevel.gasPrice, + gasPriceNeedsUpdate: false, + }, + }); +}; + +/* +* Called from UI on "gas price" field change +*/ +export const onGasPriceChange = (gasPrice: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state: State = getState().sendFormRipple; + // switch to custom fee level + let newSelectedFeeLevel = state.selectedFeeLevel; + if (state.selectedFeeLevel.value !== 'Custom') newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom'); + + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...state, + untouched: false, + touched: { ...state.touched, gasPrice: true }, + gasPrice, + selectedFeeLevel: newSelectedFeeLevel, + }, + }); +}; + +/* +* Called from UI on "data" field change +* OR from "estimateGasPrice" action +*/ +export const onGasLimitChange = (gasLimit: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const { network } = getState().selectedAccount; + if (!network) return; + const state: State = getState().sendFormRipple; + // recalculate feeLevels with recommended gasPrice + const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, gasLimit, state.selectedFeeLevel); + const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); + + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...state, + calculatingGasLimit: false, + untouched: false, + touched: { ...state.touched, gasLimit: true }, + gasLimit, + feeLevels, + selectedFeeLevel, + }, + }); +}; + +/* +* Called from UI on "nonce" field change +*/ +export const onNonceChange = (nonce: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state: State = getState().sendFormRipple; + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...state, + untouched: false, + touched: { ...state.touched, nonce: true }, + nonce, + }, + }); +}; + +/* +* Called from UI on "data" field change +*/ +export const onDataChange = (data: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state: State = getState().sendFormRipple; + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...state, + calculatingGasLimit: true, + untouched: false, + touched: { ...state.touched, data: true }, + data, + }, + }); + + dispatch(estimateGasPrice()); +}; + +/* +* Internal method +* Called from "onDataChange" action +* try to asynchronously download data from backend +*/ +const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { + const state: State = getState().sendFormRipple; + const { network } = getState().selectedAccount; + if (!network) { + // stop "calculatingGasLimit" process + dispatch(onGasLimitChange(state.gasLimit)); + return; + } + + const requestedData = state.data; + if (!ethUtils.isHex(requestedData)) { + // stop "calculatingGasLimit" process + dispatch(onGasLimitChange(requestedData.length > 0 ? state.gasLimit : network.defaultGasLimit.toString())); + return; + } + + if (state.data.length < 1) { + // set default + dispatch(onGasLimitChange(network.defaultGasLimit.toString())); + return; + } + + const gasLimit = await dispatch(BlockchainActions.estimateGasLimit(network.shortcut, state.data, state.amount, state.gasPrice)); + + // double check "data" field + // possible race condition when data changed before backend respond + if (getState().sendFormRipple.data === requestedData) { + dispatch(onGasLimitChange(gasLimit)); + } +}; + +/* +* Called from UI from "send" button +*/ + +export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { + const { + account, + network, + } = getState().selectedAccount; + + const selected: ?TrezorDevice = getState().wallet.selectedDevice; + if (!selected) return; + + if (!account || !network) return; + + const currentState: State = getState().sendFormRipple; + + const signedTransaction = await TrezorConnect.rippleSignTransaction({ + device: { + path: selected.path, + instance: selected.instance, + state: selected.state, + }, + useEmptyPassphrase: selected.useEmptyPassphrase, + path: account.addressPath, + transaction: { + fee: '100000', + flags: 0x80000000, + sequence: account.nonce, + payment: { + amount: currentState.amount, + destination: currentState.address, + }, + }, + }); + + if (!signedTransaction || !signedTransaction.success) { + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Transaction error', + message: signedTransaction.payload.error, + cancelable: true, + actions: [], + }, + }); + return; + } + + const push = await TrezorConnect.pushTransaction({ + tx: signedTransaction.payload.serializedTx, + coin: network.shortcut, + }); + + if (!push.success) { + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Transaction error', + message: push.payload.error, + cancelable: true, + actions: [], + }, + }); + return; + } + + const { txid } = push.payload; + + dispatch({ + type: SEND.TX_COMPLETE, + account, + selectedCurrency: currentState.currency, + amount: currentState.amount, + total: currentState.total, + tx: {}, + nonce: account.nonce, + txid, + txData: {}, + }); + + // clear session storage + dispatch(SessionStorageActions.clear()); + + // reset form + dispatch(init()); + + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'success', + title: 'Transaction success', + message: See transaction detail, + cancelable: true, + actions: [], + }, + }); +}; + +export default { + toggleAdvanced, + onAddressChange, + onAmountChange, + onSetMax, + onFeeLevelChange, + updateFeeLevels, + onGasPriceChange, + onGasLimitChange, + onNonceChange, + onDataChange, + onSend, +}; \ No newline at end of file diff --git a/src/actions/ripple/RippleSendFormValidationActions.js b/src/actions/ripple/SendFormValidationActions.js similarity index 92% rename from src/actions/ripple/RippleSendFormValidationActions.js rename to src/actions/ripple/SendFormValidationActions.js index 2c39bb31..3668cb2a 100644 --- a/src/actions/ripple/RippleSendFormValidationActions.js +++ b/src/actions/ripple/SendFormValidationActions.js @@ -13,7 +13,7 @@ import type { GetState, PayloadAction, } from 'flowtype'; -import type { State, FeeLevel } from 'reducers/SendFormReducer'; +import type { State, FeeLevel } from 'reducers/SendFormRippleReducer'; // general regular expressions const RIPPLE_ADDRESS_RE = new RegExp('^r[1-9A-HJ-NP-Za-km-z]{25,34}$'); @@ -39,7 +39,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct // } // const newPrice = getRandomInt(10, 50).toString(); - const state = getState().sendForm; + const state = getState().sendFormRipple; if (network === state.networkSymbol) return; // check if new price is different then currently recommended @@ -82,7 +82,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct export const validation = (): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { // clone deep nested object // to avoid overrides across state history - let state: State = JSON.parse(JSON.stringify(getState().sendForm)); + let state: State = JSON.parse(JSON.stringify(getState().sendFormRipple)); // reset errors state.errors = {}; state.warnings = {}; @@ -108,19 +108,11 @@ export const recalculateTotalAmount = ($state: State): PayloadAction => ( if (!account) return $state; const state = { ...$state }; - const isToken = state.currency !== state.networkSymbol; if (state.setMax) { - const pendingAmount = getPendingAmount(pending, state.currency, isToken); - if (isToken) { - const token = findToken(tokens, account.address, state.currency, account.deviceState); - if (token) { - state.amount = new BigNumber(token.balance).minus(pendingAmount).toString(10); - } - } else { - const b = new BigNumber(account.balance).minus(pendingAmount); - state.amount = calculateMaxAmount(b, state.gasPrice, state.gasLimit); - } + const pendingAmount = getPendingAmount(pending, state.networkSymbol, false); + const b = new BigNumber(account.balance).minus(pendingAmount); + state.amount = calculateMaxAmount(b, state.gasPrice, state.gasLimit); } state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); @@ -153,11 +145,7 @@ export const addressValidation = ($state: State): PayloadAction => (dispa if (address.length < 1) { state.errors.address = 'Address is not set'; - } else if (network.type === 'ethereum' && !EthereumjsUtil.isValidAddress(address)) { - state.errors.address = 'Address is not valid'; - } else if (network.type === 'ethereum' && address.match(UPPERCASE_RE) && !EthereumjsUtil.isValidChecksumAddress(address)) { - state.errors.address = 'Address is not a valid checksum'; - } else if (network.type === 'ripple' && !address.match(RIPPLE_ADDRESS_RE)) { + } else if (!address.match(RIPPLE_ADDRESS_RE)) { state.errors.address = 'Address is not valid'; } return state;