1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-15 21:08:57 +00:00

Merge pull request #288 from trezor/feature/ripple-custom-fee

Feature/ripple custom fee
This commit is contained in:
Vladimir Volek 2019-01-04 15:00:31 +01:00 committed by GitHub
commit 10df59f1da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 743 additions and 179 deletions

View File

@ -70,7 +70,7 @@
"rimraf": "^2.6.2",
"styled-components": "^4.1.2",
"styled-normalize": "^8.0.4",
"trezor-connect": "6.0.3-beta.5",
"trezor-connect": "6.0.3-beta.10",
"wallet-address-validator": "^0.2.4",
"web3": "1.0.0-beta.35",
"webpack": "^4.16.3",

View File

@ -52,6 +52,18 @@
"defaultGasLimit": 21000,
"defaultGasLimitTokens": 200000,
"decimals": 18,
"fee": {
"defaultFee": "64",
"minFee": "10",
"maxFee": "10000",
"defaultGasLimit": "21000",
"defaultGasLimitTokens": "200000",
"levels": [
{ "name": "High", "value": "96", "multiplier": 1.5 },
{ "name": "Normal", "value": "64", "multiplier": 1, "recommended": true },
{ "name": "Low", "value": "48", "multiplier": 0.75 }
]
},
"tokens": "./data/ropstenTokens.json",
"web3": [
"wss://ropsten1.trezor.io/geth"
@ -68,6 +80,14 @@
"shortcut": "xrp",
"bip44": "m/44'/144'/a'/0/0",
"decimals": 6,
"fee": {
"defaultFee": "12",
"minFee": "10",
"maxFee": "10000",
"levels": [
{"name": "Normal", "value": "12", "recommended": true }
]
},
"explorer": {
"tx": "https://xrpcharts.ripple.com/#/transactions/",
"address": "https://xrpcharts.ripple.com/#/graph/"
@ -81,6 +101,14 @@
"shortcut": "txrp",
"bip44": "m/44'/144'/a'/0/0",
"decimals": 6,
"fee": {
"defaultFee": "12",
"minFee": "10",
"maxFee": "10000",
"levels": [
{"name": "Normal", "value": "12", "recommended": true }
]
},
"explorer": {
"tx": "https://sisyfos.trezor.io/ripple-testnet-explorer/tx/",
"address": "https://sisyfos.trezor.io/ripple-testnet-explorer/address/"

View File

@ -8,6 +8,7 @@ import type {
Dispatch,
GetState,
PromiseAction,
BlockchainFeeLevel,
} from 'flowtype';
import type { BlockchainBlock, BlockchainNotification, BlockchainError } from 'trezor-connect';
@ -17,7 +18,7 @@ export type BlockchainAction = {
} | {
type: typeof BLOCKCHAIN.UPDATE_FEE,
shortcut: string,
fee: string,
feeLevels: Array<BlockchainFeeLevel>,
}
// Conditionally subscribe to blockchain backend
@ -100,7 +101,7 @@ export const onNotification = (payload: $ElementType<BlockchainNotification, 'pa
};
// Handle BLOCKCHAIN.ERROR event from TrezorConnect
// disconnect and remove Web3 webscocket instance if exists
// disconnect and remove Web3 websocket instance if exists
export const onError = (payload: $ElementType<BlockchainError, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const shortcut = payload.coin.shortcut.toLowerCase();
const { config } = getState().localStorage;

View File

@ -63,7 +63,7 @@ const findTokens = (accounts: Array<Account>, tokens: Array<Token>): Array<Token
const findDiscovery = (devices: Array<TrezorDevice>, discovery: Array<Discovery>): Array<Discovery> => devices.reduce((arr, dev) => arr.concat(discovery.filter(d => d.deviceState === dev.state && d.completed)), []);
const findPendingTxs = (accounts: Array<Account>, pending: Array<Transaction>): Array<Transaction> => accounts.reduce((result, account) => result.concat(pending.filter(p => p.address === account.descriptor && p.network === account.network)), []);
const findPendingTxs = (accounts: Array<Account>, pending: Array<Transaction>): Array<Transaction> => accounts.reduce((result, account) => result.concat(pending.filter(p => p.descriptor === account.descriptor && p.network === account.network)), []);
export const save = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const devices: Array<TrezorDevice> = getState().devices.filter(d => d.features && d.remember === true);

View File

@ -2,6 +2,7 @@
import * as ACCOUNT from 'actions/constants/account';
import * as SEND from 'actions/constants/send';
import * as WEB3 from 'actions/constants/web3';
import * as BLOCKCHAIN from 'actions/constants/blockchain';
import type {
Dispatch,
@ -36,6 +37,7 @@ export type SendFormAction = {
const actions = [
ACCOUNT.UPDATE_SELECTED_ACCOUNT,
WEB3.GAS_PRICE_UPDATED,
BLOCKCHAIN.UPDATE_FEE,
...Object.values(SEND).filter(v => typeof v === 'string'),
];

View File

@ -120,15 +120,16 @@ export const initWeb3 = (network: string, urlIndex: number = 0): PromiseAction<W
web3.currentProvider.on('error', onEnd);
});
export const discoverAccount = (address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
export const discoverAccount = (descriptor: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
const instance: Web3Instance = await dispatch(initWeb3(network));
const balance = await instance.web3.eth.getBalance(address);
const nonce = await instance.web3.eth.getTransactionCount(address);
const balance = await instance.web3.eth.getBalance(descriptor);
const nonce = await instance.web3.eth.getTransactionCount(descriptor);
return {
address,
descriptor,
transactions: 0,
block: 0,
balance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
nonce,
};
};

View File

@ -16,14 +16,15 @@ import type { NetworkToken } from 'reducers/LocalStorageReducer';
import * as Web3Actions from 'actions/Web3Actions';
import * as AccountsActions from 'actions/AccountsActions';
export const discoverAccount = (device: TrezorDevice, address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
export const discoverAccount = (device: TrezorDevice, descriptor: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
// get data from connect
const txs = await TrezorConnect.ethereumGetAccountInfo({
account: {
address,
descriptor,
block: 0,
transactions: 0,
balance: '0',
availableBalance: '0',
nonce: 0,
},
coin: network,
@ -34,12 +35,13 @@ export const discoverAccount = (device: TrezorDevice, address: string, network:
}
// blockbook web3 fallback
const web3account = await dispatch(Web3Actions.discoverAccount(address, network));
const web3account = await dispatch(Web3Actions.discoverAccount(descriptor, network));
return {
address,
descriptor,
transactions: txs.payload.transactions,
block: txs.payload.block,
balance: web3account.balance,
availableBalance: web3account.balance,
nonce: web3account.nonce,
};
};
@ -48,7 +50,6 @@ export const getTokenInfo = (input: string, network: string): PromiseAction<Netw
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => dispatch(Web3Actions.getTokenBalance(token));
export const getGasPrice = (network: string, defaultGasPrice: number): PromiseAction<BigNumber> => async (dispatch: Dispatch): Promise<BigNumber> => {
try {
const gasPrice = await dispatch(Web3Actions.getCurrentGasPrice(network));
@ -96,6 +97,9 @@ export const subscribe = (network: string): PromiseAction<void> => async (dispat
};
export const onBlockMined = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
// TODO: handle rollback,
// check latest saved transaction blockhash against blockhheight
// try to resolve pending transactions
await dispatch(Web3Actions.resolvePendingTransactions(network));
@ -129,10 +133,10 @@ export const onBlockMined = (network: string): PromiseAction<void> => async (dis
export const onNotification = (payload: $ElementType<BlockchainNotification, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { notification } = payload;
const account = getState().accounts.find(a => a.descriptor === notification.address);
const account = getState().accounts.find(a => a.descriptor === notification.descriptor);
if (!account) return;
if (notification.status === 'pending') {
if (!notification.blockHeight) {
dispatch({
type: PENDING.ADD,
payload: {

View File

@ -530,9 +530,7 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
const fee = ValidationActions.calculateFee(currentState.gasLimit, currentState.gasPrice);
const blockchainNotification = {
type: 'send',
status: 'pending',
confirmations: 0,
address: account.descriptor,
descriptor: account.descriptor,
inputs: [
{
addresses: [account.descriptor],
@ -553,7 +551,15 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
total: currentState.total,
sequence: nonce,
currency: isToken ? currentState.currency : undefined,
tokens: isToken ? [{
name: currentState.currency,
shortcut: currentState.currency,
value: currentState.amount,
}] : undefined,
blockHeight: 0,
blockHash: undefined,
timestamp: undefined,
};
dispatch(BlockchainActions.onNotification({

View File

@ -1,20 +1,22 @@
/* @flow */
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 AccountsActions from 'actions/AccountsActions';
import { toDecimalAmount } from 'utils/formatUtils';
import { observeChanges } from 'reducers/utils';
import type { BlockchainNotification } from 'trezor-connect';
import type {
Dispatch,
GetState,
PromiseAction,
PayloadAction,
Network,
BlockchainFeeLevel,
} from 'flowtype';
const DECIMALS: number = 6;
export const subscribe = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const accounts: Array<string> = getState().accounts.filter(a => a.network === network).map(a => a.descriptor);
await TrezorConnect.blockchainSubscribe({
@ -23,22 +25,41 @@ 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> => {
const blockchain = getState().blockchain.find(b => b.shortcut === network);
if (!blockchain) return;
if (!blockchain) return; // flowtype fallback
// const fee = await TrezorConnect.blockchainGetFee({
// coin: network,
// });
// if (!fee.success) return;
// if last update was more than 5 minutes ago
const now = new Date().getTime();
if (blockchain.feeTimestamp < now - 300000) {
const feeRequest = await TrezorConnect.blockchainEstimateFee({
coin: network,
});
if (feeRequest.success && observeChanges(blockchain.feeLevels, feeRequest.payload)) {
// check if downloaded fee levels are different
dispatch({
type: BLOCKCHAIN.UPDATE_FEE,
shortcut: network,
feeLevels: feeRequest.payload,
});
}
}
// if (fee.payload !== blockchain.fee) {
// dispatch({
// type: BLOCKCHAIN.UPDATE_FEE,
// shortcut: network,
// fee: fee.payload,
// });
// }
// TODO: check for blockchain rollbacks here!
const accounts: Array<any> = getState().accounts.filter(a => a.network === network);
// console.warn('ACCOUNTS', accounts);
@ -68,10 +89,12 @@ export const onBlockMined = (network: string): PromiseAction<void> => async (dis
export const onNotification = (payload: $ElementType<BlockchainNotification, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { notification } = payload;
const account = getState().accounts.find(a => a.descriptor === notification.address);
const account = getState().accounts.find(a => a.descriptor === notification.descriptor);
if (!account) return;
const { network } = getState().selectedAccount;
if (!network) return; // flowtype fallback
if (notification.status === 'pending') {
if (!notification.blockHeight) {
dispatch({
type: PENDING.ADD,
payload: {
@ -79,14 +102,14 @@ export const onNotification = (payload: $ElementType<BlockchainNotification, 'pa
deviceState: account.deviceState,
network: account.network,
amount: toDecimalAmount(notification.amount, DECIMALS),
total: notification.type === 'send' ? toDecimalAmount(notification.total, DECIMALS) : toDecimalAmount(notification.amount, DECIMALS),
fee: toDecimalAmount(notification.fee, DECIMALS),
amount: toDecimalAmount(notification.amount, network.decimals),
total: notification.type === 'send' ? toDecimalAmount(notification.total, network.decimals) : toDecimalAmount(notification.amount, network.decimals),
fee: toDecimalAmount(notification.fee, network.decimals),
},
});
// todo: replace "send success" notification with link to explorer
} else if (notification.status === 'confirmed') {
} else {
dispatch({
type: PENDING.TX_RESOLVED,
hash: notification.hash,
@ -106,8 +129,8 @@ export const onNotification = (payload: $ElementType<BlockchainNotification, 'pa
dispatch(AccountsActions.update({
networkType: 'ripple',
...account,
balance: toDecimalAmount(updatedAccount.payload.balance, DECIMALS),
availableBalance: toDecimalAmount(updatedAccount.payload.availableBalance, DECIMALS),
balance: toDecimalAmount(updatedAccount.payload.balance, network.decimals),
availableBalance: toDecimalAmount(updatedAccount.payload.availableBalance, network.decimals),
block: updatedAccount.payload.block,
sequence: updatedAccount.payload.sequence,
reserve: '0',

View File

@ -66,7 +66,7 @@ export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discover
deviceID: device.features ? device.features.device_id : '0',
deviceState: device.state || '0',
accountPath: account.path || [],
descriptor: account.address,
descriptor: account.descriptor,
balance: toDecimalAmount(account.balance, network.decimals),
availableBalance: toDecimalAmount(account.availableBalance, network.decimals),

View File

@ -16,9 +16,10 @@ import type {
AsyncAction,
TrezorDevice,
} from 'flowtype';
import type { State } from 'reducers/SendFormRippleReducer';
import type { State, FeeLevel } from 'reducers/SendFormRippleReducer';
import * as SessionStorageActions from '../SessionStorageActions';
import * as BlockchainActions from './BlockchainActions';
import * as ValidationActions from './SendFormValidationActions';
/*
@ -43,14 +44,14 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction =
// handle gasPrice update from backend
// recalculate fee levels if needed
if (action.type === BLOCKCHAIN.UPDATE_FEE) {
dispatch(ValidationActions.onFeeUpdated(action.shortcut, action.fee));
dispatch(ValidationActions.onFeeUpdated(action.shortcut, action.feeLevels));
return;
}
let shouldUpdate: boolean = false;
// check if "selectedAccount" reducer changed
shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, {
account: ['balance', 'nonce'],
account: ['balance', 'sequence'],
});
// check if "sendForm" reducer changed
@ -79,7 +80,7 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
network,
} = getState().selectedAccount;
if (!account || !network) return;
if (!account || account.networkType !== 'ripple' || !network) return;
const stateFromStorage = dispatch(SessionStorageActions.loadRippleDraftTransaction());
if (stateFromStorage) {
@ -91,7 +92,8 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
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);
dispatch({
@ -103,11 +105,20 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
networkSymbol: network.symbol,
feeLevels,
selectedFeeLevel,
fee: network.fee.defaultFee,
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
*/
@ -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
@ -190,7 +273,7 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
useEmptyPassphrase: selected.useEmptyPassphrase,
path: account.accountPath,
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,
sequence: account.sequence,
payment: {
@ -256,8 +339,12 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
};
export default {
toggleAdvanced,
onAddressChange,
onAmountChange,
onSetMax,
onFeeLevelChange,
updateFeeLevels,
onFeeChange,
onSend,
};

View File

@ -9,11 +9,13 @@ import type {
Dispatch,
GetState,
PayloadAction,
BlockchainFeeLevel,
} from 'flowtype';
import type { State, FeeLevel } from 'reducers/SendFormRippleReducer';
import AddressValidator from 'wallet-address-validator';
// 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 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
* 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;
if (network === state.networkSymbol) return;
if (!state.untouched) {
// if there is a transaction draft let the user know
// and let him update manually
@ -35,24 +36,21 @@ export const onFeeUpdated = (network: string, fee: string): PayloadAction<void>
state: {
...state,
feeNeedsUpdate: true,
recommendedFee: fee,
},
});
return;
}
// automatically update feeLevels and gasPrice
const feeLevels = dispatch(getFeeLevels(state.networkSymbol));
const selectedFeeLevel = getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
// automatically update feeLevels
const newFeeLevels = dispatch(getFeeLevels(feeLevels));
const selectedFeeLevel = getSelectedFeeLevel(newFeeLevels, state.selectedFeeLevel);
dispatch({
type: SEND.CHANGE,
networkType: 'ripple',
state: {
...state,
feeNeedsUpdate: false,
recommendedFee: fee,
gasPrice: selectedFeeLevel.gasPrice,
feeLevels,
feeLevels: newFeeLevels,
selectedFeeLevel,
},
});
@ -69,36 +67,51 @@ export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getSt
state.errors = {};
state.warnings = {};
state.infos = {};
state = dispatch(updateCustomFeeLabel(state));
state = dispatch(recalculateTotalAmount(state));
state = dispatch(addressValidation(state));
state = dispatch(addressLabel(state));
state = dispatch(amountValidation(state));
state = dispatch(feeValidation(state));
return state;
};
const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
const {
account,
network,
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);
if (!account || !network) return $state;
const state = { ...$state };
const fee = toDecimalAmount(state.selectedFeeLevel.fee, network.decimals);
if (state.setMax) {
const pendingAmount = getPendingAmount(pending, state.networkSymbol, false);
const b = new BigNumber(account.balance).minus(pendingAmount);
state.amount = calculateMaxAmount(b, fee);
const availableBalance = new BigNumber(account.balance).minus(pendingAmount);
state.amount = calculateMaxAmount(availableBalance, fee);
}
state.total = calculateTotal(state.amount, fee);
return state;
};
const updateCustomFeeLabel = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
const { network } = getState().selectedAccount;
if (!network) return $state; // flowtype fallback
const state = { ...$state };
if ($state.selectedFeeLevel.value === 'Custom') {
state.selectedFeeLevel = {
...state.selectedFeeLevel,
fee: state.fee,
label: `${toDecimalAmount(state.fee, network.decimals)} ${state.networkSymbol}`,
};
}
return state;
};
/*
* Address value validation
*/
@ -189,6 +202,35 @@ const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dis
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(network.fee.minFee)) {
state.errors.fee = 'Fee is below recommended';
} else if (gl.greaterThan(network.fee.maxFee)) {
state.errors.fee = 'Fee is above recommended';
}
}
return state;
};
/*
* UTILITIES
*/
@ -212,36 +254,32 @@ const calculateMaxAmount = (balance: BigNumber, fee: string): string => {
}
};
export const getFeeLevels = (symbol: string): PayloadAction<Array<FeeLevel>> => (dispatch: Dispatch, getState: GetState): Array<FeeLevel> => {
const blockchain = getState().blockchain.find(b => b.shortcut === symbol.toLowerCase());
if (!blockchain) {
// return default fee levels (TODO: get them from config)
return [{
value: 'Normal',
gasPrice: '0.000012',
label: `0.000012 ${symbol}`,
}];
}
// 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
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
return [{
value: 'Normal',
gasPrice: xrpDrops,
label: `${xrpDrops} ${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]);
};
// export const getFeeLevels = (shortcut: string): Array<FeeLevel> => ([
// {
// value: 'Normal',
// gasPrice: '1',
// label: `1 ${shortcut}`,
// },
// ]);
export const getSelectedFeeLevel = (feeLevels: Array<FeeLevel>, selected: FeeLevel): FeeLevel => {
const { value } = selected;
let selectedFeeLevel: ?FeeLevel;

View File

@ -70,26 +70,24 @@ const TransactionItem = ({
network,
}: Props) => {
const url = `${network.explorer.tx}${tx.hash}`;
const date = typeof tx.timestamp === 'string' && tx.confirmations > 0 ? tx.timestamp : undefined; // TODO: format date
const date = typeof tx.timestamp === 'string' ? tx.timestamp : undefined; // TODO: format date
const addresses = (tx.type === 'send' ? tx.outputs : tx.inputs).reduce((arr, item) => arr.concat(item.addresses), []);
const currency = tx.currency || tx.network;
const isToken = currency !== tx.network;
const amount = isToken ? `${tx.amount} ${currency}` : `${tx.total} ${network.symbol}`;
const fee = isToken && tx.type === 'send' ? `${tx.fee} ${network.symbol}` : undefined;
const operation = tx.type === 'send' ? '-' : '+';
const amount = tx.tokens ? tx.tokens.map(t => (<Amount key={t.value}>{operation}{t.value} {t.shortcut}</Amount>)) : <Amount>{operation}{tx.total} {network.symbol}</Amount>;
const fee = tx.tokens && tx.type === 'send' ? `${tx.fee} ${network.symbol}` : undefined;
return (
<Wrapper>
{ date && (<Date href={url} isGray>{ date }</Date>)}
<Addresses>
{ addresses.map(addr => (<Address key={addr}>{addr}</Address>)) }
{ tx.confirmations <= 0 && (
{ !tx.blockHeight && (
<Date href={url} isGray>Transaction hash: {tx.hash}</Date>
)}
</Addresses>
<Value className={tx.type}>
<Amount>{operation}{amount}</Amount>
{amount}
{ fee && (<Fee>{operation}{fee}</Fee>) }
</Value>
</Wrapper>

View File

@ -15,7 +15,7 @@ const Img = styled.img`
`;
const TrezorImage = ({ model }: Props) => {
// $FlowIssue
// $FlowIssue: `require` must be a string literal.
const src = require(`./images/trezor-${model}.png`); // eslint-disable-line
return (
<Wrapper>

View File

@ -159,6 +159,7 @@ export type { Account } from 'reducers/AccountsReducer';
export type { Discovery } from 'reducers/DiscoveryReducer';
export type { Token } from 'reducers/TokensReducer';
export type { Web3Instance } from 'reducers/Web3Reducer';
export type { BlockchainFeeLevel } from 'reducers/BlockchainReducer';
export type Accounts = $ElementType<State, 'accounts'>;
export type LocalStorage = $ElementType<State, 'localStorage'>;

View File

@ -6,12 +6,17 @@ import * as BLOCKCHAIN_ACTION from 'actions/constants/blockchain';
import type { Action } from 'flowtype';
import type { BlockchainConnect, BlockchainError, BlockchainBlock } from 'trezor-connect';
export type BlockchainFeeLevel = {
name: string,
value: string,
};
export type BlockchainNetwork = {
+shortcut: string,
feeTimestamp: number,
feeLevels: Array<BlockchainFeeLevel>,
connected: boolean,
block: number,
reserved: string, // xrp specific
fee: string,
};
export type State = Array<BlockchainNetwork>;
@ -27,8 +32,6 @@ const onConnect = (state: State, action: BlockchainConnect): State => {
return others.concat([{
...network,
connected: true,
fee: info.fee,
block: info.block,
}]);
}
@ -36,8 +39,8 @@ const onConnect = (state: State, action: BlockchainConnect): State => {
shortcut,
connected: true,
block: info.block,
fee: info.fee,
reserved: info.reserved || '0',
feeTimestamp: 0,
feeLevels: [],
}]);
};
@ -56,8 +59,8 @@ const onError = (state: State, action: BlockchainError): State => {
shortcut,
connected: false,
block: 0,
fee: '0',
reserved: '0',
feeTimestamp: 0,
feeLevels: [],
}]);
};
@ -75,14 +78,15 @@ const onBlock = (state: State, action: BlockchainBlock): State => {
return state;
};
const updateFee = (state: State, shortcut: string, fee: string): State => {
const updateFee = (state: State, shortcut: string, feeLevels: Array<BlockchainFeeLevel>): State => {
const network = state.find(b => b.shortcut === shortcut);
if (!network) return state;
const others = state.filter(b => b !== network);
return others.concat([{
...network,
fee,
feeTimestamp: new Date().getTime(),
feeLevels,
}]);
};
@ -96,7 +100,7 @@ export default (state: State = initialState, action: Action): State => {
case BLOCKCHAIN_EVENT.BLOCK:
return onBlock(state, action);
case BLOCKCHAIN_ACTION.UPDATE_FEE:
return updateFee(state, action.shortcut, action.fee);
return updateFee(state, action.shortcut, action.feeLevels);
default:
return state;

View File

@ -5,6 +5,14 @@ import * as STORAGE from 'actions/constants/localStorage';
import type { Action } from 'flowtype';
type NetworkFeeLevel = {
name: string,
value: string, // ETH: gasPrice in gwei, XRP: fee in drops, BTC: sat/b
multiplier: number, // ETH specific
blocks: number, // BTC specific
recommended: boolean,
};
export type Network = {
type: string;
name: string;
@ -15,13 +23,21 @@ export type Network = {
defaultGasLimit: number;
defaultGasLimitTokens: number;
defaultGasPrice: number;
chainId: number;
chainId: number; // ETH specific
explorer: {
tx: string;
address: string;
};
tokens: string;
decimals: number,
decimals: number;
fee: {
defaultFee: string;
minFee: string;
maxFee: string;
defaultGasLimit: string; // ETH specific
defaultGasLimitTokens: string; // ETH specific
levels: Array<NetworkFeeLevel>;
},
backends: Array<{
name: string;
urls: Array<string>;

View File

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

View File

@ -86,18 +86,27 @@ export const getDiscoveryProcess = (state: State): ?Discovery => {
export const getAccountPendingTx = (pending: Array<Transaction>, account: ?Account): Array<Transaction> => {
const a = account;
if (!a) return [];
return pending.filter(p => p.network === a.network && p.address === a.descriptor);
return pending.filter(p => p.network === a.network && p.descriptor === a.descriptor);
};
export const getPendingSequence = (pending: Array<Transaction>): number => pending.reduce((value: number, tx: Transaction) => {
export const getPendingSequence = (pending: Array<Transaction>): number => pending.reduce((value: number, tx: Transaction): number => {
if (tx.rejected) return value;
return Math.max(value, tx.sequence + 1);
}, 0);
export const getPendingAmount = (pending: Array<Transaction>, currency: string, token: boolean = false): BigNumber => pending.reduce((value: BigNumber, tx: Transaction) => {
if (tx.currency === currency && !tx.rejected) {
return new BigNumber(value).plus(token ? tx.amount : tx.total);
export const getPendingAmount = (pending: Array<Transaction>, currency: string, token: boolean = false): BigNumber => pending.reduce((value: BigNumber, tx: Transaction): BigNumber => {
if (!token) {
// regular transactions
// add fees from token txs and amount from regular txs
return new BigNumber(value).plus(tx.tokens ? tx.fee : tx.total);
}
if (tx.tokens) {
// token transactions
const allTokens = tx.tokens.filter(t => t.shortcut === currency);
const tokensValue: BigNumber = allTokens.reduce((tv, t) => new BigNumber(value).plus(t.value), new BigNumber('0'));
return new BigNumber(value).plus(tokensValue);
}
// default
return value;
}, new BigNumber('0'));
@ -115,7 +124,7 @@ export const getWeb3 = (state: State): ?Web3Instance => {
return state.web3.find(w3 => w3.network === locationState.network);
};
export const observeChanges = (prev: ?Object, current: ?Object, filter?: {[k: string]: Array<string>}): boolean => {
export const observeChanges = (prev: ?any, current: ?any, filter?: {[k: string]: Array<string>}): boolean => {
// 1. both objects are the same (solves simple types like string, boolean and number)
if (prev === current) return false;
// 2. one of the objects is null/undefined

View File

@ -0,0 +1,81 @@
// useful data for tests
export default [
{
type: 'send',
network: 'trop',
deviceState: '4058d01c7c964787b7d06f0f32ce229088e123a042bf95aad658f1b1b99c73fc',
deviceID: 'A21DA462908B176CEE0402C1',
descriptor: '0x73d0385F4d8E00C5e6504C6030F47BF6212736A8',
inputs: [
{
addresses: ['0x73d0385F4d8E00C5e6504C6030F47BF6212736A8'],
},
],
outputs: [
{
addresses: ['0x73d0385F4d8E00C5e6504C6030F47BF6212736A8', '2a', '3a'],
},
],
hash: '0x100000',
amount: '0.01',
fee: '0.00001',
total: '0.01001',
sequence: 1,
// tokens: undefined,
tokens: [
{
name: 'Name',
shortcut: 'T01',
value: '200',
},
// {
// name: 'Name2',
// shortcut: 'USD',
// value: '100',
// },
],
blockHeight: 0,
blockHash: undefined,
},
{
type: 'send',
network: 'txrp',
deviceState: '4058d01c7c964787b7d06f0f32ce229088e123a042bf95aad658f1b1b99c73fc',
deviceID: 'A21DA462908B176CEE0402C1',
descriptor: 'rNaqKtKrMSwpwZSzRckPf7S96DkimjkF4H',
inputs: [
{
addresses: ['rNaqKtKrMSwpwZSzRckPf7S96DkimjkF4H'],
},
],
outputs: [
{
addresses: ['rNaqKtKrMSwpwZSzRckPf7S96DkimjkF4H', '2a', '3a'],
},
],
hash: '0x100000',
amount: '0.01',
fee: '0.00001',
total: '0.01001',
sequence: 1,
tokens: undefined,
// tokens: [
// {
// name: 'Name',
// shortcut: 'T01',
// value: '200',
// },
// {
// name: 'Name2',
// shortcut: 'USD',
// value: '100',
// },
// ],
blockHeight: 0,
blockHash: undefined,
},
];

View File

@ -54,6 +54,18 @@ export const hexToString = (hex: string): string => {
return str;
};
export const toDecimalAmount = (amount: string, decimals: number): string => new BigNumber(amount).div(10 ** decimals).toString(10);
export const toDecimalAmount = (amount: string | number, decimals: number): string => {
try {
return new BigNumber(amount).div(10 ** decimals).toString(10);
} catch (error) {
return '0';
}
};
export const fromDecimalAmount = (amount: string, decimals: number): string => new BigNumber(amount).times(10 ** decimals).toString(10);
export const fromDecimalAmount = (amount: string | number, decimals: number): string => {
try {
return new BigNumber(amount).times(10 ** decimals).toString(10);
} catch (error) {
return '0';
}
};

View File

@ -2,8 +2,8 @@
export const getViewportHeight = (): number => (
// $FlowIssue
document.documentElement.clientHeight || document.body.clientHeight // $FlowIssue
// $FlowIssue: "clientHeight" missing in null
document.documentElement.clientHeight || document.body.clientHeight
);
export const getScrollX = (): number => {
@ -12,8 +12,8 @@ export const getScrollX = (): number => {
} if (window.scrollLeft !== undefined) {
return window.scrollLeft;
}
// $FlowIssue
return (document.documentElement || document.body.parentNode || document.body).scrollLeft; // $FlowIssue
// $FlowIssue: parentNode || scrollLeft missing
return (document.documentElement || document.body.parentNode || document.body).scrollLeft;
};
export const getScrollY = (): number => {
@ -22,6 +22,6 @@ export const getScrollY = (): number => {
} if (window.scrollTop !== undefined) {
return window.scrollTop;
}
// $FlowIssue
return (document.documentElement || document.body.parentNode || document.body).scrollTop; // $FlowIssue
// $FlowIssue: parentNode || scrollTop missing
return (document.documentElement || document.body.parentNode || document.body).scrollTop;
};

View File

@ -13,9 +13,9 @@ const getInfoUrl = (networkShortcut: ?string) => {
const urls = {
default: 'https://wiki.trezor.io',
xrp: 'https://wiki.trezor.io/Ripple_(XRP)',
txrp: 'https://wiki.trezor.io/Ripple_(XRP)',
};
return networkShortcut ? urls[networkShortcut] : urls.default;
return networkShortcut && urls[networkShortcut] ? urls[networkShortcut] : urls.default;
};
type Props = {

View File

@ -4,14 +4,15 @@ import styled from 'styled-components';
import React from 'react';
import { FONT_SIZE, FONT_WEIGHT } from 'config/variables';
import { NavLink } from 'react-router-dom';
import { connect } from 'react-redux';
import colors from 'config/colors';
import type { Location } from 'react-router';
import type { State } from 'flowtype';
import Indicator from './components/Indicator';
type Props = {
location: Location;
router: $ElementType<State, 'router'>,
selectedAccount: $ElementType<State, 'selectedAccount'>,
};
const Wrapper = styled.div`
@ -56,20 +57,28 @@ class TopNavigationAccount extends React.PureComponent<Props> {
wrapper: ?HTMLElement;
render() {
const { state, pathname } = this.props.location;
const { state, pathname } = this.props.router.location;
if (!state) return null;
const { network } = this.props.selectedAccount;
if (!network) return null;
const basePath = `/device/${state.device}/network/${state.network}/account/${state.account}`;
return (
<Wrapper className="account-tabs" ref={this.wrapperRefCallback}>
<StyledNavLink exact to={`${basePath}`}>Summary</StyledNavLink>
<StyledNavLink to={`${basePath}/receive`}>Receive</StyledNavLink>
<StyledNavLink to={`${basePath}/send`}>Send</StyledNavLink>
<StyledNavLink to={`${basePath}/signverify`}>Sign &amp; Verify</StyledNavLink>
{network.type === 'ethereum'
&& <StyledNavLink to={`${basePath}/signverify`}>Sign &amp; Verify</StyledNavLink>
}
<Indicator pathname={pathname} wrapper={() => this.wrapper} />
</Wrapper>
);
}
}
export default TopNavigationAccount;
export default connect((state: State): Props => ({
router: state.router,
selectedAccount: state.selectedAccount,
}), null)(TopNavigationAccount);

View File

@ -11,7 +11,7 @@ import Icon from 'components/Icon';
import ICONS from 'config/icons';
import { FONT_SIZE } from 'config/variables';
import type { Props as BaseProps } from '../../ethereum/Container';
import type { Props as BaseProps } from '../../Container';
type Props = BaseProps & {
children: React.Node,

View File

@ -1,6 +1,7 @@
/* @flow */
import React from 'react';
import BigNumber from 'bignumber.js';
import styled, { css } from 'styled-components';
import { Select } from 'components/Select';
import Button from 'components/Button';
@ -13,8 +14,9 @@ import colors from 'config/colors';
import Title from 'views/Wallet/components/Title';
import P from 'components/Paragraph';
import Content from 'views/Wallet/components/Content';
import * as stateUtils from 'reducers/utils';
import type { Token } from 'flowtype';
import AdvancedForm from '../components/AdvancedForm';
import AdvancedForm from './components/AdvancedForm';
import PendingTransactions from '../components/PendingTransactions';
import type { Props } from './Container';
@ -221,10 +223,11 @@ const AccountSend = (props: Props) => {
const isCurrentCurrencyToken = networkSymbol !== currency;
let selectedTokenBalance = 0;
let selectedTokenBalance = '0';
const selectedToken = tokens.find(t => t.symbol === currency);
if (selectedToken) {
selectedTokenBalance = selectedToken.balance;
const pendingAmount: BigNumber = stateUtils.getPendingAmount(props.selectedAccount.pending, selectedToken.symbol, true);
selectedTokenBalance = new BigNumber(selectedToken.balance).minus(pendingAmount).toString(10);
}
let isSendButtonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending;

View File

@ -0,0 +1,144 @@
/* @flow */
import * as React from 'react';
import styled from 'styled-components';
import colors from 'config/colors';
import Input from 'components/inputs/Input';
import Tooltip from 'components/Tooltip';
import Icon from 'components/Icon';
import ICONS from 'config/icons';
import type { Props as BaseProps } from '../../Container';
type Props = BaseProps & {
children: React.Node,
}
// TODO: Decide on a small screen width for the whole app
// and put it inside config/variables.js
// same variable also in "AccountSend/index.js"
const SmallScreenWidth = '850px';
const InputLabelWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`;
const AdvancedSettingsWrapper = styled.div`
padding: 20px 0;
display: flex;
flex-direction: column;
justify-content: space-between;
border-top: 1px solid ${colors.DIVIDER};
`;
const GasInputRow = styled.div`
width: 100%;
display: flex;
@media screen and (max-width: ${SmallScreenWidth}) {
flex-direction: column;
}
`;
const GasInput = styled(Input)`
/* min-height: 85px; */
padding-bottom: 28px;
&:first-child {
padding-right: 20px;
}
@media screen and (max-width: ${SmallScreenWidth}) {
&:first-child {
padding-right: 0;
}
}
`;
const AdvancedSettingsSendButtonWrapper = styled.div`
width: 100%;
display: flex;
justify-content: flex-end;
`;
const getFeeInputState = (feeErrors: string, feeWarnings: string): string => {
let state = '';
if (feeWarnings && !feeErrors) {
state = 'warning';
}
if (feeErrors) {
state = 'error';
}
return state;
};
const Left = styled.div`
display: flex;
align-items: center;
`;
// stateless component
const AdvancedForm = (props: Props) => {
const {
network,
} = props.selectedAccount;
if (!network) return null;
const {
errors,
warnings,
infos,
fee,
} = props.sendForm;
const {
onFeeChange,
} = props.sendFormActions;
return (
<AdvancedSettingsWrapper>
<GasInputRow>
<GasInput
state={getFeeInputState(errors.fee, warnings.fee)}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
topLabel={(
<InputLabelWrapper>
<Left>
Fee
<Tooltip
content={(
<React.Fragment>
Transfer cost in XRP drops
</React.Fragment>
)}
maxWidth={410}
readMoreLink="https://developers.ripple.com/transaction-cost.html"
placement="top"
>
<Icon
icon={ICONS.HELP}
color={colors.TEXT_SECONDARY}
size={24}
/>
</Tooltip>
</Left>
</InputLabelWrapper>
)}
bottomText={errors.fee || warnings.fee || infos.fee}
value={fee}
onChange={event => onFeeChange(event.target.value)}
/>
</GasInputRow>
<AdvancedSettingsSendButtonWrapper>
{ props.children }
</AdvancedSettingsSendButtonWrapper>
</AdvancedSettingsWrapper>
);
};
export default AdvancedForm;

View File

@ -6,12 +6,15 @@ import { Select } from 'components/Select';
import Button from 'components/Button';
import Input from 'components/inputs/Input';
import Icon from 'components/Icon';
import Link from 'components/Link';
import ICONS from 'config/icons';
import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables';
import colors from 'config/colors';
import Title from 'views/Wallet/components/Title';
import P from 'components/Paragraph';
import Content from 'views/Wallet/components/Content';
import PendingTransactions from '../components/PendingTransactions';
import AdvancedForm from './components/AdvancedForm';
import type { Props } from './Container';
@ -66,6 +69,34 @@ const CurrencySelect = styled(Select)`
flex: 0.2;
`;
const FeeOptionWrapper = styled.div`
display: flex;
justify-content: space-between;
`;
const FeeLabelWrapper = styled.div`
display: flex;
align-items: center;
padding-bottom: 10px;
`;
const FeeLabel = styled.span`
color: ${colors.TEXT_SECONDARY};
`;
const UpdateFeeWrapper = styled.span`
margin-left: 8px;
display: flex;
align-items: center;
font-size: ${FONT_SIZE.SMALL};
color: ${colors.WARNING_PRIMARY};
`;
const StyledLink = styled(Link)`
margin-left: 4px;
white-space: nowrap;
`;
const ToggleAdvancedSettingsWrapper = styled.div`
min-height: 40px;
margin-bottom: 20px;
@ -80,6 +111,14 @@ const ToggleAdvancedSettingsWrapper = styled.div`
}
`;
const ToggleAdvancedSettingsButton = styled(Button)`
min-height: 40px;
padding: 0;
display: flex;
align-items: center;
font-weight: ${FONT_WEIGHT.SEMIBOLD};
`;
const SendButton = styled(Button)`
min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')};
word-break: break-all;
@ -89,6 +128,10 @@ const SendButton = styled(Button)`
}
`;
const AdvancedSettingsIcon = styled(Icon)`
margin-left: 10px;
`;
// render helpers
const getAddressInputState = (address: string, addressErrors: string, addressWarnings: string): string => {
let state = '';
@ -128,6 +171,9 @@ const AccountSend = (props: Props) => {
address,
amount,
setMax,
feeLevels,
selectedFeeLevel,
feeNeedsUpdate,
total,
errors,
warnings,
@ -137,9 +183,12 @@ const AccountSend = (props: Props) => {
} = props.sendForm;
const {
toggleAdvanced,
onAddressChange,
onAmountChange,
onSetMax,
onFeeLevelChange,
updateFeeLevels,
onSend,
} = props.sendFormActions;
@ -231,18 +280,74 @@ const AccountSend = (props: Props) => {
/>
</InputRow>
<InputRow>
<FeeLabelWrapper>
<FeeLabel>Fee</FeeLabel>
{feeNeedsUpdate && (
<UpdateFeeWrapper>
<Icon
icon={ICONS.WARNING}
color={colors.WARNING_PRIMARY}
size={20}
/>
Recommended fees updated. <StyledLink onClick={updateFeeLevels} isGreen>Click here to use them</StyledLink>
</UpdateFeeWrapper>
)}
</FeeLabelWrapper>
<Select
isSearchable={false}
isClearable={false}
value={selectedFeeLevel}
onChange={onFeeLevelChange}
options={feeLevels}
formatOptionLabel={option => (
<FeeOptionWrapper>
<P>{option.value}</P>
<P>{option.label}</P>
</FeeOptionWrapper>
)}
/>
</InputRow>
<ToggleAdvancedSettingsWrapper
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
>
<SendButton
isDisabled={isSendButtonDisabled}
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
onClick={() => onSend()}
<ToggleAdvancedSettingsButton
isTransparent
onClick={toggleAdvanced}
>
{sendButtonText}
</SendButton>
Advanced settings
<AdvancedSettingsIcon
icon={ICONS.ARROW_DOWN}
color={colors.TEXT_SECONDARY}
size={24}
isActive={advanced}
canAnimate
/>
</ToggleAdvancedSettingsButton>
{isAdvancedSettingsHidden && (
<SendButton
isDisabled={isSendButtonDisabled}
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
onClick={() => onSend()}
>
{sendButtonText}
</SendButton>
)}
</ToggleAdvancedSettingsWrapper>
{advanced && (
<AdvancedForm {...props}>
<SendButton
isDisabled={isSendButtonDisabled}
onClick={() => onSend()}
>
{sendButtonText}
</SendButton>
</AdvancedForm>
)}
{props.selectedAccount.pending.length > 0 && (
<PendingTransactions

View File

@ -78,6 +78,8 @@ const AccountSummary = (props: Props) => {
const pendingAmount: BigNumber = stateUtils.getPendingAmount(pending, network.symbol);
const balance: string = new BigNumber(account.balance).minus(pendingAmount).toString(10);
const TMP_SHOW_HISTORY = false;
return (
<Content>
<React.Fragment>
@ -86,27 +88,29 @@ const AccountSummary = (props: Props) => {
<StyledCoinLogo network={account.network} />
<AccountTitle>Account #{parseInt(account.index, 10) + 1}</AccountTitle>
</AccountName>
<Link href={explorerLink} isGray>See full transaction history</Link>
{ !account.empty && <Link href={explorerLink} isGray>See full transaction history</Link> }
</AccountHeading>
<AccountBalance
network={network}
balance={balance}
fiat={props.fiat}
/>
<H2Wrapper>
<H2>History</H2>
<StyledTooltip
maxWidth={200}
placement="top"
content="Insert token name, symbol or address to be able to send it."
>
<StyledIcon
icon={ICONS.HELP}
color={colors.TEXT_SECONDARY}
size={24}
/>
</StyledTooltip>
</H2Wrapper>
{ TMP_SHOW_HISTORY && (
<H2Wrapper>
<H2>History</H2>
<StyledTooltip
maxWidth={200}
placement="top"
content="Insert token name, symbol or address to be able to send it."
>
<StyledIcon
icon={ICONS.HELP}
color={colors.TEXT_SECONDARY}
size={24}
/>
</StyledTooltip>
</H2Wrapper>)
}
</React.Fragment>
</Content>
);

View File

@ -130,7 +130,6 @@
"@babel/runtime@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz#b03e42eeddf5898e00646e4c840fa07ba8dcad7f"
integrity sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==
dependencies:
regenerator-runtime "^0.12.0"
@ -2621,7 +2620,6 @@ connect-history-api-fallback@^1.3.0:
connected-react-router@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.0.0.tgz#cb7ccbbc5ed353832ecd91d68289c916e8aba734"
integrity sha512-TarPqf2wY3cz993Mw3eBg2U12M5OmaGwKzJsinvRQh61nKb8WMUvimyhu6u2HeWS8625PHFXjNOU0OIAMWj/bQ==
dependencies:
immutable "^3.8.1"
seamless-immutable "^7.1.3"
@ -4840,7 +4838,6 @@ hoist-non-react-statics@^2.5.0:
hoist-non-react-statics@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz#c09c0555c84b38a7ede6912b61efddafd6e75e1e"
integrity sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==
dependencies:
react-is "^16.3.2"
@ -5034,7 +5031,6 @@ ignore@^3.3.5:
immutable@^3.8.1:
version "3.8.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
import-local@^1.0.0:
version "1.0.0"
@ -5147,7 +5143,6 @@ invariant@^2.2.0, invariant@^2.2.4:
invariant@^2.2.1, invariant@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
integrity sha1-nh9WrArNtr8wMwbzOL47IErmA2A=
dependencies:
loose-envify "^1.0.0"
@ -5903,7 +5898,6 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^3.7.0, js-yaml@^3.9.0, js-yaml@^3.9.1:
version "3.12.0"
@ -6363,7 +6357,6 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
@ -8049,7 +8042,6 @@ react-qr-svg@^2.1.0:
react-redux@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.0.tgz#09e86eeed5febb98e9442458ad2970c8f1a173ef"
integrity sha512-EmbC3uLl60pw2VqSSkj6HpZ6jTk12RMrwXMBdYtM6niq0MdEaRq9KYCwpJflkOZj349BLGQm1MI/JO1W96kLWQ==
dependencies:
"@babel/runtime" "^7.2.0"
hoist-non-react-statics "^3.2.1"
@ -8084,7 +8076,6 @@ react-router@^4.2.0:
react-router@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e"
integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==
dependencies:
history "^4.7.2"
hoist-non-react-statics "^2.5.0"
@ -8365,7 +8356,6 @@ regenerator-runtime@^0.11.0:
regenerator-runtime@^0.12.0:
version "0.12.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
regenerator-transform@^0.10.0:
version "0.10.1"
@ -8831,7 +8821,6 @@ scryptsy@^1.2.1:
seamless-immutable@^7.1.3:
version "7.1.4"
resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8"
integrity sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==
secp256k1@^3.0.1:
version "3.4.0"
@ -9871,9 +9860,9 @@ tr46@^1.0.1:
dependencies:
punycode "^2.1.0"
trezor-connect@6.0.3-beta.5:
version "6.0.3-beta.5"
resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-6.0.3-beta.5.tgz#5ceea84ee4c990fd4df27c0e0b4feacbce30f894"
trezor-connect@6.0.3-beta.10:
version "6.0.3-beta.10"
resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-6.0.3-beta.10.tgz#a703b06946bb98912cbe719f2bb3393ee41f03b1"
dependencies:
babel-runtime "^6.26.0"
events "^1.1.1"
@ -10359,7 +10348,6 @@ warning@^3.0.0:
warning@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607"
integrity sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==
dependencies:
loose-envify "^1.0.0"