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:
commit
10df59f1da
@ -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",
|
||||
|
@ -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/"
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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'),
|
||||
];
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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: {
|
||||
|
@ -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({
|
||||
|
@ -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',
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
};
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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'>;
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
81
src/reducers/utils/pendingTxsExamples.js
Normal file
81
src/reducers/utils/pendingTxsExamples.js
Normal 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,
|
||||
},
|
||||
];
|
@ -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';
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
@ -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 = {
|
||||
|
@ -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 & Verify</StyledNavLink>
|
||||
{network.type === 'ethereum'
|
||||
&& <StyledNavLink to={`${basePath}/signverify`}>Sign & 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);
|
@ -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,
|
@ -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;
|
||||
|
@ -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;
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
|
18
yarn.lock
18
yarn.lock
@ -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"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user