1
0
mirror of https://github.com/trezor/trezor-wallet synced 2025-01-03 21:00:55 +00:00

add fees to Ripple

This commit is contained in:
Szymon Lesisz 2018-12-28 16:15:18 +01:00
parent 025e7856cd
commit be9cd4fef0
4 changed files with 205 additions and 66 deletions

View File

@ -1,7 +1,7 @@
/* @flow */ /* @flow */
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
// import * as BLOCKCHAIN from 'actions/constants/blockchain'; import * as BLOCKCHAIN from 'actions/constants/blockchain';
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import * as AccountsActions from 'actions/AccountsActions'; import * as AccountsActions from 'actions/AccountsActions';
import { toDecimalAmount } from 'utils/formatUtils'; import { toDecimalAmount } from 'utils/formatUtils';
@ -11,6 +11,9 @@ import type {
Dispatch, Dispatch,
GetState, GetState,
PromiseAction, PromiseAction,
PayloadAction,
Network,
BlockchainFeeLevel,
} from 'flowtype'; } from 'flowtype';
const DECIMALS: number = 6; const DECIMALS: number = 6;
@ -23,22 +26,40 @@ export const subscribe = (network: string): PromiseAction<void> => async (dispat
}); });
}; };
// Get current known fee
// Use default values from appConfig.json if it wasn't downloaded from blockchain yet
// update them later, after onBlockMined event
export const getFeeLevels = (network: Network): PayloadAction<Array<BlockchainFeeLevel>> => (dispatch: Dispatch, getState: GetState): Array<BlockchainFeeLevel> => {
const blockchain = getState().blockchain.find(b => b.shortcut === network.shortcut);
if (!blockchain || blockchain.feeLevels.length < 1) {
return network.fee.levels.map(level => ({
name: level.name,
value: level.value,
}));
}
return blockchain.feeLevels;
};
export const onBlockMined = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const onBlockMined = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const blockchain = getState().blockchain.find(b => b.shortcut === network); const blockchain = getState().blockchain.find(b => b.shortcut === network);
if (!blockchain) return; if (!blockchain) return; // flowtype fallback
// const fee = await TrezorConnect.blockchainGetFee({ // if last update was more than 5 minutes ago
// coin: network, const now = new Date().getTime();
// }); if (blockchain.feeTimestamp < now - 300000) {
// if (!fee.success) return; const feeRequest = await TrezorConnect.blockchainEstimateFee({
coin: network,
});
if (feeRequest.success) {
dispatch({
type: BLOCKCHAIN.UPDATE_FEE,
shortcut: network,
feeLevels: feeRequest.payload,
});
}
}
// if (fee.payload !== blockchain.fee) { // TODO: check for blockchain rollbacks here!
// dispatch({
// type: BLOCKCHAIN.UPDATE_FEE,
// shortcut: network,
// fee: fee.payload,
// });
// }
const accounts: Array<any> = getState().accounts.filter(a => a.network === network); const accounts: Array<any> = getState().accounts.filter(a => a.network === network);
// console.warn('ACCOUNTS', accounts); // console.warn('ACCOUNTS', accounts);

View File

