1
0
mirror of https://github.com/trezor/trezor-wallet synced 2025-03-03 09:46:06 +00:00

Merge pull request #328 from trezor/fix/ripple-amount-validation

Fix/ripple amount validation
This commit is contained in:
Vladimir Volek 2019-01-15 13:38:19 +01:00 committed by GitHub
commit bb9d59bece
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 77 additions and 9 deletions

View File

@ -77,6 +77,5 @@ export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discover
networkType: 'ripple', networkType: 'ripple',
sequence: account.sequence, sequence: account.sequence,
reserve: toDecimalAmount(account.reserve, network.decimals), reserve: toDecimalAmount(account.reserve, network.decimals),
// reserve: '20',
}; };
}; };

View File

@ -60,7 +60,7 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction =
} }
if (shouldUpdate) { if (shouldUpdate) {
const validated = dispatch(ValidationActions.validation()); const validated = dispatch(ValidationActions.validation(prevState.sendFormRipple));
dispatch({ dispatch({
type: SEND.VALIDATION, type: SEND.VALIDATION,
networkType: 'ripple', networkType: 'ripple',

View File

@ -1,5 +1,5 @@
/* @flow */ /* @flow */
import TrezorConnect from 'trezor-connect';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import * as SEND from 'actions/constants/send'; import * as SEND from 'actions/constants/send';
import { findDevice, getPendingAmount } from 'reducers/utils'; import { findDevice, getPendingAmount } from 'reducers/utils';
@ -9,6 +9,7 @@ import type {
Dispatch, Dispatch,
GetState, GetState,
PayloadAction, PayloadAction,
PromiseAction,
BlockchainFeeLevel, BlockchainFeeLevel,
} from 'flowtype'; } from 'flowtype';
import type { State, FeeLevel } from 'reducers/SendFormRippleReducer'; import type { State, FeeLevel } from 'reducers/SendFormRippleReducer';
@ -59,7 +60,7 @@ export const onFeeUpdated = (network: string, feeLevels: Array<BlockchainFeeLeve
/* /*
* Recalculate amount, total and fees * Recalculate amount, total and fees
*/ */
export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => { export const validation = (prevState: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
// clone deep nested object // clone deep nested object
// to avoid overrides across state history // to avoid overrides across state history
let state: State = JSON.parse(JSON.stringify(getState().sendFormRipple)); let state: State = JSON.parse(JSON.stringify(getState().sendFormRipple));
@ -74,6 +75,9 @@ export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getSt
state = dispatch(amountValidation(state)); state = dispatch(amountValidation(state));
state = dispatch(feeValidation(state)); state = dispatch(feeValidation(state));
state = dispatch(destinationTagValidation(state)); state = dispatch(destinationTagValidation(state));
if (state.touched.address && prevState.address !== state.address) {
dispatch(addressBalanceValidation(state));
}
return state; return state;
}; };
@ -83,14 +87,14 @@ const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatc
network, network,
pending, pending,
} = getState().selectedAccount; } = getState().selectedAccount;
if (!account || !network) return $state; if (!account || account.networkType !== 'ripple' || !network) return $state;
const state = { ...$state }; const state = { ...$state };
const fee = toDecimalAmount(state.selectedFeeLevel.fee, network.decimals); const fee = toDecimalAmount(state.selectedFeeLevel.fee, network.decimals);
if (state.setMax) { if (state.setMax) {
const pendingAmount = getPendingAmount(pending, state.networkSymbol, false); const pendingAmount = getPendingAmount(pending, state.networkSymbol, false);
const availableBalance = new BigNumber(account.balance).minus(pendingAmount); const availableBalance = new BigNumber(account.balance).minus(account.reserve).minus(pendingAmount);
state.amount = calculateMaxAmount(availableBalance, fee); state.amount = calculateMaxAmount(availableBalance, fee);
} }
@ -135,6 +139,43 @@ const addressValidation = ($state: State): PayloadAction<State> => (dispatch: Di
return state; return state;
}; };
/*
* Address balance validation
* Fetch data from trezor-connect and set minimum required amount in reducer
*/
const addressBalanceValidation = ($state: State): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
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 * Address label assignation
*/ */
@ -184,7 +225,7 @@ const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dis
account, account,
pending, pending,
} = getState().selectedAccount; } = getState().selectedAccount;
if (!account) return state; if (!account || account.networkType !== 'ripple') return state;
const { amount } = state; const { amount } = state;
if (amount.length < 1) { if (amount.length < 1) {
@ -193,13 +234,21 @@ const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dis
state.errors.amount = 'Amount is not a number'; state.errors.amount = 'Amount is not a number';
} else { } else {
const pendingAmount: BigNumber = getPendingAmount(pending, state.networkSymbol); const pendingAmount: BigNumber = getPendingAmount(pending, state.networkSymbol);
if (!state.amount.match(XRP_6_RE)) { if (!state.amount.match(XRP_6_RE)) {
state.errors.amount = 'Maximum 6 decimals allowed'; state.errors.amount = 'Maximum 6 decimals allowed';
} else if (new BigNumber(state.total).greaterThan(new BigNumber(account.balance).minus(pendingAmount))) { } else if (new BigNumber(state.total).greaterThan(new BigNumber(account.balance).minus(pendingAmount))) {
state.errors.amount = 'Not enough funds'; 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; return state;
}; };

View File

@ -21,6 +21,7 @@ export type State = {
touched: {[k: string]: boolean}; touched: {[k: string]: boolean};
address: string; address: string;
amount: string; amount: string;
minAmount: string;
setMax: boolean; setMax: boolean;
feeLevels: Array<FeeLevel>; feeLevels: Array<FeeLevel>;
selectedFeeLevel: FeeLevel; selectedFeeLevel: FeeLevel;
@ -47,6 +48,7 @@ export const initialState: State = {
touched: {}, touched: {},
address: '', address: '',
amount: '', amount: '',
minAmount: '0',
setMax: false, setMax: false,
feeLevels: [], feeLevels: [],
selectedFeeLevel: { selectedFeeLevel: {

View File

@ -22,6 +22,16 @@ import type { Props } from './Container';
// and put it inside config/variables.js // and put it inside config/variables.js
const SmallScreenWidth = '850px'; const SmallScreenWidth = '850px';
const AmountInputLabelWrapper = styled.div`
display: flex;
justify-content: space-between;
`;
const AmountInputLabel = styled.span`
text-align: right;
color: ${colors.TEXT_SECONDARY};
`;
const InputRow = styled.div` const InputRow = styled.div`
padding-bottom: 28px; padding-bottom: 28px;
`; `;
@ -228,6 +238,7 @@ const AccountSend = (props: Props) => {
const tokensSelectData: Array<{ value: string, label: string }> = [{ value: network.symbol, label: network.symbol }]; const tokensSelectData: Array<{ value: string, label: string }> = [{ value: network.symbol, label: network.symbol }];
const tokensSelectValue = tokensSelectData[0]; const tokensSelectValue = tokensSelectData[0];
const isAdvancedSettingsHidden = !advanced; const isAdvancedSettingsHidden = !advanced;
const accountReserve: ?string = account.networkType === 'ripple' && !account.empty ? account.reserve : null;
return ( return (
<Content> <Content>
@ -252,7 +263,14 @@ const AccountSend = (props: Props) => {
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
spellCheck="false" spellCheck="false"
topLabel="Amount" topLabel={(
<AmountInputLabelWrapper>
<AmountInputLabel>Amount</AmountInputLabel>
{accountReserve && (
<AmountInputLabel>Reserve: {accountReserve} {network.symbol}</AmountInputLabel>
)}
</AmountInputLabelWrapper>
)}
value={amount} value={amount}
onChange={event => onAmountChange(event.target.value)} onChange={event => onAmountChange(event.target.value)}
bottomText={errors.amount || warnings.amount || infos.amount} bottomText={errors.amount || warnings.amount || infos.amount}