From bfebef73c77073c117c091fb2dc467b628f65d04 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Mon, 3 Dec 2018 21:01:33 +0100 Subject: [PATCH] ripple/SendFormValidations --- src/actions/ripple/SendFormActions.js | 14 +- .../ripple/SendFormValidationActions.js | 280 ++---------------- .../Wallet/views/Account/Send/ripple/index.js | 2 - 3 files changed, 40 insertions(+), 256 deletions(-) diff --git a/src/actions/ripple/SendFormActions.js b/src/actions/ripple/SendFormActions.js index 8cf9e2aa..df241259 100644 --- a/src/actions/ripple/SendFormActions.js +++ b/src/actions/ripple/SendFormActions.js @@ -4,7 +4,7 @@ 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 * as BLOCKCHAIN from 'actions/constants/blockchain'; import { initialState } from 'reducers/SendFormRippleReducer'; import * as reducerUtils from 'reducers/utils'; import { fromDecimalAmount } from 'utils/formatUtils'; @@ -57,8 +57,8 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction = // handle gasPrice update from backend // recalculate fee levels if needed - if (action.type === WEB3.GAS_PRICE_UPDATED) { - dispatch(ValidationActions.onGasPriceUpdated(action.network, action.gasPrice)); + if (action.type === BLOCKCHAIN.UPDATE_FEE) { + // dispatch(ValidationActions.onGasPriceUpdated(action.network, action.gasPrice)); return; } @@ -107,7 +107,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS return; } - const feeLevels = ValidationActions.getFeeLevels(network.symbol, '1', '1'); + const feeLevels = ValidationActions.getFeeLevels(network.symbol); const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); dispatch({ @@ -191,6 +191,9 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge if (!account || !network) return; + const blockchain = getState().blockchain.find(b => b.shortcut === account.network); + if (!blockchain) return; + const currentState: State = getState().sendFormRipple; const amount = fromDecimalAmount(currentState.amount, 6); @@ -203,8 +206,7 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge useEmptyPassphrase: selected.useEmptyPassphrase, path: account.addressPath, transaction: { - // fee: '12', - fee: '10', // Fee must be in the range of 10 to 10,000 drops + fee: blockchain.fee, // Fee must be in the range of 10 to 10,000 drops flags: 0x80000000, sequence: account.sequence, payment: { diff --git a/src/actions/ripple/SendFormValidationActions.js b/src/actions/ripple/SendFormValidationActions.js index 3668cb2a..2b4f194d 100644 --- a/src/actions/ripple/SendFormValidationActions.js +++ b/src/actions/ripple/SendFormValidationActions.js @@ -1,12 +1,8 @@ /* @flow */ import BigNumber from 'bignumber.js'; -import EthereumjsUtil from 'ethereumjs-util'; -import EthereumjsUnits from 'ethereumjs-units'; -import { findToken } from 'reducers/TokensReducer'; import { findDevice, getPendingAmount } from 'reducers/utils'; -import * as SEND from 'actions/constants/send'; -import * as ethUtils from 'utils/ethUtils'; +import { toDecimalAmount } from 'utils/formatUtils'; import type { Dispatch, @@ -16,65 +12,9 @@ import type { 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}$'); +const XRP_ADDRESS_RE = new RegExp('^r[1-9A-HJ-NP-Za-km-z]{25,34}$'); const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$'); -const UPPERCASE_RE = new RegExp('^(.*[A-Z].*)$'); -const ABS_RE = new RegExp('^[0-9]+$'); -const ETH_18_RE = new RegExp('^(0|0\\.([0-9]{0,18})?|[1-9][0-9]*\\.?([0-9]{0,18})?|\\.[0-9]{0,18})$'); -const dynamicRegexp = (decimals: number): RegExp => { - if (decimals > 0) { - return new RegExp(`^(0|0\\.([0-9]{0,${decimals}})?|[1-9][0-9]*\\.?([0-9]{0,${decimals}})?|\\.[0-9]{1,${decimals}})$`); - } - return ABS_RE; -}; - -/* -* Called from SendFormActions.observe -* Reaction for WEB3.GAS_PRICE_UPDATED action -*/ -export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAction => (dispatch: Dispatch, getState: GetState): void => { - // testing random data - // function getRandomInt(min, max) { - // return Math.floor(Math.random() * (max - min + 1)) + min; - // } - // const newPrice = getRandomInt(10, 50).toString(); - - const state = getState().sendFormRipple; - if (network === state.networkSymbol) return; - - // check if new price is different then currently recommended - const newPrice: string = EthereumjsUnits.convert(gasPrice, 'wei', 'gwei'); - - if (newPrice !== state.recommendedGasPrice) { - if (!state.untouched) { - // if there is a transaction draft let the user know - // and let him update manually - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - gasPriceNeedsUpdate: true, - recommendedGasPrice: newPrice, - }, - }); - } else { - // automatically update feeLevels and gasPrice - const feeLevels = getFeeLevels(state.networkSymbol, newPrice, state.gasLimit); - const selectedFeeLevel = getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); - dispatch({ - type: SEND.CHANGE, - state: { - ...state, - gasPriceNeedsUpdate: false, - recommendedGasPrice: newPrice, - gasPrice: selectedFeeLevel.gasPrice, - feeLevels, - selectedFeeLevel, - }, - }); - } - } -}; +const XRP_6_RE = new RegExp('^(0|0\\.([0-9]{0,6})?|[1-9][0-9]*\\.?([0-9]{0,6})?|\\.[0-9]{0,6})$'); /* * Recalculate amount, total and fees @@ -88,65 +28,53 @@ export const validation = (): PayloadAction => (dispatch: Dispatch, getSt state.warnings = {}; state.infos = {}; state = dispatch(recalculateTotalAmount(state)); - state = dispatch(updateCustomFeeLabel(state)); state = dispatch(addressValidation(state)); state = dispatch(addressLabel(state)); state = dispatch(amountValidation(state)); - state = dispatch(gasLimitValidation(state)); - state = dispatch(gasPriceValidation(state)); - state = dispatch(nonceValidation(state)); - state = dispatch(dataValidation(state)); return state; }; -export const recalculateTotalAmount = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { +const recalculateTotalAmount = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { const { account, - tokens, pending, } = getState().selectedAccount; if (!account) return $state; + const blockchain = getState().blockchain.find(b => b.shortcut === account.network); + if (!blockchain) return $state; + const fee = toDecimalAmount(blockchain.fee, 6); + const state = { ...$state }; if (state.setMax) { const pendingAmount = getPendingAmount(pending, state.networkSymbol, false); const b = new BigNumber(account.balance).minus(pendingAmount); - state.amount = calculateMaxAmount(b, state.gasPrice, state.gasLimit); + state.amount = calculateMaxAmount(b, fee); } - state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); - return state; -}; - -export const updateCustomFeeLabel = ($state: State): PayloadAction => (): State => { - const state = { ...$state }; - if ($state.selectedFeeLevel.value === 'Custom') { - state.selectedFeeLevel = { - ...state.selectedFeeLevel, - gasPrice: state.gasPrice, - label: `${calculateFee(state.gasPrice, state.gasLimit)} ${state.networkSymbol}`, - }; - } + state.total = calculateTotal(state.amount, fee); return state; }; /* * Address value validation */ -export const addressValidation = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { +const addressValidation = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { const state = { ...$state }; if (!state.touched.address) return state; - const { network } = getState().selectedAccount; - if (!network) return state; + const { account, network } = getState().selectedAccount; + if (!account || !network) return state; const { address } = state; if (address.length < 1) { state.errors.address = 'Address is not set'; - } else if (!address.match(RIPPLE_ADDRESS_RE)) { + } else if (!address.match(XRP_ADDRESS_RE)) { state.errors.address = 'Address is not valid'; + } else if (address.toLowerCase() === account.address.toLowerCase()) { + state.errors.address = 'Cannot send to myself'; } return state; }; @@ -154,7 +82,7 @@ export const addressValidation = ($state: State): PayloadAction => (dispa /* * Address label assignation */ -export const addressLabel = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { +const addressLabel = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { const state = { ...$state }; if (!state.touched.address || state.errors.address) return state; @@ -192,13 +120,12 @@ export const addressLabel = ($state: State): PayloadAction => (dispatch: /* * Amount value validation */ -export const amountValidation = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { +const amountValidation = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { const state = { ...$state }; if (!state.touched.amount) return state; const { account, - tokens, pending, } = getState().selectedAccount; if (!account) return state; @@ -209,25 +136,10 @@ export const amountValidation = ($state: State): PayloadAction => (dispat } else if (amount.length > 0 && !amount.match(NUMBER_RE)) { state.errors.amount = 'Amount is not a number'; } else { - const isToken: boolean = state.currency !== state.networkSymbol; - const pendingAmount: BigNumber = getPendingAmount(pending, state.currency, isToken); + const pendingAmount: BigNumber = getPendingAmount(pending, state.networkSymbol); - if (isToken) { - const token = findToken(tokens, account.address, state.currency, account.deviceState); - if (!token) return state; - const decimalRegExp = dynamicRegexp(parseInt(token.decimals, 0)); - - if (!state.amount.match(decimalRegExp)) { - state.errors.amount = `Maximum ${token.decimals} decimals allowed`; - } else if (new BigNumber(state.total).greaterThan(account.balance)) { - state.errors.amount = `Not enough ${state.networkSymbol} to cover transaction fee`; - } else if (new BigNumber(state.amount).greaterThan(new BigNumber(token.balance).minus(pendingAmount))) { - state.errors.amount = 'Not enough funds'; - } else if (new BigNumber(state.amount).lessThanOrEqualTo('0')) { - state.errors.amount = 'Amount is too low'; - } - } else if (!state.amount.match(ETH_18_RE)) { - state.errors.amount = 'Maximum 18 decimals allowed'; + if (!state.amount.match(XRP_6_RE)) { + state.errors.amount = 'Maximum 6 decimals allowed'; } else if (new BigNumber(state.total).greaterThan(new BigNumber(account.balance).minus(pendingAmount))) { state.errors.amount = 'Not enough funds'; } @@ -235,121 +147,21 @@ export const amountValidation = ($state: State): PayloadAction => (dispat return state; }; -/* -* Gas limit value validation -*/ -export const gasLimitValidation = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { - const state = { ...$state }; - if (!state.touched.gasLimit) return state; - - const { - network, - } = getState().selectedAccount; - if (!network) return state; - - const { gasLimit } = state; - if (gasLimit.length < 1) { - state.errors.gasLimit = 'Gas limit is not set'; - } else if (gasLimit.length > 0 && !gasLimit.match(NUMBER_RE)) { - state.errors.gasLimit = 'Gas limit is not a number'; - } else { - const gl: BigNumber = new BigNumber(gasLimit); - if (gl.lessThan(1)) { - state.errors.gasLimit = 'Gas limit is too low'; - } else if (gl.lessThan(state.currency !== state.networkSymbol ? network.defaultGasLimitTokens : network.defaultGasLimit)) { - state.warnings.gasLimit = 'Gas limit is below recommended'; - } - } - return state; -}; - -/* -* Gas price value validation -*/ -export const gasPriceValidation = ($state: State): PayloadAction => (): State => { - const state = { ...$state }; - if (!state.touched.gasPrice) return state; - - const { gasPrice } = state; - if (gasPrice.length < 1) { - state.errors.gasPrice = 'Gas price is not set'; - } else if (gasPrice.length > 0 && !gasPrice.match(NUMBER_RE)) { - state.errors.gasPrice = 'Gas price is not a number'; - } else { - const gp: BigNumber = new BigNumber(gasPrice); - if (gp.greaterThan(1000)) { - state.warnings.gasPrice = 'Gas price is too high'; - } else if (gp.lessThanOrEqualTo('0')) { - state.errors.gasPrice = 'Gas price is too low'; - } - } - return state; -}; - -/* -* Nonce value validation -*/ -export const nonceValidation = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { - const state = { ...$state }; - if (!state.touched.nonce) return state; - - const { - account, - } = getState().selectedAccount; - if (!account) return state; - - const { nonce } = state; - if (nonce.length < 1) { - state.errors.nonce = 'Nonce is not set'; - } else if (!nonce.match(ABS_RE)) { - state.errors.nonce = 'Nonce is not a valid number'; - } else { - const n: BigNumber = new BigNumber(nonce); - if (n.lessThan(account.nonce)) { - state.warnings.nonce = 'Nonce is lower than recommended'; - } else if (n.greaterThan(account.nonce)) { - state.warnings.nonce = 'Nonce is greater than recommended'; - } - } - return state; -}; - -/* -* Gas price value validation -*/ -export const dataValidation = ($state: State): PayloadAction => (): State => { - const state = { ...$state }; - if (!state.touched.data || state.data.length === 0) return state; - if (!ethUtils.isHex(state.data)) { - state.errors.data = 'Data is not valid hexadecimal'; - } - return state; -}; - /* * UTILITIES */ -export const calculateFee = (gasPrice: string, gasLimit: string): string => { +const calculateTotal = (amount: string, fee: string): string => { try { - return EthereumjsUnits.convert(new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether'); + return new BigNumber(amount).plus(fee).toString(10); } catch (error) { return '0'; } }; -export const calculateTotal = (amount: string, gasPrice: string, gasLimit: string): string => { - try { - return new BigNumber(amount).plus(calculateFee(gasPrice, gasLimit)).toString(10); - } catch (error) { - return '0'; - } -}; - -export const calculateMaxAmount = (balance: BigNumber, gasPrice: string, gasLimit: string): string => { +const calculateMaxAmount = (balance: BigNumber, fee: string): string => { try { // TODO - minus pendings - const fee = calculateFee(gasPrice, gasLimit); const max = balance.minus(fee); if (max.lessThan(0)) return '0'; return max.toString(10); @@ -358,42 +170,14 @@ export const calculateMaxAmount = (balance: BigNumber, gasPrice: string, gasLimi } }; -export const getFeeLevels = (symbol: string, gasPrice: BigNumber | string, gasLimit: string, selected?: FeeLevel): Array => { - const price: BigNumber = typeof gasPrice === 'string' ? new BigNumber(gasPrice) : gasPrice; - const quarter: BigNumber = price.dividedBy(4); - const high: string = price.plus(quarter.times(2)).toString(10); - const low: string = price.minus(quarter.times(2)).toString(10); +export const getFeeLevels = (shortcut: string): Array => ([ + { + value: 'Normal', + gasPrice: '1', + label: `1 ${shortcut}`, + }, +]); - const customLevel: FeeLevel = selected && selected.value === 'Custom' ? { - value: 'Custom', - gasPrice: selected.gasPrice, - // label: `${ calculateFee(gasPrice, gasLimit) } ${ symbol }` - label: `${calculateFee(selected.gasPrice, gasLimit)} ${symbol}`, - } : { - value: 'Custom', - gasPrice: low, - label: '', - }; - - return [ - { - value: 'High', - gasPrice: high, - label: `${calculateFee(high, gasLimit)} ${symbol}`, - }, - { - value: 'Normal', - gasPrice: gasPrice.toString(), - label: `${calculateFee(price.toString(10), gasLimit)} ${symbol}`, - }, - { - value: 'Low', - gasPrice: low, - label: `${calculateFee(low, gasLimit)} ${symbol}`, - }, - customLevel, - ]; -}; export const getSelectedFeeLevel = (feeLevels: Array, selected: FeeLevel): FeeLevel => { const { value } = selected; @@ -404,4 +188,4 @@ export const getSelectedFeeLevel = (feeLevels: Array, selected: FeeLev selectedFeeLevel = feeLevels.find(f => f.value === 'Normal'); } return selectedFeeLevel || selected; -}; \ No newline at end of file +}; diff --git a/src/views/Wallet/views/Account/Send/ripple/index.js b/src/views/Wallet/views/Account/Send/ripple/index.js index e63d9eca..ea07f40f 100644 --- a/src/views/Wallet/views/Account/Send/ripple/index.js +++ b/src/views/Wallet/views/Account/Send/ripple/index.js @@ -160,8 +160,6 @@ const AccountSend = (props: Props) => { isSendButtonDisabled = true; } - isSendButtonDisabled = false; - const tokensSelectData: Array<{ value: string, label: string }> = [{ value: network.symbol, label: network.symbol }]; const tokensSelectValue = tokensSelectData[0]; const isAdvancedSettingsHidden = !advanced;