1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-11 19:09:10 +00:00
trezor-wallet/src/actions/ripple/SendFormValidationActions.js

361 lines
12 KiB
JavaScript
Raw Normal View History

2018-09-22 16:49:05 +00:00
/* @flow */
import TrezorConnect from 'trezor-connect';
2018-09-22 16:49:05 +00:00
import BigNumber from 'bignumber.js';
2018-12-05 13:12:23 +00:00
import * as SEND from 'actions/constants/send';
2018-09-22 16:49:05 +00:00
import { findDevice, getPendingAmount } from 'reducers/utils';
2018-12-03 20:01:33 +00:00
import { toDecimalAmount } from 'utils/formatUtils';
2018-09-22 16:49:05 +00:00
import type {
Dispatch,
GetState,
PayloadAction,
PromiseAction,
2018-12-28 15:15:18 +00:00
BlockchainFeeLevel,
2018-09-22 16:49:05 +00:00
} from 'flowtype';
2018-11-29 20:02:56 +00:00
import type { State, FeeLevel } from 'reducers/SendFormRippleReducer';
2018-09-22 16:49:05 +00:00
import AddressValidator from 'wallet-address-validator';
2018-09-22 16:49:05 +00:00
// general regular expressions
2018-12-28 15:15:18 +00:00
const ABS_RE = new RegExp('^[0-9]+$');
2018-09-22 16:49:05 +00:00
const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$');
2018-12-03 20:01:33 +00:00
const XRP_6_RE = new RegExp('^(0|0\\.([0-9]{0,6})?|[1-9][0-9]*\\.?([0-9]{0,6})?|\\.[0-9]{0,6})$');
2018-09-22 16:49:05 +00:00
2018-12-05 13:12:23 +00:00
/*
* Called from SendFormActions.observe
* Reaction for BLOCKCHAIN.FEE_UPDATED action
*/
2018-12-28 15:15:18 +00:00
export const onFeeUpdated = (network: string, feeLevels: Array<BlockchainFeeLevel>): PayloadAction<void> => (dispatch: Dispatch, getState: GetState): void => {
2018-12-05 13:12:23 +00:00
const state = getState().sendFormRipple;
if (network === state.networkSymbol) return;
if (!state.untouched) {
// if there is a transaction draft let the user know
// and let him update manually
dispatch({
type: SEND.CHANGE,
networkType: 'ripple',
state: {
...state,
feeNeedsUpdate: true,
},
});
return;
}
2018-12-28 15:15:18 +00:00
// automatically update feeLevels
const newFeeLevels = dispatch(getFeeLevels(feeLevels));
const selectedFeeLevel = getSelectedFeeLevel(newFeeLevels, state.selectedFeeLevel);
2018-12-05 13:12:23 +00:00
dispatch({
type: SEND.CHANGE,
networkType: 'ripple',
state: {
...state,
feeNeedsUpdate: false,
2018-12-28 15:15:18 +00:00
feeLevels: newFeeLevels,
2018-12-05 13:12:23 +00:00
selectedFeeLevel,
},
});
};
2018-09-22 16:49:05 +00:00
/*
* Recalculate amount, total and fees
*/
export const validation = (prevState: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
2018-09-22 16:49:05 +00:00
// clone deep nested object
// to avoid overrides across state history
2018-11-29 20:02:56 +00:00
let state: State = JSON.parse(JSON.stringify(getState().sendFormRipple));
2018-09-22 16:49:05 +00:00
// reset errors
state.errors = {};
state.warnings = {};
state.infos = {};
2018-12-28 15:15:18 +00:00
state = dispatch(updateCustomFeeLabel(state));
2019-01-02 10:34:36 +00:00
state = dispatch(recalculateTotalAmount(state));
2018-09-22 16:49:05 +00:00
state = dispatch(addressValidation(state));
state = dispatch(addressLabel(state));
state = dispatch(amountValidation(state));
2018-12-28 15:15:18 +00:00
state = dispatch(feeValidation(state));
state = dispatch(destinationTagValidation(state));
if (state.touched.address && prevState.address !== state.address) {
dispatch(addressBalanceValidation(state));
}
2018-09-22 16:49:05 +00:00
return state;
};
2018-12-03 20:01:33 +00:00
const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
2018-09-22 16:49:05 +00:00
const {
account,
2019-01-02 10:34:36 +00:00
network,
2018-09-22 16:49:05 +00:00
pending,
} = getState().selectedAccount;
2019-01-12 18:31:42 +00:00
if (!account || account.networkType !== 'ripple' || !network) return $state;
2018-09-22 16:49:05 +00:00
const state = { ...$state };
2019-01-02 10:34:36 +00:00
const fee = toDecimalAmount(state.selectedFeeLevel.fee, network.decimals);
2018-09-22 16:49:05 +00:00
if (state.setMax) {
2018-11-29 20:02:56 +00:00
const pendingAmount = getPendingAmount(pending, state.networkSymbol, false);
2019-01-12 18:31:42 +00:00
const availableBalance = new BigNumber(account.balance).minus(account.reserve).minus(pendingAmount);
2019-01-02 10:34:36 +00:00
state.amount = calculateMaxAmount(availableBalance, fee);
2018-09-22 16:49:05 +00:00
}
2019-01-02 10:34:36 +00:00
state.total = calculateTotal(state.amount, fee);
2018-12-28 15:15:18 +00:00
return state;
};
2019-01-02 10:34:36 +00:00
const updateCustomFeeLabel = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
const { network } = getState().selectedAccount;
if (!network) return $state; // flowtype fallback
2018-12-28 15:15:18 +00:00
const state = { ...$state };
if ($state.selectedFeeLevel.value === 'Custom') {
state.selectedFeeLevel = {
...state.selectedFeeLevel,
fee: state.fee,
2019-01-02 10:34:36 +00:00
label: `${toDecimalAmount(state.fee, network.decimals)} ${state.networkSymbol}`,
2018-12-28 15:15:18 +00:00
};
}
2018-09-22 16:49:05 +00:00
return state;
};
/*
* Address value validation
*/
2018-12-03 20:01:33 +00:00
const addressValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
2018-09-22 16:49:05 +00:00
const state = { ...$state };
if (!state.touched.address) return state;
2018-12-03 20:01:33 +00:00
const { account, network } = getState().selectedAccount;
if (!account || !network) return state;
2018-11-29 20:02:56 +00:00
2018-09-22 16:49:05 +00:00
const { address } = state;
if (address.length < 1) {
state.errors.address = 'Address is not set';
} else if (!AddressValidator.validate(address, 'XRP')) {
2018-09-22 16:49:05 +00:00
state.errors.address = 'Address is not valid';
} else if (address.toLowerCase() === account.descriptor.toLowerCase()) {
2018-12-03 20:01:33 +00:00
state.errors.address = 'Cannot send to myself';
2018-09-22 16:49:05 +00:00
}
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,
},
});
};
2018-09-22 16:49:05 +00:00
/*
* Address label assignation
*/
2018-12-03 20:01:33 +00:00
const addressLabel = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
2018-09-22 16:49:05 +00:00
const state = { ...$state };
if (!state.touched.address || state.errors.address) return state;
const {
account,
network,
} = getState().selectedAccount;
if (!account || !network) return state;
const { address } = state;
const savedAccounts = getState().accounts.filter(a => a.descriptor.toLowerCase() === address.toLowerCase());
2018-09-22 16:49:05 +00:00
if (savedAccounts.length > 0) {
// check if found account belongs to this network
2018-10-15 13:44:10 +00:00
const currentNetworkAccount = savedAccounts.find(a => a.network === network.shortcut);
2018-09-22 16:49:05 +00:00
if (currentNetworkAccount) {
const device = findDevice(getState().devices, currentNetworkAccount.deviceID, currentNetworkAccount.deviceState);
if (device) {
state.infos.address = `${device.instanceLabel} Account #${(currentNetworkAccount.index + 1)}`;
}
} else {
// corner-case: the same derivation path is used on different networks
const otherNetworkAccount = savedAccounts[0];
const device = findDevice(getState().devices, otherNetworkAccount.deviceID, otherNetworkAccount.deviceState);
2018-10-15 13:44:10 +00:00
const { networks } = getState().localStorage.config;
const otherNetwork = networks.find(c => c.shortcut === otherNetworkAccount.network);
2018-09-22 16:49:05 +00:00
if (device && otherNetwork) {
state.warnings.address = `Looks like it's ${device.instanceLabel} Account #${(otherNetworkAccount.index + 1)} address of ${otherNetwork.name} network`;
}
}
}
return state;
};
/*
* Amount value validation
*/
2018-12-03 20:01:33 +00:00
const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
2018-09-22 16:49:05 +00:00
const state = { ...$state };
if (!state.touched.amount) return state;
const {
account,
pending,
} = getState().selectedAccount;
2019-01-11 16:55:36 +00:00
if (!account || account.networkType !== 'ripple') return state;
2018-09-22 16:49:05 +00:00
const { amount } = state;
if (amount.length < 1) {
state.errors.amount = 'Amount is not set';
} else if (amount.length > 0 && !amount.match(NUMBER_RE)) {
state.errors.amount = 'Amount is not a number';
} else {
2018-12-03 20:01:33 +00:00
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).isGreaterThan(new BigNumber(account.balance).minus(pendingAmount))) {
2018-09-22 16:49:05 +00:00
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}`;
}
2018-09-22 16:49:05 +00:00
return state;
};
2018-12-28 15:15:18 +00:00
/*
* Fee value validation
*/
export const feeValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
const state = { ...$state };
if (!state.touched.fee) return state;
const {
network,
} = getState().selectedAccount;
if (!network) return state;
const { fee } = state;
if (fee.length < 1) {
state.errors.fee = 'Fee is not set';
} else if (fee.length > 0 && !fee.match(ABS_RE)) {
state.errors.fee = 'Fee must be an absolute number';
} else {
const gl: BigNumber = new BigNumber(fee);
if (gl.isLessThan(network.fee.minFee)) {
2018-12-28 15:15:18 +00:00
state.errors.fee = 'Fee is below recommended';
} else if (gl.isGreaterThan(network.fee.maxFee)) {
2019-01-03 14:37:08 +00:00
state.errors.fee = 'Fee is above recommended';
2018-12-28 15:15:18 +00:00
}
}
return state;
};
/*
* Destination Tag value validation
*/
export const destinationTagValidation = ($state: State): PayloadAction<State> => (): State => {
const state = { ...$state };
if (!state.touched.destinationTag) return state;
const { destinationTag } = state;
if (destinationTag.length > 0 && !destinationTag.match(ABS_RE)) {
state.errors.destinationTag = 'Destination tag must be an absolute number';
}
return state;
};
2018-12-28 15:15:18 +00:00
2018-09-22 16:49:05 +00:00
/*
* UTILITIES
*/
2018-12-03 20:01:33 +00:00
const calculateTotal = (amount: string, fee: string): string => {
2018-09-22 16:49:05 +00:00
try {
const bAmount = new BigNumber(amount);
// BigNumber() returns NaN on non-numeric string
if (bAmount.isNaN()) {
throw new Error('Amount is not a number');
}
return bAmount.plus(fee).toFixed();
2018-09-22 16:49:05 +00:00
} catch (error) {
return '0';
}
};
2018-12-03 20:01:33 +00:00
const calculateMaxAmount = (balance: BigNumber, fee: string): string => {
2018-09-22 16:49:05 +00:00
try {
// TODO - minus pendings
const max = balance.minus(fee);
if (max.isLessThan(0)) return '0';
2019-01-08 10:50:30 +00:00
return max.toFixed();
2018-09-22 16:49:05 +00:00
} catch (error) {
return '0';
}
};
2018-12-28 15:15:18 +00:00
// Generate FeeLevel dataset for "fee" select
export const getFeeLevels = (feeLevels: Array<BlockchainFeeLevel>, selected?: FeeLevel): PayloadAction<Array<FeeLevel>> => (dispatch: Dispatch, getState: GetState): Array<FeeLevel> => {
const { network } = getState().selectedAccount;
if (!network) return []; // flowtype fallback
// map BlockchainFeeLevel to SendFormReducer FeeLevel
const levels = feeLevels.map(level => ({
value: level.name,
fee: level.value,
label: `${toDecimalAmount(level.value, network.decimals)} ${network.symbol}`,
}));
// add "Custom" level
const customLevel = selected && selected.value === 'Custom' ? {
value: 'Custom',
fee: selected.fee,
label: `${toDecimalAmount(selected.fee, network.decimals)} ${network.symbol}`,
} : {
value: 'Custom',
fee: '0',
label: '',
};
return levels.concat([customLevel]);
2018-12-05 13:12:23 +00:00
};
2018-09-22 16:49:05 +00:00
export const getSelectedFeeLevel = (feeLevels: Array<FeeLevel>, selected: FeeLevel): FeeLevel => {
const { value } = selected;
let selectedFeeLevel: ?FeeLevel;
selectedFeeLevel = feeLevels.find(f => f.value === value);
if (!selectedFeeLevel) {
// fallback to default
selectedFeeLevel = feeLevels.find(f => f.value === 'Normal');
}
return selectedFeeLevel || selected;
2018-12-03 20:01:33 +00:00
};