@ -16,9 +16,10 @@ import type {
AsyncAction, AsyncAction,
TrezorDevice, TrezorDevice,
} from 'flowtype'; } from 'flowtype';
import type { State } from 'reducers/SendFormRippleReducer'; import type { State, FeeLevel } from 'reducers/SendFormRippleReducer';
import * as SessionStorageActions from '../SessionStorageActions'; import * as SessionStorageActions from '../SessionStorageActions';
import * as BlockchainActions from './BlockchainActions';
import * as ValidationActions from './SendFormValidationActions'; import * as ValidationActions from './SendFormValidationActions';
/* /*
@ -43,14 +44,14 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction =
// handle gasPrice update from backend // handle gasPrice update from backend
// recalculate fee levels if needed // recalculate fee levels if needed
if (action.type === BLOCKCHAIN.UPDATE_FEE) { if (action.type === BLOCKCHAIN.UPDATE_FEE) {
dispatch(ValidationActions.onFeeUpdated(action.shortcut, action.fee)); dispatch(ValidationActions.onFeeUpdated(action.shortcut, action.feeLevels));
return; return;
} }
let shouldUpdate: boolean = false; let shouldUpdate: boolean = false;
// check if "selectedAccount" reducer changed // check if "selectedAccount" reducer changed
shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, { shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, {
account: ['balance', 'nonce'], account: ['balance', 'sequence'],
}); });
// check if "sendForm" reducer changed // check if "sendForm" reducer changed
@ -79,7 +80,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
network, network,
} = getState().selectedAccount; } = getState().selectedAccount;
if (!account || !network) return; if (!account || account.networkType !== 'ripple' || !network) return;
const stateFromStorage = dispatch(SessionStorageActions.loadRippleDraftTransaction()); const stateFromStorage = dispatch(SessionStorageActions.loadRippleDraftTransaction());
if (stateFromStorage) { if (stateFromStorage) {
@ -91,7 +92,8 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
return; return;
} }
const feeLevels = dispatch(ValidationActions.getFeeLevels(network.symbol)); const blockchainFeeLevels = dispatch(BlockchainActions.getFeeLevels(network));
const feeLevels = dispatch(ValidationActions.getFeeLevels(blockchainFeeLevels));
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel);
dispatch({ dispatch({
@ -103,11 +105,20 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
networkSymbol: network.symbol, networkSymbol: network.symbol,
feeLevels, feeLevels,
selectedFeeLevel, selectedFeeLevel,
fee: network.fee.defaultFee,
sequence: '1', sequence: '1',
}, },
}); });
}; };
/*
* Called from UI from "advanced" button
*/
export const toggleAdvanced = (): Action => ({
type: SEND.TOGGLE_ADVANCED,
networkType: 'ripple',
});
/* /*
* Called from UI on "address" field change * Called from UI on "address" field change
*/ */
@ -160,6 +171,78 @@ 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().sendFormRipple;
const isCustom = feeLevel.value === 'Custom';
const advanced = isCustom ? true : state.advanced;
dispatch({
type: SEND.CHANGE,
networkType: 'ripple',
state: {
...state,
advanced,
selectedFeeLevel: feeLevel,
fee: isCustom ? state.selectedFeeLevel.fee : feeLevel.fee,
},
});
};
/*
* 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 blockchainFeeLevels = dispatch(BlockchainActions.getFeeLevels(network));
const state: State = getState().sendFormRipple;
const feeLevels = dispatch(ValidationActions.getFeeLevels(blockchainFeeLevels, state.selectedFeeLevel));
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
dispatch({
type: SEND.CHANGE,
networkType: 'ripple',
state: {
...state,
feeLevels,
selectedFeeLevel,
feeNeedsUpdate: false,
},
});
};
/*
* Called from UI on "advanced / fee" field change
*/
export const onFeeChange = (fee: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const { network } = getState().selectedAccount;
if (!network) return;
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, fee: true },
selectedFeeLevel: newSelectedFeeLevel,
fee,
},
});
};
/* /*
* Called from UI from "send" button * Called from UI from "send" button
@ -190,7 +273,7 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
useEmptyPassphrase: selected.useEmptyPassphrase, useEmptyPassphrase: selected.useEmptyPassphrase,
path: account.accountPath, path: account.accountPath,
transaction: { transaction: {
fee: blockchain.fee, // Fee must be in the range of 10 to 10,000 drops fee: currentState.selectedFeeLevel.fee, // Fee must be in the range of 10 to 10,000 drops
flags: 0x80000000, flags: 0x80000000,
sequence: account.sequence, sequence: account.sequence,
payment: { payment: {
@ -256,8 +339,12 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
}; };
export default { export default {
toggleAdvanced,
onAddressChange, onAddressChange,
onAmountChange, onAmountChange,
onSetMax, onSetMax,
onFeeLevelChange,
updateFeeLevels,
onFeeChange,
onSend, onSend,
}; };

View File

@ -9,11 +9,13 @@ import type {
Dispatch, Dispatch,
GetState, GetState,
PayloadAction, PayloadAction,
BlockchainFeeLevel,
} from 'flowtype'; } from 'flowtype';
import type { State, FeeLevel } from 'reducers/SendFormRippleReducer'; import type { State, FeeLevel } from 'reducers/SendFormRippleReducer';
import AddressValidator from 'wallet-address-validator'; import AddressValidator from 'wallet-address-validator';
// general regular expressions // general regular expressions
const ABS_RE = new RegExp('^[0-9]+$');
const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$'); const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$');
const XRP_6_RE = new RegExp('^(0|0\\.([0-9]{0,6})?|[1-9][0-9]*\\.?([0-9]{0,6})?|\\.[0-9]{0,6})$'); const XRP_6_RE = new RegExp('^(0|0\\.([0-9]{0,6})?|[1-9][0-9]*\\.?([0-9]{0,6})?|\\.[0-9]{0,6})$');
@ -21,11 +23,10 @@ const XRP_6_RE = new RegExp('^(0|0\\.([0-9]{0,6})?|[1-9][0-9]*\\.?([0-9]{0,6})?|
* Called from SendFormActions.observe * Called from SendFormActions.observe
* Reaction for BLOCKCHAIN.FEE_UPDATED action * Reaction for BLOCKCHAIN.FEE_UPDATED action
*/ */
export const onFeeUpdated = (network: string, fee: string): PayloadAction<void> => (dispatch: Dispatch, getState: GetState): void => { export const onFeeUpdated = (network: string, feeLevels: Array<BlockchainFeeLevel>): PayloadAction<void> => (dispatch: Dispatch, getState: GetState): void => {
const state = getState().sendFormRipple; const state = getState().sendFormRipple;
if (network === state.networkSymbol) return; if (network === state.networkSymbol) return;
if (!state.untouched) { if (!state.untouched) {
// if there is a transaction draft let the user know // if there is a transaction draft let the user know
// and let him update manually // and let him update manually
@ -35,24 +36,21 @@ export const onFeeUpdated = (network: string, fee: string): PayloadAction<void>
state: { state: {
...state, ...state,
feeNeedsUpdate: true, feeNeedsUpdate: true,
recommendedFee: fee,
}, },
}); });
return; return;
} }
// automatically update feeLevels and gasPrice // automatically update feeLevels
const feeLevels = dispatch(getFeeLevels(state.networkSymbol)); const newFeeLevels = dispatch(getFeeLevels(feeLevels));
const selectedFeeLevel = getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); const selectedFeeLevel = getSelectedFeeLevel(newFeeLevels, state.selectedFeeLevel);
dispatch({ dispatch({
type: SEND.CHANGE, type: SEND.CHANGE,
networkType: 'ripple', networkType: 'ripple',
state: { state: {
...state, ...state,
feeNeedsUpdate: false, feeNeedsUpdate: false,
recommendedFee: fee, feeLevels: newFeeLevels,
gasPrice: selectedFeeLevel.gasPrice,
feeLevels,
selectedFeeLevel, selectedFeeLevel,
}, },
}); });
@ -70,9 +68,11 @@ export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getSt
state.warnings = {}; state.warnings = {};
state.infos = {}; state.infos = {};
state = dispatch(recalculateTotalAmount(state)); state = dispatch(recalculateTotalAmount(state));
state = dispatch(updateCustomFeeLabel(state));
state = dispatch(addressValidation(state)); state = dispatch(addressValidation(state));
state = dispatch(addressLabel(state)); state = dispatch(addressLabel(state));
state = dispatch(amountValidation(state)); state = dispatch(amountValidation(state));
state = dispatch(feeValidation(state));
return state; return state;
}; };
@ -83,19 +83,27 @@ const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatc
} = getState().selectedAccount; } = getState().selectedAccount;
if (!account) return $state; 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 }; const state = { ...$state };
if (state.setMax) { if (state.setMax) {
const pendingAmount = getPendingAmount(pending, state.networkSymbol, false); const pendingAmount = getPendingAmount(pending, state.networkSymbol, false);
const b = new BigNumber(account.balance).minus(pendingAmount); const availableBalance = new BigNumber(account.balance).minus(pendingAmount);
state.amount = calculateMaxAmount(b, fee); state.amount = calculateMaxAmount(availableBalance, state.selectedFeeLevel.fee);
} }
state.total = calculateTotal(state.amount, fee); state.total = calculateTotal(state.amount, state.selectedFeeLevel.fee);
return state;
};
const updateCustomFeeLabel = ($state: State): PayloadAction<State> => (): State => {
const state = { ...$state };
if ($state.selectedFeeLevel.value === 'Custom') {
state.selectedFeeLevel = {
...state.selectedFeeLevel,
fee: state.fee,
label: `${toDecimalAmount(state.fee, 6)} ${state.networkSymbol}`,
};
}
return state; return state;
}; };
@ -189,6 +197,33 @@ const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dis
return state; return state;
}; };
/*
* 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.lessThan(10)) {
state.errors.fee = 'Fee is below recommended';
}
}
return state;
};
/* /*
* UTILITIES * UTILITIES
*/ */
@ -212,36 +247,32 @@ const calculateMaxAmount = (balance: BigNumber, fee: string): string => {
} }
}; };
export const getFeeLevels = (symbol: string): PayloadAction<Array<FeeLevel>> => (dispatch: Dispatch, getState: GetState): Array<FeeLevel> => { // Generate FeeLevel dataset for "fee" select
const blockchain = getState().blockchain.find(b => b.shortcut === symbol.toLowerCase()); export const getFeeLevels = (feeLevels: Array<BlockchainFeeLevel>, selected?: FeeLevel): PayloadAction<Array<FeeLevel>> => (dispatch: Dispatch, getState: GetState): Array<FeeLevel> => {
if (!blockchain) { const { network } = getState().selectedAccount;
// return default fee levels (TODO: get them from config) if (!network) return []; // flowtype fallback
return [{
value: 'Normal',
gasPrice: '0.000012',
label: `0.000012 ${symbol}`,
}];
}
const xrpDrops = toDecimalAmount(blockchain.fee, 6); // map BlockchainFeeLevel to SendFormReducer FeeLevel
const levels = feeLevels.map(level => ({
value: level.name,
fee: level.value,
label: `${toDecimalAmount(level.value, network.decimals)} ${network.symbol}`,
}));
// TODO: calc fee levels // add "Custom" level
return [{ const customLevel = selected && selected.value === 'Custom' ? {
value: 'Normal', value: 'Custom',
gasPrice: xrpDrops, fee: selected.fee,
label: `${xrpDrops} ${symbol}`, label: `${toDecimalAmount(selected.fee, network.decimals)} ${network.symbol}`,
}]; } : {
value: 'Custom',
fee: '0',
label: '',
};
return levels.concat([customLevel]);
}; };
// export const getFeeLevels = (shortcut: string): Array<FeeLevel> => ([
// {
// value: 'Normal',
// gasPrice: '1',
// label: `1 ${shortcut}`,
// },
// ]);
export const getSelectedFeeLevel = (feeLevels: Array<FeeLevel>, selected: FeeLevel): FeeLevel => { export const getSelectedFeeLevel = (feeLevels: Array<FeeLevel>, selected: FeeLevel): FeeLevel => {
const { value } = selected; const { value } = selected;
let selectedFeeLevel: ?FeeLevel; let selectedFeeLevel: ?FeeLevel;

View File

@ -7,7 +7,7 @@ import type { Action } from 'flowtype';
export type FeeLevel = { export type FeeLevel = {
label: string; label: string;
gasPrice: string; fee: string;
value: string; value: string;
} }
@ -24,7 +24,7 @@ export type State = {
setMax: boolean; setMax: boolean;
feeLevels: Array<FeeLevel>; feeLevels: Array<FeeLevel>;
selectedFeeLevel: FeeLevel; selectedFeeLevel: FeeLevel;
recommendedFee: string; fee: string;
feeNeedsUpdate: boolean; feeNeedsUpdate: boolean;
sequence: string; sequence: string;
total: string; total: string;
@ -49,11 +49,11 @@ export const initialState: State = {
setMax: false, setMax: false,
feeLevels: [], feeLevels: [],
selectedFeeLevel: { selectedFeeLevel: {
label: 'Normal',
gasPrice: '0',
value: 'Normal', value: 'Normal',
label: '',
fee: '0',
}, },
recommendedFee: '0', fee: '0',
feeNeedsUpdate: false, feeNeedsUpdate: false,
sequence: '0', sequence: '0',
total: '0', total: '0',