diff --git a/src/actions/ripple/SendFormActions.js b/src/actions/ripple/SendFormActions.js index ebe3fdc2..562326a4 100644 --- a/src/actions/ripple/SendFormActions.js +++ b/src/actions/ripple/SendFormActions.js @@ -60,7 +60,7 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction = } if (shouldUpdate) { - const validated = dispatch(ValidationActions.validation()); + const validated = dispatch(ValidationActions.validation(prevState.sendFormRipple)); dispatch({ type: SEND.VALIDATION, networkType: 'ripple', diff --git a/src/actions/ripple/SendFormValidationActions.js b/src/actions/ripple/SendFormValidationActions.js index 70ff0c54..4741ed49 100644 --- a/src/actions/ripple/SendFormValidationActions.js +++ b/src/actions/ripple/SendFormValidationActions.js @@ -1,5 +1,5 @@ /* @flow */ - +import TrezorConnect from 'trezor-connect'; import BigNumber from 'bignumber.js'; import * as SEND from 'actions/constants/send'; import { findDevice, getPendingAmount } from 'reducers/utils'; @@ -9,6 +9,7 @@ import type { Dispatch, GetState, PayloadAction, + PromiseAction, BlockchainFeeLevel, } from 'flowtype'; import type { State, FeeLevel } from 'reducers/SendFormRippleReducer'; @@ -59,7 +60,7 @@ export const onFeeUpdated = (network: string, feeLevels: Array => (dispatch: Dispatch, getState: GetState): State => { +export const validation = (prevState: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { // clone deep nested object // to avoid overrides across state history let state: State = JSON.parse(JSON.stringify(getState().sendFormRipple)); @@ -74,6 +75,9 @@ export const validation = (): PayloadAction => (dispatch: Dispatch, getSt state = dispatch(amountValidation(state)); state = dispatch(feeValidation(state)); state = dispatch(destinationTagValidation(state)); + if (state.touched.address && prevState.address !== state.address) { + dispatch(addressBalanceValidation(state)); + } return state; }; @@ -135,6 +139,43 @@ const addressValidation = ($state: State): PayloadAction => (dispatch: Di return state; }; +/* +* Address balance validation +* Fetch data from trezor-connect and set minimum required amount in reducer +*/ +const addressBalanceValidation = ($state: State): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { + const { network } = getState().selectedAccount; + if (!network) return; + + let minAmount: string = '0'; + const response = await TrezorConnect.rippleGetAccountInfo({ + account: { + descriptor: $state.address, + }, + coin: network.shortcut, + }); + if (response.success) { + const empty = response.payload.sequence <= 0 && response.payload.balance === '0'; + if (empty) { + minAmount = toDecimalAmount(response.payload.reserve, network.decimals); + } + } + + // TODO: consider checking local (known) accounts reserve instead of async fetching + + // a2 (not empty): rJX2KwzaLJDyFhhtXKi3htaLfaUH2tptEX + // a4 (empty): r9skfe7kZkvqss7oMB3tuj4a59PXD5wRa2 + + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...getState().sendFormRipple, + minAmount, + }, + }); +}; + /* * Address label assignation */ @@ -193,13 +234,21 @@ const amountValidation = ($state: State): PayloadAction => (dispatch: Dis state.errors.amount = 'Amount is not a number'; } else { const pendingAmount: BigNumber = getPendingAmount(pending, state.networkSymbol); - 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'; } } + + if (!state.errors.amount && new BigNumber(account.balance).minus(state.total).lt(account.reserve)) { + state.errors.amount = `Not enough funds. Reserved amount for this account is ${account.reserve} ${state.networkSymbol}`; + } + + if (!state.errors.amount && new BigNumber(state.amount).lt(state.minAmount)) { + state.errors.amount = `Amount is too low. Minimum amount for creating a new account is ${state.minAmount} ${state.networkSymbol}`; + } + return state; };