@ -1,6 +1,4 @@
|
||||
/* @flow */
|
||||
|
||||
export const READY: 'blockchain__ready' = 'blockchain__ready';
|
||||
export const CONNECTING: 'blockchain__connecting' = 'blockchain__connecting';
|
||||
export const CONNECTED: 'blockchain__connected' = 'blockchain__connected';
|
||||
export const DISCONNECTED: 'blockchain__disconnected' = 'blockchain__disconnected';
|
||||
export const UPDATE_FEE: 'blockchain__update_fee' = 'blockchain__update_fee';
|
@ -0,0 +1,171 @@
|
||||
/* @flow */
|
||||
|
||||
import TrezorConnect from 'trezor-connect';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
import type {
|
||||
TrezorDevice,
|
||||
Dispatch,
|
||||
GetState,
|
||||
PromiseAction,
|
||||
} from 'flowtype';
|
||||
import type { EthereumAccount } from 'trezor-connect';
|
||||
import type { Token } from 'reducers/TokensReducer';
|
||||
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> => {
|
||||
// get data from connect
|
||||
const txs = await TrezorConnect.ethereumGetAccountInfo({
|
||||
account: {
|
||||
address,
|
||||
block: 0,
|
||||
transactions: 0,
|
||||
balance: '0',
|
||||
nonce: 0,
|
||||
},
|
||||
coin: network,
|
||||
});
|
||||
|
||||
if (!txs.success) {
|
||||
throw new Error(txs.payload.error);
|
||||
}
|
||||
|
||||
// blockbook web3 fallback
|
||||
const web3account = await dispatch(Web3Actions.discoverAccount(address, network));
|
||||
return {
|
||||
address,
|
||||
transactions: txs.payload.transactions,
|
||||
block: txs.payload.block,
|
||||
balance: web3account.balance,
|
||||
nonce: web3account.nonce,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTokenInfo = (input: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch): Promise<NetworkToken> => dispatch(Web3Actions.getTokenInfo(input, network));
|
||||
|
||||
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));
|
||||
return gasPrice === '0' ? new BigNumber(defaultGasPrice) : new BigNumber(gasPrice);
|
||||
} catch (error) {
|
||||
return new BigNumber(defaultGasPrice);
|
||||
}
|
||||
};
|
||||
|
||||
const estimateProxy: Array<Promise<string>> = [];
|
||||
export const estimateGasLimit = (network: string, data: string, value: string, gasPrice: string): PromiseAction<string> => async (dispatch: Dispatch): Promise<string> => {
|
||||
// Since this method could be called multiple times in short period of time
|
||||
// check for pending calls in proxy and if there more than two (first is current running and the second is waiting for result of first)
|
||||
// TODO: should reject second call immediately?
|
||||
if (estimateProxy.length > 0) {
|
||||
// wait for proxy result (but do not process it)
|
||||
await estimateProxy[0];
|
||||
}
|
||||
|
||||
const call = dispatch(Web3Actions.estimateGasLimit(network, {
|
||||
to: '',
|
||||
data,
|
||||
value,
|
||||
gasPrice,
|
||||
}));
|
||||
// add current call to proxy
|
||||
estimateProxy.push(call);
|
||||
// wait for result
|
||||
const result = await call;
|
||||
// remove current call from proxy
|
||||
estimateProxy.splice(0, 1);
|
||||
// return result
|
||||
return result;
|
||||
};
|
||||
|
||||
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.address); // eslint-disable-line no-unused-vars
|
||||
await TrezorConnect.blockchainSubscribe({
|
||||
accounts,
|
||||
coin: network,
|
||||
});
|
||||
// init web3 instance if not exists
|
||||
await dispatch(Web3Actions.initWeb3(network));
|
||||
};
|
||||
|
||||
export const onBlockMined = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
// try to resolve pending transactions
|
||||
await dispatch(Web3Actions.resolvePendingTransactions(network));
|
||||
|
||||
await dispatch(Web3Actions.updateGasPrice(network));
|
||||
|
||||
const accounts: Array<any> = getState().accounts.filter(a => a.network === network);
|
||||
if (accounts.length > 0) {
|
||||
// find out which account changed
|
||||
const response = await TrezorConnect.ethereumGetAccountInfo({
|
||||
accounts,
|
||||
coin: network,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
response.payload.forEach((a, i) => {
|
||||
if (a.transactions > 0) {
|
||||
// load additional data from Web3 (balance, nonce, tokens)
|
||||
dispatch(Web3Actions.updateAccount(accounts[i], a, network));
|
||||
} else {
|
||||
// there are no new txs, just update block
|
||||
dispatch(AccountsActions.update({ ...accounts[i], block: a.block }));
|
||||
|
||||
// HACK: since blockbook can't work with smart contracts for now
|
||||
// try to update tokens balances added to this account using Web3
|
||||
dispatch(Web3Actions.updateAccountTokens(accounts[i]));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const onNotification = (/*network: string*/): PromiseAction<void> => async (): Promise<void> => {
|
||||
// todo: get transaction history here
|
||||
// console.warn("OnBlAccount", account);
|
||||
// this event can be triggered multiple times
|
||||
// // 1. check if pair [txid + address] is already in reducer
|
||||
// const network: string = payload.coin.shortcut.toLowerCase();
|
||||
// const address: string = EthereumjsUtil.toChecksumAddress(payload.tx.address);
|
||||
// const txInfo = await dispatch(Web3Actions.getPendingInfo(network, payload.tx.txid));
|
||||
|
||||
// // const exists = getState().pending.filter(p => p.id === payload.tx.txid && p.address === address);
|
||||
// const exists = getState().pending.filter(p => p.address === address);
|
||||
// if (exists.length < 1) {
|
||||
// if (txInfo) {
|
||||
// dispatch({
|
||||
// type: PENDING.ADD,
|
||||
// payload: {
|
||||
// type: 'send',
|
||||
// id: payload.tx.txid,
|
||||
// network,
|
||||
// currency: 'tETH',
|
||||
// amount: txInfo.value,
|
||||
// total: '0',
|
||||
// tx: {},
|
||||
// nonce: txInfo.nonce,
|
||||
// address,
|
||||
// rejected: false,
|
||||
// },
|
||||
// });
|
||||
// } else {
|
||||
// // tx info not found (yet?)
|
||||
// // dispatch({
|
||||
// // type: PENDING.ADD_UNKNOWN,
|
||||
// // payload: {
|
||||
// // network,
|
||||
// // ...payload.tx,
|
||||
// // }
|
||||
// // });
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
export const onError = (network: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||
dispatch(Web3Actions.disconnect(network));
|
||||
};
|
@ -0,0 +1,93 @@
|
||||
/* @flow */
|
||||
|
||||
import TrezorConnect from 'trezor-connect';
|
||||
import EthereumjsUtil from 'ethereumjs-util';
|
||||
import * as DISCOVERY from 'actions/constants/discovery';
|
||||
import * as BlockchainActions from 'actions/ethereum/BlockchainActions';
|
||||
|
||||
import type {
|
||||
PromiseAction,
|
||||
Dispatch,
|
||||
TrezorDevice,
|
||||
Network,
|
||||
Account,
|
||||
} from 'flowtype';
|
||||
import type { Discovery } from 'reducers/DiscoveryReducer';
|
||||
|
||||
|
||||
export type DiscoveryStartAction = {
|
||||
type: typeof DISCOVERY.START,
|
||||
networkType: 'ethereum',
|
||||
network: Network,
|
||||
device: TrezorDevice,
|
||||
publicKey: string,
|
||||
chainCode: string,
|
||||
basePath: Array<number>,
|
||||
};
|
||||
|
||||
// first iteration
|
||||
// generate public key for this account
|
||||
// start discovery process
|
||||
export const begin = (device: TrezorDevice, network: Network): PromiseAction<DiscoveryStartAction> => async (): Promise<DiscoveryStartAction> => {
|
||||
// get xpub from TREZOR
|
||||
const response = await TrezorConnect.getPublicKey({
|
||||
device: {
|
||||
path: device.path,
|
||||
instance: device.instance,
|
||||
state: device.state,
|
||||
},
|
||||
path: network.bip44,
|
||||
keepSession: true, // acquire and hold session
|
||||
//useEmptyPassphrase: !device.instance,
|
||||
useEmptyPassphrase: device.useEmptyPassphrase,
|
||||
network: network.name,
|
||||
});
|
||||
|
||||
// handle TREZOR response error
|
||||
if (!response.success) {
|
||||
throw new Error(response.payload.error);
|
||||
}
|
||||
|
||||
const basePath: Array<number> = response.payload.path;
|
||||
|
||||
return {
|
||||
type: DISCOVERY.START,
|
||||
networkType: 'ethereum',
|
||||
network,
|
||||
device,
|
||||
publicKey: response.payload.publicKey,
|
||||
chainCode: response.payload.chainCode,
|
||||
basePath,
|
||||
};
|
||||
};
|
||||
|
||||
export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): PromiseAction<Account> => async (dispatch: Dispatch): Promise<Account> => {
|
||||
const derivedKey = discoveryProcess.hdKey.derive(`m/${discoveryProcess.accountIndex}`);
|
||||
const path = discoveryProcess.basePath.concat(discoveryProcess.accountIndex);
|
||||
const publicAddress: string = EthereumjsUtil.publicToAddress(derivedKey.publicKey, true).toString('hex');
|
||||
const ethAddress: string = EthereumjsUtil.toChecksumAddress(publicAddress);
|
||||
const { network } = discoveryProcess;
|
||||
|
||||
// TODO: check if address was created before
|
||||
const account = await dispatch(BlockchainActions.discoverAccount(device, ethAddress, network));
|
||||
|
||||
// const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0';
|
||||
const empty = account.nonce <= 0 && account.balance === '0';
|
||||
|
||||
return {
|
||||
index: discoveryProcess.accountIndex,
|
||||
loaded: true,
|
||||
network,
|
||||
deviceID: device.features ? device.features.device_id : '0',
|
||||
deviceState: device.state || '0',
|
||||
addressPath: path,
|
||||
address: ethAddress,
|
||||
balance: account.balance,
|
||||
availableBalance: account.balance,
|
||||
sequence: account.nonce,
|
||||
nonce: account.nonce,
|
||||
block: account.block,
|
||||
transactions: account.transactions,
|
||||
empty,
|
||||
};
|
||||
};
|
@ -0,0 +1,587 @@
|
||||
/* @flow */
|
||||
import React from 'react';
|
||||
import Link from 'components/Link';
|
||||
import TrezorConnect from 'trezor-connect';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as ACCOUNT from 'actions/constants/account';
|
||||
import * as NOTIFICATION from 'actions/constants/notification';
|
||||
import * as SEND from 'actions/constants/send';
|
||||
import * as PENDING from 'actions/constants/pendingTx';
|
||||
import * as WEB3 from 'actions/constants/web3';
|
||||
import { initialState } from 'reducers/SendFormEthereumReducer';
|
||||
import { findToken } from 'reducers/TokensReducer';
|
||||
import * as reducerUtils from 'reducers/utils';
|
||||
import * as ethUtils from 'utils/ethUtils';
|
||||
|
||||
import type {
|
||||
Dispatch,
|
||||
GetState,
|
||||
State as ReducersState,
|
||||
Action,
|
||||
ThunkAction,
|
||||
AsyncAction,
|
||||
TrezorDevice,
|
||||
} from 'flowtype';
|
||||
import type { State, FeeLevel } from 'reducers/SendFormEthereumReducer';
|
||||
import * as SessionStorageActions from '../SessionStorageActions';
|
||||
import { prepareEthereumTx, serializeEthereumTx } from '../TxActions';
|
||||
import * as BlockchainActions from './BlockchainActions';
|
||||
|
||||
import * as ValidationActions from './SendFormValidationActions';
|
||||
|
||||
// list of all actions which has influence on "sendFormEthereum" reducer
|
||||
// other actions will be ignored
|
||||
const actions = [
|
||||
ACCOUNT.UPDATE_SELECTED_ACCOUNT,
|
||||
WEB3.GAS_PRICE_UPDATED,
|
||||
...Object.values(SEND).filter(v => typeof v === 'string'),
|
||||
];
|
||||
|
||||
/*
|
||||
* Called from WalletService
|
||||
*/
|
||||
export const observe = (prevState: ReducersState, action: Action): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
// ignore not listed actions
|
||||
if (actions.indexOf(action.type) < 0) return;
|
||||
|
||||
const currentState = getState();
|
||||
// do not proceed if it's not "send" url
|
||||
if (!currentState.router.location.state.send) return;
|
||||
|
||||
// if action type is SEND.VALIDATION which is called as result of this process
|
||||
// save data to session storage
|
||||
if (action.type === SEND.VALIDATION) {
|
||||
dispatch(SessionStorageActions.saveDraftTransaction());
|
||||
return;
|
||||
}
|
||||
|
||||
// if send form was not initialized
|
||||
if (currentState.sendFormEthereum.currency === '') {
|
||||
dispatch(init());
|
||||
return;
|
||||
}
|
||||
|
||||
// handle gasPrice update from backend
|
||||
// recalculate fee levels if needed
|
||||
if (action.type === WEB3.GAS_PRICE_UPDATED) {
|
||||
dispatch(ValidationActions.onGasPriceUpdated(action.network, action.gasPrice));
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldUpdate: boolean = false;
|
||||
// check if "selectedAccount" reducer changed
|
||||
shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, {
|
||||
account: ['balance', 'nonce', 'tokens'],
|
||||
});
|
||||
if (shouldUpdate && currentState.sendFormEthereum.currency !== currentState.sendFormEthereum.networkSymbol) {
|
||||
// make sure that this token is added into account
|
||||
const { account, tokens } = getState().selectedAccount;
|
||||
if (!account) return;
|
||||
const token = findToken(tokens, account.address, currentState.sendFormEthereum.currency, account.deviceState);
|
||||
if (!token) {
|
||||
// token not found, re-init form
|
||||
dispatch(init());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// check if "sendFormEthereum" reducer changed
|
||||
if (!shouldUpdate) {
|
||||
shouldUpdate = reducerUtils.observeChanges(prevState.sendFormEthereum, currentState.sendFormEthereum);
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
const validated = dispatch(ValidationActions.validation());
|
||||
dispatch({
|
||||
type: SEND.VALIDATION,
|
||||
networkType: 'ethereum',
|
||||
state: validated,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from "observe" action
|
||||
* Initialize "sendFormEthereum" reducer data
|
||||
* Get data either from session storage or "selectedAccount" reducer
|
||||
*/
|
||||
export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
} = getState().selectedAccount;
|
||||
|
||||
if (!account || !network) return;
|
||||
|
||||
const stateFromStorage = dispatch(SessionStorageActions.loadEthereumDraftTransaction());
|
||||
if (stateFromStorage) {
|
||||
// TODO: consider if current gasPrice should be set here as "recommendedGasPrice"
|
||||
dispatch({
|
||||
type: SEND.INIT,
|
||||
networkType: 'ethereum',
|
||||
state: stateFromStorage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const gasPrice: BigNumber = await dispatch(BlockchainActions.getGasPrice(network.shortcut, network.defaultGasPrice));
|
||||
const gasLimit = network.defaultGasLimit.toString();
|
||||
const feeLevels = ValidationActions.getFeeLevels(network.symbol, gasPrice, gasLimit);
|
||||
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel);
|
||||
|
||||
dispatch({
|
||||
type: SEND.INIT,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...initialState,
|
||||
networkName: network.shortcut,
|
||||
networkSymbol: network.symbol,
|
||||
currency: network.symbol,
|
||||
feeLevels,
|
||||
selectedFeeLevel,
|
||||
recommendedGasPrice: gasPrice.toString(),
|
||||
gasLimit,
|
||||
gasPrice: gasPrice.toString(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI from "advanced" button
|
||||
*/
|
||||
export const toggleAdvanced = (): Action => ({
|
||||
type: SEND.TOGGLE_ADVANCED,
|
||||
});
|
||||
|
||||
/*
|
||||
* Called from UI on "address" field change
|
||||
*/
|
||||
export const onAddressChange = (address: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state: State = getState().sendFormEthereum;
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
untouched: false,
|
||||
touched: { ...state.touched, address: true },
|
||||
address,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI on "amount" field change
|
||||
*/
|
||||
export const onAmountChange = (amount: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state = getState().sendFormEthereum;
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
untouched: false,
|
||||
touched: { ...state.touched, amount: true },
|
||||
setMax: false,
|
||||
amount,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI on "currency" selection change
|
||||
*/
|
||||
export const onCurrencyChange = (currency: { value: string, label: string }): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
} = getState().selectedAccount;
|
||||
if (!account || !network) return;
|
||||
|
||||
const state = getState().sendFormEthereum;
|
||||
|
||||
const isToken = currency.value !== state.networkSymbol;
|
||||
const gasLimit = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString();
|
||||
|
||||
const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, gasLimit, state.selectedFeeLevel);
|
||||
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
|
||||
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
currency: currency.value,
|
||||
feeLevels,
|
||||
selectedFeeLevel,
|
||||
gasLimit,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI from "set max" button
|
||||
*/
|
||||
export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state = getState().sendFormEthereum;
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
untouched: false,
|
||||
touched: { ...state.touched, amount: true },
|
||||
setMax: !state.setMax,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI on "fee" selection change
|
||||
*/
|
||||
export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state = getState().sendFormEthereum;
|
||||
|
||||
const isCustom = feeLevel.value === 'Custom';
|
||||
let newGasLimit = state.gasLimit;
|
||||
let newGasPrice = state.gasPrice;
|
||||
const advanced = isCustom ? true : state.advanced;
|
||||
|
||||
if (!isCustom) {
|
||||
// if selected fee is not custom
|
||||
// update gasLimit to default and gasPrice to selected value
|
||||
const { network } = getState().selectedAccount;
|
||||
if (!network) return;
|
||||
const isToken = state.currency !== state.networkSymbol;
|
||||
if (isToken) {
|
||||
newGasLimit = network.defaultGasLimitTokens.toString();
|
||||
} else {
|
||||
// corner case: gas limit was changed by user OR by "estimateGasPrice" action
|
||||
// leave gasLimit as it is
|
||||
newGasLimit = state.touched.gasLimit ? state.gasLimit : network.defaultGasLimit.toString();
|
||||
}
|
||||
newGasPrice = feeLevel.gasPrice;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
advanced,
|
||||
selectedFeeLevel: feeLevel,
|
||||
gasLimit: newGasLimit,
|
||||
gasPrice: newGasPrice,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* 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 state: State = getState().sendFormEthereum;
|
||||
const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, state.gasLimit, state.selectedFeeLevel);
|
||||
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
|
||||
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
feeLevels,
|
||||
selectedFeeLevel,
|
||||
gasPrice: selectedFeeLevel.gasPrice,
|
||||
gasPriceNeedsUpdate: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI on "gas price" field change
|
||||
*/
|
||||
export const onGasPriceChange = (gasPrice: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state: State = getState().sendFormEthereum;
|
||||
// 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: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
untouched: false,
|
||||
touched: { ...state.touched, gasPrice: true },
|
||||
gasPrice,
|
||||
selectedFeeLevel: newSelectedFeeLevel,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI on "data" field change
|
||||
* OR from "estimateGasPrice" action
|
||||
*/
|
||||
export const onGasLimitChange = (gasLimit: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const { network } = getState().selectedAccount;
|
||||
if (!network) return;
|
||||
const state: State = getState().sendFormEthereum;
|
||||
// recalculate feeLevels with recommended gasPrice
|
||||
const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, gasLimit, state.selectedFeeLevel);
|
||||
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
|
||||
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
calculatingGasLimit: false,
|
||||
untouched: false,
|
||||
touched: { ...state.touched, gasLimit: true },
|
||||
gasLimit,
|
||||
feeLevels,
|
||||
selectedFeeLevel,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI on "nonce" field change
|
||||
*/
|
||||
export const onNonceChange = (nonce: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state: State = getState().sendFormEthereum;
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
untouched: false,
|
||||
touched: { ...state.touched, nonce: true },
|
||||
nonce,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI on "data" field change
|
||||
*/
|
||||
export const onDataChange = (data: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state: State = getState().sendFormEthereum;
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
calculatingGasLimit: true,
|
||||
untouched: false,
|
||||
touched: { ...state.touched, data: true },
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(estimateGasPrice());
|
||||
};
|
||||
|
||||
export const setDefaultGasLimit = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state: State = getState().sendFormEthereum;
|
||||
const { network } = getState().selectedAccount;
|
||||
if (!network) return;
|
||||
|
||||
const isToken = state.currency !== state.networkSymbol;
|
||||
const gasLimit = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString();
|
||||
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
calculatingGasLimit: false,
|
||||
untouched: false,
|
||||
touched: { ...state.touched, gasLimit: false },
|
||||
gasLimit,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Internal method
|
||||
* Called from "onDataChange" action
|
||||
* try to asynchronously download data from backend
|
||||
*/
|
||||
const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const state: State = getState().sendFormEthereum;
|
||||
const { network } = getState().selectedAccount;
|
||||
if (!network) {
|
||||
// stop "calculatingGasLimit" process
|
||||
dispatch(onGasLimitChange(state.gasLimit));
|
||||
return;
|
||||
}
|
||||
|
||||
const requestedData = state.data;
|
||||
if (!ethUtils.isHex(requestedData)) {
|
||||
// stop "calculatingGasLimit" process
|
||||
dispatch(onGasLimitChange(requestedData.length > 0 ? state.gasLimit : network.defaultGasLimit.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.data.length < 1) {
|
||||
// set default
|
||||
dispatch(onGasLimitChange(network.defaultGasLimit.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
const gasLimit = await dispatch(BlockchainActions.estimateGasLimit(network.shortcut, state.data, state.amount, state.gasPrice));
|
||||
|
||||
// double check "data" field
|
||||
// possible race condition when data changed before backend respond
|
||||
if (getState().sendFormEthereum.data === requestedData) {
|
||||
dispatch(onGasLimitChange(gasLimit));
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI from "send" button
|
||||
*/
|
||||
export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
pending,
|
||||
} = getState().selectedAccount;
|
||||
|
||||
if (!account || !network) return;
|
||||
|
||||
const currentState: State = getState().sendFormEthereum;
|
||||
|
||||
const isToken: boolean = currentState.currency !== currentState.networkSymbol;
|
||||
const pendingNonce: number = reducerUtils.getPendingSequence(pending);
|
||||
const nonce = pendingNonce > 0 && pendingNonce >= account.nonce ? pendingNonce : account.nonce;
|
||||
|
||||
const txData = await dispatch(prepareEthereumTx({
|
||||
network: network.shortcut,
|
||||
token: isToken ? findToken(getState().tokens, account.address, currentState.currency, account.deviceState) : null,
|
||||
from: account.address,
|
||||
to: currentState.address,
|
||||
amount: currentState.amount,
|
||||
data: currentState.data,
|
||||
gasLimit: currentState.gasLimit,
|
||||
gasPrice: currentState.gasPrice,
|
||||
nonce,
|
||||
}));
|
||||
|
||||
const selected: ?TrezorDevice = getState().wallet.selectedDevice;
|
||||
if (!selected) return;
|
||||
|
||||
const signedTransaction = await TrezorConnect.ethereumSignTransaction({
|
||||
device: {
|
||||
path: selected.path,
|
||||
instance: selected.instance,
|
||||
state: selected.state,
|
||||
},
|
||||
// useEmptyPassphrase: !selected.instance,
|
||||
useEmptyPassphrase: selected.useEmptyPassphrase,
|
||||
path: account.addressPath,
|
||||
transaction: txData,
|
||||
});
|
||||
|
||||
if (!signedTransaction || !signedTransaction.success) {
|
||||
dispatch({
|
||||
type: NOTIFICATION.ADD,
|
||||
payload: {
|
||||
type: 'error',
|
||||
title: 'Transaction error',
|
||||
message: signedTransaction.payload.error,
|
||||
cancelable: true,
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
txData.r = signedTransaction.payload.r;
|
||||
txData.s = signedTransaction.payload.s;
|
||||
txData.v = signedTransaction.payload.v;
|
||||
|
||||
try {
|
||||
const serializedTx: string = await dispatch(serializeEthereumTx(txData));
|
||||
const push = await TrezorConnect.pushTransaction({
|
||||
tx: serializedTx,
|
||||
coin: network.shortcut,
|
||||
});
|
||||
|
||||
if (!push.success) {
|
||||
throw new Error(push.payload.error);
|
||||
}
|
||||
|
||||
const { txid } = push.payload;
|
||||
|
||||
dispatch({ type: SEND.TX_COMPLETE });
|
||||
|
||||
dispatch({
|
||||
type: PENDING.ADD,
|
||||
payload: {
|
||||
type: 'send',
|
||||
deviceState: account.deviceState,
|
||||
sequence: nonce,
|
||||
hash: txid,
|
||||
network: account.network,
|
||||
address: account.address,
|
||||
currency: currentState.currency,
|
||||
amount: currentState.amount,
|
||||
total: currentState.total,
|
||||
fee: '0', // TODO: calculate fee
|
||||
},
|
||||
});
|
||||
|
||||
// clear session storage
|
||||
dispatch(SessionStorageActions.clear());
|
||||
|
||||
// reset form
|
||||
dispatch(init());
|
||||
|
||||
dispatch({
|
||||
type: NOTIFICATION.ADD,
|
||||
payload: {
|
||||
type: 'success',
|
||||
title: 'Transaction success',
|
||||
message: <Link href={`${network.explorer.tx}${txid}`} isGreen>See transaction detail</Link>,
|
||||
cancelable: true,
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: NOTIFICATION.ADD,
|
||||
payload: {
|
||||
type: 'error',
|
||||
title: 'Transaction error',
|
||||
message: error.message || error,
|
||||
cancelable: true,
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
toggleAdvanced,
|
||||
onAddressChange,
|
||||
onAmountChange,
|
||||
onCurrencyChange,
|
||||
onSetMax,
|
||||
onFeeLevelChange,
|
||||
updateFeeLevels,
|
||||
onGasPriceChange,
|
||||
onGasLimitChange,
|
||||
setDefaultGasLimit,
|
||||
onNonceChange,
|
||||
onDataChange,
|
||||
onSend,
|
||||
};
|
@ -0,0 +1,90 @@
|
||||
/* @flow */
|
||||
|
||||
import TrezorConnect from 'trezor-connect';
|
||||
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 type { BlockchainNotification } from 'trezor-connect';
|
||||
import type {
|
||||
Dispatch,
|
||||
GetState,
|
||||
PromiseAction,
|
||||
} from 'flowtype';
|
||||
|
||||
|
||||
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.address);
|
||||
await TrezorConnect.blockchainSubscribe({
|
||||
accounts,
|
||||
coin: network,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const onBlockMined = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const fee = await TrezorConnect.blockchainGetFee({
|
||||
coin: network,
|
||||
});
|
||||
if (!fee.success) return;
|
||||
|
||||
const blockchain = getState().blockchain.find(b => b.shortcut === network);
|
||||
if (!blockchain) return;
|
||||
|
||||
if (fee.payload !== blockchain.fee) {
|
||||
dispatch({
|
||||
type: BLOCKCHAIN.UPDATE_FEE,
|
||||
shortcut: network,
|
||||
fee: fee.payload,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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.address === notification.address);
|
||||
if (!account) return;
|
||||
|
||||
if (notification.status === 'pending') {
|
||||
dispatch({
|
||||
type: PENDING.ADD,
|
||||
payload: {
|
||||
type: notification.type,
|
||||
deviceState: account.deviceState,
|
||||
sequence: account.sequence,
|
||||
hash: notification.hash,
|
||||
network: account.network,
|
||||
address: account.address,
|
||||
currency: account.network,
|
||||
amount: notification.amount,
|
||||
total: notification.amount,
|
||||
fee: notification.fee,
|
||||
},
|
||||
});
|
||||
|
||||
// todo: replace "send success" notification with link to explorer
|
||||
} else if (notification.status === 'confirmed') {
|
||||
dispatch({
|
||||
type: PENDING.TX_RESOLVED,
|
||||
hash: notification.hash,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedAccount = await TrezorConnect.rippleGetAccountInfo({
|
||||
account: {
|
||||
address: account.address,
|
||||
block: account.block,
|
||||
history: false,
|
||||
},
|
||||
});
|
||||
if (!updatedAccount.success) return;
|
||||
|
||||
dispatch(AccountsActions.update({
|
||||
...account,
|
||||
balance: toDecimalAmount(updatedAccount.payload.balance, 6),
|
||||
availableDevice: toDecimalAmount(updatedAccount.payload.availableBalance, 6),
|
||||
block: updatedAccount.payload.block,
|
||||
sequence: updatedAccount.payload.sequence,
|
||||
}));
|
||||
};
|
@ -0,0 +1,77 @@
|
||||
/* @flow */
|
||||
|
||||
import TrezorConnect from 'trezor-connect';
|
||||
import * as DISCOVERY from 'actions/constants/discovery';
|
||||
import { toDecimalAmount } from 'utils/formatUtils';
|
||||
|
||||
import type {
|
||||
PromiseAction,
|
||||
GetState,
|
||||
Dispatch,
|
||||
TrezorDevice,
|
||||
Network,
|
||||
Account,
|
||||
} from 'flowtype';
|
||||
import type { Discovery } from 'reducers/DiscoveryReducer';
|
||||
|
||||
export type DiscoveryStartAction = {
|
||||
type: typeof DISCOVERY.START,
|
||||
networkType: 'ripple',
|
||||
network: Network,
|
||||
device: TrezorDevice,
|
||||
};
|
||||
|
||||
export const begin = (device: TrezorDevice, network: Network): PromiseAction<DiscoveryStartAction> => async (): Promise<DiscoveryStartAction> => ({
|
||||
type: DISCOVERY.START,
|
||||
networkType: 'ripple',
|
||||
network,
|
||||
device,
|
||||
});
|
||||
|
||||
export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): PromiseAction<Account> => async (dispatch: Dispatch, getState: GetState): Promise<Account> => {
|
||||
const { config } = getState().localStorage;
|
||||
const network = config.networks.find(c => c.shortcut === discoveryProcess.network);
|
||||
if (!network) throw new Error('Discovery network not found');
|
||||
|
||||
const { accountIndex } = discoveryProcess;
|
||||
const path = network.bip44.slice(0).replace('a', accountIndex.toString());
|
||||
|
||||
const response = await TrezorConnect.rippleGetAccountInfo({
|
||||
device: {
|
||||
path: device.path,
|
||||
instance: device.instance,
|
||||
state: device.state,
|
||||
},
|
||||
account: {
|
||||
path,
|
||||
block: 0,
|
||||
},
|
||||
keepSession: true, // acquire and hold session
|
||||
useEmptyPassphrase: device.useEmptyPassphrase,
|
||||
});
|
||||
|
||||
// handle TREZOR response error
|
||||
if (!response.success) {
|
||||
throw new Error(response.payload.error);
|
||||
}
|
||||
|
||||
const account = response.payload;
|
||||
const empty = account.sequence <= 0 && account.balance === '0';
|
||||
|
||||
return {
|
||||
index: discoveryProcess.accountIndex,
|
||||
loaded: true,
|
||||
network: network.shortcut,
|
||||
deviceID: device.features ? device.features.device_id : '0',
|
||||
deviceState: device.state || '0',
|
||||
addressPath: account.path || [],
|
||||
address: account.address,
|
||||
balance: toDecimalAmount(account.balance, 6),
|
||||
availableBalance: toDecimalAmount(account.availableBalance, 6),
|
||||
sequence: account.sequence,
|
||||
nonce: account.sequence,
|
||||
block: account.block,
|
||||
transactions: account.transactions,
|
||||
empty,
|
||||
};
|
||||
};
|
@ -0,0 +1,265 @@
|
||||
/* @flow */
|
||||
import React from 'react';
|
||||
import Link from 'components/Link';
|
||||
import TrezorConnect from 'trezor-connect';
|
||||
import * as NOTIFICATION from 'actions/constants/notification';
|
||||
import * as SEND from 'actions/constants/send';
|
||||
import * as BLOCKCHAIN from 'actions/constants/blockchain';
|
||||
import { initialState } from 'reducers/SendFormRippleReducer';
|
||||
import * as reducerUtils from 'reducers/utils';
|
||||
import { fromDecimalAmount } from 'utils/formatUtils';
|
||||
|
||||
import type {
|
||||
Dispatch,
|
||||
GetState,
|
||||
State as ReducersState,
|
||||
Action,
|
||||
ThunkAction,
|
||||
AsyncAction,
|
||||
TrezorDevice,
|
||||
} from 'flowtype';
|
||||
import type { State } from 'reducers/SendFormRippleReducer';
|
||||
import * as SessionStorageActions from '../SessionStorageActions';
|
||||
|
||||
import * as ValidationActions from './SendFormValidationActions';
|
||||
|
||||
/*
|
||||
* Called from WalletService
|
||||
*/
|
||||
export const observe = (prevState: ReducersState, action: Action): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const currentState = getState();
|
||||
|
||||
// if action type is SEND.VALIDATION which is called as result of this process
|
||||
// save data to session storage
|
||||
if (action.type === SEND.VALIDATION) {
|
||||
dispatch(SessionStorageActions.saveDraftTransaction());
|
||||
return;
|
||||
}
|
||||
|
||||
// if send form was not initialized
|
||||
if (currentState.sendFormRipple.networkSymbol === '') {
|
||||
dispatch(init());
|
||||
return;
|
||||
}
|
||||
|
||||
// handle gasPrice update from backend
|
||||
// recalculate fee levels if needed
|
||||
if (action.type === BLOCKCHAIN.UPDATE_FEE) {
|
||||
dispatch(ValidationActions.onFeeUpdated(action.shortcut, action.fee));
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldUpdate: boolean = false;
|
||||
// check if "selectedAccount" reducer changed
|
||||
shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, {
|
||||
account: ['balance', 'nonce'],
|
||||
});
|
||||
|
||||
// check if "sendForm" reducer changed
|
||||
if (!shouldUpdate) {
|
||||
shouldUpdate = reducerUtils.observeChanges(prevState.sendFormRipple, currentState.sendFormRipple);
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
const validated = dispatch(ValidationActions.validation());
|
||||
dispatch({
|
||||
type: SEND.VALIDATION,
|
||||
networkType: 'ripple',
|
||||
state: validated,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from "observe" action
|
||||
* Initialize "sendFormRipple" reducer data
|
||||
* Get data either from session storage or "selectedAccount" reducer
|
||||
*/
|
||||
export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
} = getState().selectedAccount;
|
||||
|
||||
if (!account || !network) return;
|
||||
|
||||
const stateFromStorage = dispatch(SessionStorageActions.loadRippleDraftTransaction());
|
||||
if (stateFromStorage) {
|
||||
dispatch({
|
||||
type: SEND.INIT,
|
||||
networkType: 'ripple',
|
||||
state: stateFromStorage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const feeLevels = dispatch(ValidationActions.getFeeLevels(network.symbol));
|
||||
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel);
|
||||
|
||||
dispatch({
|
||||
type: SEND.INIT,
|
||||
networkType: 'ripple',
|
||||
state: {
|
||||
...initialState,
|
||||
networkName: network.shortcut,
|
||||
networkSymbol: network.symbol,
|
||||
feeLevels,
|
||||
selectedFeeLevel,
|
||||
sequence: '1',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI on "address" field change
|
||||
*/
|
||||
export const onAddressChange = (address: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state: State = getState().sendFormRipple;
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ripple',
|
||||
state: {
|
||||
...state,
|
||||
untouched: false,
|
||||
touched: { ...state.touched, address: true },
|
||||
address,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI on "amount" field change
|
||||
*/
|
||||
export const onAmountChange = (amount: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state = getState().sendFormRipple;
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ripple',
|
||||
state: {
|
||||
...state,
|
||||
untouched: false,
|
||||
touched: { ...state.touched, amount: true },
|
||||
setMax: false,
|
||||
amount,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Called from UI from "set max" button
|
||||
*/
|
||||
export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state = getState().sendFormRipple;
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ripple',
|
||||
state: {
|
||||
...state,
|
||||
untouched: false,
|
||||
touched: { ...state.touched, amount: true },
|
||||
setMax: !state.setMax,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Called from UI from "send" button
|
||||
*/
|
||||
export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
} = getState().selectedAccount;
|
||||
|
||||
const selected: ?TrezorDevice = getState().wallet.selectedDevice;
|
||||
if (!selected) return;
|
||||
|
||||
if (!account || !network) return;
|
||||
|
||||
const blockchain = getState().blockchain.find(b => b.shortcut === account.network);
|
||||
if (!blockchain) return;
|
||||
|
||||
const currentState: State = getState().sendFormRipple;
|
||||
const amount = fromDecimalAmount(currentState.amount, 6);
|
||||
|
||||
const signedTransaction = await TrezorConnect.rippleSignTransaction({
|
||||
device: {
|
||||
path: selected.path,
|
||||
instance: selected.instance,
|
||||
state: selected.state,
|
||||
},
|
||||
useEmptyPassphrase: selected.useEmptyPassphrase,
|
||||
path: account.addressPath,
|
||||
transaction: {
|
||||
fee: blockchain.fee, // Fee must be in the range of 10 to 10,000 drops
|
||||
flags: 0x80000000,
|
||||
sequence: account.sequence,
|
||||
payment: {
|
||||
amount,
|
||||
destination: currentState.address,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!signedTransaction || !signedTransaction.success) {
|
||||
dispatch({
|
||||
type: NOTIFICATION.ADD,
|
||||
payload: {
|
||||
type: 'error',
|
||||
title: 'Transaction error',
|
||||
message: signedTransaction.payload.error,
|
||||
cancelable: true,
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const push = await TrezorConnect.pushTransaction({
|
||||
tx: signedTransaction.payload.serializedTx,
|
||||
coin: network.shortcut,
|
||||
});
|
||||
|
||||
if (!push.success) {
|
||||
dispatch({
|
||||
type: NOTIFICATION.ADD,
|
||||
payload: {
|
||||
type: 'error',
|
||||
title: 'Transaction error',
|
||||
message: push.payload.error,
|
||||
cancelable: true,
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { txid } = push.payload;
|
||||
|
||||
dispatch({ type: SEND.TX_COMPLETE });
|
||||
|
||||
// clear session storage
|
||||
dispatch(SessionStorageActions.clear());
|
||||
|
||||
// reset form
|
||||
dispatch(init());
|
||||
|
||||
dispatch({
|
||||
type: NOTIFICATION.ADD,
|
||||
payload: {
|
||||
type: 'success',
|
||||
title: 'Transaction success',
|
||||
message: <Link href={`${network.explorer.tx}${txid}`} isGreen>See transaction detail</Link>,
|
||||
cancelable: true,
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
onAddressChange,
|
||||
onAmountChange,
|
||||
onSetMax,
|
||||
onSend,
|
||||
};
|
@ -0,0 +1,254 @@
|
||||
/* @flow */
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as SEND from 'actions/constants/send';
|
||||
import { findDevice, getPendingAmount } from 'reducers/utils';
|
||||
import { toDecimalAmount } from 'utils/formatUtils';
|
||||
|
||||
import type {
|
||||
Dispatch,
|
||||
GetState,
|
||||
PayloadAction,
|
||||
} from 'flowtype';
|
||||
import type { State, FeeLevel } from 'reducers/SendFormRippleReducer';
|
||||
|
||||
// general regular expressions
|
||||
const XRP_ADDRESS_RE = new RegExp('^r[1-9A-HJ-NP-Za-km-z]{25,34}$');
|
||||
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})$');
|
||||
|
||||
/*
|
||||
* Called from SendFormActions.observe
|
||||
* Reaction for BLOCKCHAIN.FEE_UPDATED action
|
||||
*/
|
||||
export const onFeeUpdated = (network: string, fee: string): 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
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ripple',
|
||||
state: {
|
||||
...state,
|
||||
feeNeedsUpdate: true,
|
||||
recommendedFee: fee,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// automatically update feeLevels and gasPrice
|
||||
const feeLevels = dispatch(getFeeLevels(state.networkSymbol));
|
||||
const selectedFeeLevel = getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ripple',
|
||||
state: {
|
||||
...state,
|
||||
feeNeedsUpdate: false,
|
||||
recommendedFee: fee,
|
||||
gasPrice: selectedFeeLevel.gasPrice,
|
||||
feeLevels,
|
||||
selectedFeeLevel,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Recalculate amount, total and fees
|
||||
*/
|
||||
export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||
// clone deep nested object
|
||||
// to avoid overrides across state history
|
||||
let state: State = JSON.parse(JSON.stringify(getState().sendFormRipple));
|
||||
// reset errors
|
||||
state.errors = {};
|
||||
state.warnings = {};
|
||||
state.infos = {};
|
||||
state = dispatch(recalculateTotalAmount(state));
|
||||
state = dispatch(addressValidation(state));
|
||||
state = dispatch(addressLabel(state));
|
||||
state = dispatch(amountValidation(state));
|
||||
return state;
|
||||
};
|
||||
|
||||
const recalculateTotalAmount = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||
const {
|
||||
account,
|
||||
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);
|
||||
|
||||
const state = { ...$state };
|
||||
|
||||
if (state.setMax) {
|
||||
const pendingAmount = getPendingAmount(pending, state.networkSymbol, false);
|
||||
const b = new BigNumber(account.balance).minus(pendingAmount);
|
||||
state.amount = calculateMaxAmount(b, fee);
|
||||
}
|
||||
|
||||
state.total = calculateTotal(state.amount, fee);
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Address value validation
|
||||
*/
|
||||
const addressValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||
const state = { ...$state };
|
||||
if (!state.touched.address) return state;
|
||||
|
||||
const { account, network } = getState().selectedAccount;
|
||||
if (!account || !network) return state;
|
||||
|
||||
const { address } = state;
|
||||
|
||||
if (address.length < 1) {
|
||||
state.errors.address = 'Address is not set';
|
||||
} else if (!address.match(XRP_ADDRESS_RE)) {
|
||||
state.errors.address = 'Address is not valid';
|
||||
} else if (address.toLowerCase() === account.address.toLowerCase()) {
|
||||
state.errors.address = 'Cannot send to myself';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Address label assignation
|
||||
*/
|
||||
const addressLabel = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||
const state = { ...$state };
|
||||
if (!state.touched.address || state.errors.address) return state;
|
||||
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
} = getState().selectedAccount;
|
||||
if (!account || !network) return state;
|
||||
const { address } = state;
|
||||
|
||||
const savedAccounts = getState().accounts.filter(a => a.address.toLowerCase() === address.toLowerCase());
|
||||
if (savedAccounts.length > 0) {
|
||||
// check if found account belongs to this network
|
||||
const currentNetworkAccount = savedAccounts.find(a => a.network === network.shortcut);
|
||||
if (currentNetworkAccount) {
|
||||
const device = findDevice(getState().devices, currentNetworkAccount.deviceID, currentNetworkAccount.deviceState);
|
||||
if (device) {
|
||||
state.infos.address = `${device.instanceLabel} Account #${(currentNetworkAccount.index + 1)}`;
|
||||
}
|
||||
} else {
|
||||
// corner-case: the same derivation path is used on different networks
|
||||
const otherNetworkAccount = savedAccounts[0];
|
||||
const device = findDevice(getState().devices, otherNetworkAccount.deviceID, otherNetworkAccount.deviceState);
|
||||
const { networks } = getState().localStorage.config;
|
||||
const otherNetwork = networks.find(c => c.shortcut === otherNetworkAccount.network);
|
||||
if (device && otherNetwork) {
|
||||
state.warnings.address = `Looks like it's ${device.instanceLabel} Account #${(otherNetworkAccount.index + 1)} address of ${otherNetwork.name} network`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Amount value validation
|
||||
*/
|
||||
const amountValidation = ($state: State): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||
const state = { ...$state };
|
||||
if (!state.touched.amount) return state;
|
||||
|
||||
const {
|
||||
account,
|
||||
pending,
|
||||
} = getState().selectedAccount;
|
||||
if (!account) return state;
|
||||
|
||||
const { amount } = state;
|
||||
if (amount.length < 1) {
|
||||
state.errors.amount = 'Amount is not set';
|
||||
} else if (amount.length > 0 && !amount.match(NUMBER_RE)) {
|
||||
state.errors.amount = 'Amount is not a number';
|
||||
} else {
|
||||
const pendingAmount: BigNumber = getPendingAmount(pending, state.networkSymbol);
|
||||
|
||||
if (!state.amount.match(XRP_6_RE)) {
|
||||
state.errors.amount = 'Maximum 6 decimals allowed';
|
||||
} else if (new BigNumber(state.total).greaterThan(new BigNumber(account.balance).minus(pendingAmount))) {
|
||||
state.errors.amount = 'Not enough funds';
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* UTILITIES
|
||||
*/
|
||||
|
||||
const calculateTotal = (amount: string, fee: string): string => {
|
||||
try {
|
||||
return new BigNumber(amount).plus(fee).toString(10);
|
||||
} catch (error) {
|
||||
return '0';
|
||||
}
|
||||
};
|
||||
|
||||
const calculateMaxAmount = (balance: BigNumber, fee: string): string => {
|
||||
try {
|
||||
// TODO - minus pendings
|
||||
const max = balance.minus(fee);
|
||||
if (max.lessThan(0)) return '0';
|
||||
return max.toString(10);
|
||||
} catch (error) {
|
||||
return '0';
|
||||
}
|
||||
};
|
||||
|
||||
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}`,
|
||||
}];
|
||||
}
|
||||
|
||||
const xrpDrops = toDecimalAmount(blockchain.fee, 6);
|
||||
|
||||
// TODO: calc fee levels
|
||||
return [{
|
||||
value: 'Normal',
|
||||
gasPrice: xrpDrops,
|
||||
label: `${xrpDrops} ${symbol}`,
|
||||
}];
|
||||
};
|
||||
|
||||
// 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;
|
||||
selectedFeeLevel = feeLevels.find(f => f.value === value);
|
||||
if (!selectedFeeLevel) {
|
||||
// fallback to default
|
||||
selectedFeeLevel = feeLevels.find(f => f.value === 'Normal');
|
||||
}
|
||||
return selectedFeeLevel || selected;
|
||||
};
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 3.0 KiB |
@ -0,0 +1,70 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import colors from 'config/colors';
|
||||
import icons from 'config/icons';
|
||||
import Icon from 'components/Icon';
|
||||
import Link from 'components/Link';
|
||||
import Button from 'components/Button';
|
||||
import { H2 } from 'components/Heading';
|
||||
import P from 'components/Paragraph';
|
||||
import coins from 'constants/coins';
|
||||
|
||||
import CardanoImage from './images/cardano.png';
|
||||
import type { Props as BaseProps } from '../../Container';
|
||||
|
||||
type Props = {
|
||||
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>;
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
max-width: 620px;
|
||||
padding: 30px 48px;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
margin: 10px 0 10px 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 10px;
|
||||
`;
|
||||
|
||||
const Img = styled.img`
|
||||
display: block;
|
||||
max-width: 100px;
|
||||
margin: 0 auto;
|
||||
height: auto;
|
||||
padding-bottom: 20px;
|
||||
`;
|
||||
|
||||
const CardanoWallet = (props: Props) => (
|
||||
<Wrapper>
|
||||
<StyledLink onClick={props.onCancel}>
|
||||
<Icon
|
||||
size={20}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
icon={icons.CLOSE}
|
||||
/>
|
||||
</StyledLink>
|
||||
<Img src={CardanoImage} />
|
||||
<H2>Cardano wallet</H2>
|
||||
<P isSmaller>You will be redirected to external wallet</P>
|
||||
|
||||
<Link href={coins.find(i => i.id === 'ada').url}>
|
||||
<StyledButton>Go to external wallet</StyledButton>
|
||||
</Link>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
CardanoWallet.propTypes = {
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CardanoWallet;
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
After Width: | Height: | Size: 6.3 KiB |
@ -0,0 +1,70 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import colors from 'config/colors';
|
||||
import icons from 'config/icons';
|
||||
import Icon from 'components/Icon';
|
||||
import Link from 'components/Link';
|
||||
import Button from 'components/Button';
|
||||
import { H2 } from 'components/Heading';
|
||||
import P from 'components/Paragraph';
|
||||
import coins from 'constants/coins';
|
||||
|
||||
import StellarImage from './images/xlm.png';
|
||||
import type { Props as BaseProps } from '../../Container';
|
||||
|
||||
type Props = {
|
||||
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>;
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
max-width: 620px;
|
||||
padding: 30px 48px;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
margin: 10px 0 10px 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 10px;
|
||||
`;
|
||||
|
||||
const Img = styled.img`
|
||||
display: block;
|
||||
max-width: 100px;
|
||||
margin: 0 auto;
|
||||
height: auto;
|
||||
padding-bottom: 20px;
|
||||
`;
|
||||
|
||||
const StellarWallet = (props: Props) => (
|
||||
<Wrapper>
|
||||
<StyledLink onClick={props.onCancel}>
|
||||
<Icon
|
||||
size={20}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
icon={icons.CLOSE}
|
||||
/>
|
||||
</StyledLink>
|
||||
<Img src={StellarImage} />
|
||||
<H2>Stellar wallet</H2>
|
||||
<P isSmaller>You will be redirected to external wallet</P>
|
||||
|
||||
<Link href={coins.find(i => i.id === 'xlm').url}>
|
||||
<StyledButton>Go to external wallet</StyledButton>
|
||||
</Link>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
StellarWallet.propTypes = {
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default StellarWallet;
|
@ -0,0 +1,99 @@
|
||||
/* @flow */
|
||||
|
||||
import * as SEND from 'actions/constants/send';
|
||||
import * as ACCOUNT from 'actions/constants/account';
|
||||
|
||||
import type { Action } from 'flowtype';
|
||||
|
||||
export type FeeLevel = {
|
||||
label: string;
|
||||
gasPrice: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type State = {
|
||||
+networkName: string;
|
||||
+networkSymbol: string;
|
||||
|
||||
// form fields
|
||||
advanced: boolean;
|
||||
untouched: boolean; // set to true when user made any changes in form
|
||||
touched: {[k: string]: boolean};
|
||||
address: string;
|
||||
amount: string;
|
||||
setMax: boolean;
|
||||
feeLevels: Array<FeeLevel>;
|
||||
selectedFeeLevel: FeeLevel;
|
||||
recommendedFee: string;
|
||||
feeNeedsUpdate: boolean;
|
||||
sequence: string;
|
||||
total: string;
|
||||
|
||||
errors: {[k: string]: string};
|
||||
warnings: {[k: string]: string};
|
||||
infos: {[k: string]: string};
|
||||
|
||||
sending: boolean;
|
||||
}
|
||||
|
||||
|
||||
export const initialState: State = {
|
||||
networkName: '',
|
||||
networkSymbol: '',
|
||||
|
||||
advanced: false,
|
||||
untouched: true,
|
||||
touched: {},
|
||||
address: '',
|
||||
amount: '',
|
||||
setMax: false,
|
||||
feeLevels: [],
|
||||
selectedFeeLevel: {
|
||||
label: 'Normal',
|
||||
gasPrice: '0',
|
||||
value: 'Normal',
|
||||
},
|
||||
recommendedFee: '0',
|
||||
feeNeedsUpdate: false,
|
||||
sequence: '0',
|
||||
total: '0',
|
||||
|
||||
errors: {},
|
||||
warnings: {},
|
||||
infos: {},
|
||||
|
||||
sending: false,
|
||||
};
|
||||
|
||||
export default (state: State = initialState, action: Action): State => {
|
||||
if (action.type === ACCOUNT.DISPOSE) return initialState;
|
||||
if (!action.networkType || action.networkType !== 'ripple') return state;
|
||||
|
||||
switch (action.type) {
|
||||
case SEND.INIT:
|
||||
case SEND.CHANGE:
|
||||
case SEND.VALIDATION:
|
||||
return action.state;
|
||||
|
||||
case SEND.TOGGLE_ADVANCED:
|
||||
return {
|
||||
...state,
|
||||
advanced: !state.advanced,
|
||||
};
|
||||
|
||||
case SEND.TX_SENDING:
|
||||
return {
|
||||
...state,
|
||||
sending: true,
|
||||
};
|
||||
|
||||
case SEND.TX_ERROR:
|
||||
return {
|
||||
...state,
|
||||
sending: false,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
@ -0,0 +1,183 @@
|
||||
/* @flow */
|
||||
import React from 'react';
|
||||
import { QRCode } from 'react-qr-svg';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Title from 'views/Wallet/components/Title';
|
||||
import Button from 'components/Button';
|
||||
import Icon from 'components/Icon';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Input from 'components/inputs/Input';
|
||||
|
||||
import ICONS from 'config/icons';
|
||||
import colors from 'config/colors';
|
||||
import { CONTEXT_DEVICE } from 'actions/constants/modal';
|
||||
|
||||
import Content from 'views/Wallet/components/Content';
|
||||
import VerifyAddressTooltip from '../components/VerifyAddressTooltip';
|
||||
|
||||
import type { Props } from './Container';
|
||||
|
||||
const Label = styled.div`
|
||||
padding-bottom: 10px;
|
||||
color: ${colors.TEXT_SECONDARY};
|
||||
`;
|
||||
|
||||
const AddressWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledQRCode = styled(QRCode)`
|
||||
padding: 15px;
|
||||
margin-top: 0 25px;
|
||||
border: 1px solid ${colors.BODY};
|
||||
`;
|
||||
|
||||
const ShowAddressButton = styled(Button)`
|
||||
min-width: 195px;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
align-self: flex-end;
|
||||
justify-content: center;
|
||||
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
@media screen and (max-width: 795px) {
|
||||
margin-top: 10px;
|
||||
align-self: auto;
|
||||
border-radius: 3px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ShowAddressIcon = styled(Icon)`
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
`;
|
||||
|
||||
const EyeButton = styled(Button)`
|
||||
z-index: 10001;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
background: transparent;
|
||||
top: 5px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding-bottom: 28px;
|
||||
|
||||
@media screen and (max-width: 795px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const QrWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const AccountReceive = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
discovery,
|
||||
shouldRender,
|
||||
loader,
|
||||
} = props.selectedAccount;
|
||||
const { type, title, message } = loader;
|
||||
if (!device || !account || !discovery || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
|
||||
|
||||
const {
|
||||
addressVerified,
|
||||
addressUnverified,
|
||||
} = props.receive;
|
||||
|
||||
const isAddressVerifying = props.modal.context === CONTEXT_DEVICE && props.modal.windowType === 'ButtonRequest_Address';
|
||||
const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified;
|
||||
|
||||
let address = `${account.address.substring(0, 20)}...`;
|
||||
if (addressVerified || addressUnverified || isAddressVerifying) {
|
||||
({ address } = account);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<React.Fragment>
|
||||
<Title>Receive Ethereum or tokens</Title>
|
||||
<AddressWrapper isShowingQrCode={addressVerified || addressUnverified}>
|
||||
<Row>
|
||||
<Input
|
||||
type="text"
|
||||
readOnly
|
||||
autoSelect
|
||||
topLabel="Address"
|
||||
value={address}
|
||||
isPartiallyHidden={isAddressHidden}
|
||||
trezorAction={isAddressVerifying ? (
|
||||
<React.Fragment>
|
||||
<Icon
|
||||
icon={ICONS.T1}
|
||||
color={colors.WHITE}
|
||||
/>
|
||||
Check address on your Trezor
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
icon={((addressVerified || addressUnverified) && !isAddressVerifying) && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
content={(
|
||||
<VerifyAddressTooltip
|
||||
isConnected={device.connected}
|
||||
isAvailable={device.available}
|
||||
addressUnverified={addressUnverified}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<EyeButton onClick={() => props.showAddress(account.addressPath)}>
|
||||
<Icon
|
||||
icon={addressUnverified ? ICONS.EYE_CROSSED : ICONS.EYE}
|
||||
color={addressUnverified ? colors.ERROR_PRIMARY : colors.TEXT_PRIMARY}
|
||||
/>
|
||||
</EyeButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
{!(addressVerified || addressUnverified) && (
|
||||
<ShowAddressButton onClick={() => props.showAddress(account.addressPath)} isDisabled={device.connected && !discovery.completed}>
|
||||
<ShowAddressIcon icon={ICONS.EYE} color={colors.WHITE} />Show full address
|
||||
</ShowAddressButton>
|
||||
)}
|
||||
</Row>
|
||||
{(addressVerified || addressUnverified) && !isAddressVerifying && (
|
||||
<QrWrapper>
|
||||
<Label>QR code</Label>
|
||||
<StyledQRCode
|
||||
bgColor="#FFFFFF"
|
||||
fgColor="#000000"
|
||||
level="Q"
|
||||
style={{ width: 150 }}
|
||||
value={account.address}
|
||||
/>
|
||||
</QrWrapper>
|
||||
)}
|
||||
</AddressWrapper>
|
||||
</React.Fragment>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountReceive;
|
@ -1,183 +1,28 @@
|
||||
/* @flow */
|
||||
import React from 'react';
|
||||
import { QRCode } from 'react-qr-svg';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Title from 'views/Wallet/components/Title';
|
||||
import Button from 'components/Button';
|
||||
import Icon from 'components/Icon';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Input from 'components/inputs/Input';
|
||||
|
||||
import ICONS from 'config/icons';
|
||||
import colors from 'config/colors';
|
||||
import { CONTEXT_DEVICE } from 'actions/constants/modal';
|
||||
|
||||
import Content from 'views/Wallet/components/Content';
|
||||
import VerifyAddressTooltip from './components/VerifyAddressTooltip';
|
||||
|
||||
import type { Props } from './Container';
|
||||
|
||||
const Label = styled.div`
|
||||
padding-bottom: 10px;
|
||||
color: ${colors.TEXT_SECONDARY};
|
||||
`;
|
||||
|
||||
const AddressWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledQRCode = styled(QRCode)`
|
||||
padding: 15px;
|
||||
margin-top: 0 25px;
|
||||
border: 1px solid ${colors.BODY};
|
||||
`;
|
||||
|
||||
const ShowAddressButton = styled(Button)`
|
||||
min-width: 195px;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
align-self: flex-end;
|
||||
justify-content: center;
|
||||
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
@media screen and (max-width: 795px) {
|
||||
margin-top: 10px;
|
||||
align-self: auto;
|
||||
border-radius: 3px;
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import type { State } from 'flowtype';
|
||||
import EthereumTypeReceiveForm from './ethereum/Container';
|
||||
import RippleTypeReceiveForm from './ripple/Container';
|
||||
|
||||
export type BaseProps = {
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||
}
|
||||
|
||||
// return container for requested network type
|
||||
export default connect((state: State): BaseProps => ({
|
||||
selectedAccount: state.selectedAccount,
|
||||
}), null)((props) => {
|
||||
const { network } = props.selectedAccount;
|
||||
if (!network) return null;
|
||||
|
||||
switch (network.type) {
|
||||
case 'ethereum':
|
||||
return <EthereumTypeReceiveForm />;
|
||||
case 'ripple':
|
||||
return <RippleTypeReceiveForm />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
`;
|
||||
|
||||
const ShowAddressIcon = styled(Icon)`
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
`;
|
||||
|
||||
const EyeButton = styled(Button)`
|
||||
z-index: 10001;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
background: transparent;
|
||||
top: 5px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding-bottom: 28px;
|
||||
|
||||
@media screen and (max-width: 795px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const QrWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const AccountReceive = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
discovery,
|
||||
shouldRender,
|
||||
loader,
|
||||
} = props.selectedAccount;
|
||||
const { type, title, message } = loader;
|
||||
if (!device || !account || !discovery || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
|
||||
|
||||
const {
|
||||
addressVerified,
|
||||
addressUnverified,
|
||||
} = props.receive;
|
||||
|
||||
const isAddressVerifying = props.modal.context === CONTEXT_DEVICE && props.modal.windowType === 'ButtonRequest_Address';
|
||||
const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified;
|
||||
|
||||
let address = `${account.address.substring(0, 20)}...`;
|
||||
if (addressVerified || addressUnverified || isAddressVerifying) {
|
||||
({ address } = account);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<React.Fragment>
|
||||
<Title>Receive Ethereum or tokens</Title>
|
||||
<AddressWrapper isShowingQrCode={addressVerified || addressUnverified}>
|
||||
<Row>
|
||||
<Input
|
||||
type="text"
|
||||
readOnly
|
||||
autoSelect
|
||||
topLabel="Address"
|
||||
value={address}
|
||||
isPartiallyHidden={isAddressHidden}
|
||||
trezorAction={isAddressVerifying ? (
|
||||
<React.Fragment>
|
||||
<Icon
|
||||
icon={ICONS.T1}
|
||||
color={colors.WHITE}
|
||||
/>
|
||||
Check address on your Trezor
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
icon={((addressVerified || addressUnverified) && !isAddressVerifying) && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
content={(
|
||||
<VerifyAddressTooltip
|
||||
isConnected={device.connected}
|
||||
isAvailable={device.available}
|
||||
addressUnverified={addressUnverified}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<EyeButton onClick={() => props.showAddress(account.addressPath)}>
|
||||
<Icon
|
||||
icon={addressUnverified ? ICONS.EYE_CROSSED : ICONS.EYE}
|
||||
color={addressUnverified ? colors.ERROR_PRIMARY : colors.TEXT_PRIMARY}
|
||||
/>
|
||||
</EyeButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
{!(addressVerified || addressUnverified) && (
|
||||
<ShowAddressButton onClick={() => props.showAddress(account.addressPath)} isDisabled={device.connected && !discovery.completed}>
|
||||
<ShowAddressIcon icon={ICONS.EYE} color={colors.WHITE} />Show full address
|
||||
</ShowAddressButton>
|
||||
)}
|
||||
</Row>
|
||||
{(addressVerified || addressUnverified) && !isAddressVerifying && (
|
||||
<QrWrapper>
|
||||
<Label>QR code</Label>
|
||||
<StyledQRCode
|
||||
bgColor="#FFFFFF"
|
||||
fgColor="#000000"
|
||||
level="Q"
|
||||
style={{ width: 150 }}
|
||||
value={account.address}
|
||||
/>
|
||||
</QrWrapper>
|
||||
)}
|
||||
</AddressWrapper>
|
||||
</React.Fragment>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountReceive;
|
||||
});
|
@ -0,0 +1,36 @@
|
||||
/* @flow */
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { showAddress } from 'actions/ReceiveActions';
|
||||
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
|
||||
import type { State, Dispatch } from 'flowtype';
|
||||
import Receive from './index';
|
||||
|
||||
type OwnProps = { }
|
||||
|
||||
type StateProps = {
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||
receive: $ElementType<State, 'receive'>,
|
||||
modal: $ElementType<State, 'modal'>,
|
||||
wallet: $ElementType<State, 'wallet'>,
|
||||
}
|
||||
|
||||
type DispatchProps = {
|
||||
showAddress: typeof showAddress
|
||||
};
|
||||
|
||||
export type Props = StateProps & DispatchProps;
|
||||
|
||||
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
|
||||
selectedAccount: state.selectedAccount,
|
||||
receive: state.receive,
|
||||
modal: state.modal,
|
||||
wallet: state.wallet,
|
||||
});
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
|
||||
showAddress: bindActionCreators(showAddress, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Receive);
|
@ -0,0 +1,183 @@
|
||||
/* @flow */
|
||||
import React from 'react';
|
||||
import { QRCode } from 'react-qr-svg';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Title from 'views/Wallet/components/Title';
|
||||
import Button from 'components/Button';
|
||||
import Icon from 'components/Icon';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Input from 'components/inputs/Input';
|
||||
|
||||
import ICONS from 'config/icons';
|
||||
import colors from 'config/colors';
|
||||
import { CONTEXT_DEVICE } from 'actions/constants/modal';
|
||||
|
||||
import Content from 'views/Wallet/components/Content';
|
||||
import VerifyAddressTooltip from '../components/VerifyAddressTooltip';
|
||||
|
||||
import type { Props } from './Container';
|
||||
|
||||
const Label = styled.div`
|
||||
padding-bottom: 10px;
|
||||
color: ${colors.TEXT_SECONDARY};
|
||||
`;
|
||||
|
||||
const AddressWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledQRCode = styled(QRCode)`
|
||||
padding: 15px;
|
||||
margin-top: 0 25px;
|
||||
border: 1px solid ${colors.BODY};
|
||||
`;
|
||||
|
||||
const ShowAddressButton = styled(Button)`
|
||||
min-width: 195px;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
align-self: flex-end;
|
||||
justify-content: center;
|
||||
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
@media screen and (max-width: 795px) {
|
||||
margin-top: 10px;
|
||||
align-self: auto;
|
||||
border-radius: 3px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ShowAddressIcon = styled(Icon)`
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
`;
|
||||
|
||||
const EyeButton = styled(Button)`
|
||||
z-index: 10001;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
background: transparent;
|
||||
top: 5px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding-bottom: 28px;
|
||||
|
||||
@media screen and (max-width: 795px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const QrWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const AccountReceive = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
discovery,
|
||||
shouldRender,
|
||||
loader,
|
||||
} = props.selectedAccount;
|
||||
const { type, title, message } = loader;
|
||||
if (!device || !account || !discovery || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
|
||||
|
||||
const {
|
||||
addressVerified,
|
||||
addressUnverified,
|
||||
} = props.receive;
|
||||
|
||||
const isAddressVerifying = props.modal.context === CONTEXT_DEVICE && props.modal.windowType === 'ButtonRequest_Address';
|
||||
const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified;
|
||||
|
||||
let address = `${account.address.substring(0, 20)}...`;
|
||||
if (addressVerified || addressUnverified || isAddressVerifying) {
|
||||
({ address } = account);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<React.Fragment>
|
||||
<Title>Receive Ripple</Title>
|
||||
<AddressWrapper isShowingQrCode={addressVerified || addressUnverified}>
|
||||
<Row>
|
||||
<Input
|
||||
type="text"
|
||||
readOnly
|
||||
autoSelect
|
||||
topLabel="Address"
|
||||
value={address}
|
||||
isPartiallyHidden={isAddressHidden}
|
||||
trezorAction={isAddressVerifying ? (
|
||||
<React.Fragment>
|
||||
<Icon
|
||||
icon={ICONS.T1}
|
||||
color={colors.WHITE}
|
||||
/>
|
||||
Check address on your Trezor
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
icon={((addressVerified || addressUnverified) && !isAddressVerifying) && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
content={(
|
||||
<VerifyAddressTooltip
|
||||
isConnected={device.connected}
|
||||
isAvailable={device.available}
|
||||
addressUnverified={addressUnverified}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<EyeButton onClick={() => props.showAddress(account.addressPath)}>
|
||||
<Icon
|
||||
icon={addressUnverified ? ICONS.EYE_CROSSED : ICONS.EYE}
|
||||
color={addressUnverified ? colors.ERROR_PRIMARY : colors.TEXT_PRIMARY}
|
||||
/>
|
||||
</EyeButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
{!(addressVerified || addressUnverified) && (
|
||||
<ShowAddressButton onClick={() => props.showAddress(account.addressPath)} isDisabled={device.connected && !discovery.completed}>
|
||||
<ShowAddressIcon icon={ICONS.EYE} color={colors.WHITE} />Show full address
|
||||
</ShowAddressButton>
|
||||
)}
|
||||
</Row>
|
||||
{(addressVerified || addressUnverified) && !isAddressVerifying && (
|
||||
<QrWrapper>
|
||||
<Label>QR code</Label>
|
||||
<StyledQRCode
|
||||
bgColor="#FFFFFF"
|
||||
fgColor="#000000"
|
||||
level="Q"
|
||||
style={{ width: 150 }}
|
||||
value={account.address}
|
||||
/>
|
||||
</QrWrapper>
|
||||
)}
|
||||
</AddressWrapper>
|
||||
</React.Fragment>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountReceive;
|
@ -0,0 +1,39 @@
|
||||
/* @flow */
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import SendFormActions from 'actions/ethereum/SendFormActions';
|
||||
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
|
||||
import type { State, Dispatch } from 'flowtype';
|
||||
import AccountSend from './index';
|
||||
|
||||
type OwnProps = {}
|
||||
|
||||
export type StateProps = {
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||
sendForm: $ElementType<State, 'sendFormEthereum'>,
|
||||
wallet: $ElementType<State, 'wallet'>,
|
||||
fiat: $ElementType<State, 'fiat'>,
|
||||
localStorage: $ElementType<State, 'localStorage'>,
|
||||
}
|
||||
|
||||
export type DispatchProps = {
|
||||
sendFormActions: typeof SendFormActions,
|
||||
}
|
||||
|
||||
export type Props = StateProps & DispatchProps;
|
||||
|
||||
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
|
||||
selectedAccount: state.selectedAccount,
|
||||
sendForm: state.sendFormEthereum,
|
||||
wallet: state.wallet,
|
||||
fiat: state.fiat,
|
||||
localStorage: state.localStorage,
|
||||
});
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
|
||||
sendFormActions: bindActionCreators(SendFormActions, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AccountSend);
|
@ -0,0 +1,403 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
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 type { Token } from 'flowtype';
|
||||
import AdvancedForm from '../components/AdvancedForm';
|
||||
import PendingTransactions from '../components/PendingTransactions';
|
||||
|
||||
import type { Props } from './Container';
|
||||
|
||||
// TODO: Decide on a small screen width for the whole app
|
||||
// and put it inside config/variables.js
|
||||
const SmallScreenWidth = '850px';
|
||||
|
||||
const AmountInputLabelWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const AmountInputLabel = styled.span`
|
||||
text-align: right;
|
||||
color: ${colors.TEXT_SECONDARY};
|
||||
`;
|
||||
|
||||
const InputRow = styled.div`
|
||||
padding-bottom: 28px;
|
||||
`;
|
||||
|
||||
const SetMaxAmountButton = styled(Button)`
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: ${FONT_SIZE.SMALLER};
|
||||
font-weight: ${FONT_WEIGHT.SMALLEST};
|
||||
color: ${colors.TEXT_SECONDARY};
|
||||
|
||||
border-radius: 0;
|
||||
border: 1px solid ${colors.DIVIDER};
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
background: transparent;
|
||||
transition: ${TRANSITION.HOVER};
|
||||
|
||||
&:hover {
|
||||
background: ${colors.GRAY_LIGHT};
|
||||
}
|
||||
|
||||
${props => props.isActive && css`
|
||||
color: ${colors.WHITE};
|
||||
background: ${colors.GREEN_PRIMARY};
|
||||
border-color: ${colors.GREEN_PRIMARY};
|
||||
|
||||
&:hover {
|
||||
background: ${colors.GREEN_SECONDARY};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: ${colors.GREEN_TERTIARY};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const CurrencySelect = styled(Select)`
|
||||
min-width: 77px;
|
||||
height: 40px;
|
||||
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.SMALLER};
|
||||
color: ${colors.WARNING_PRIMARY};
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
margin-left: 4px;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const ToggleAdvancedSettingsWrapper = styled.div`
|
||||
min-height: 40px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
@media screen and (max-width: ${SmallScreenWidth}) {
|
||||
${props => (props.isAdvancedSettingsHidden && css`
|
||||
flex-direction: column;
|
||||
`)}
|
||||
}
|
||||
`;
|
||||
|
||||
const ToggleAdvancedSettingsButton = styled(Button)`
|
||||
min-height: 40px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: ${FONT_WEIGHT.BIGGER};
|
||||
`;
|
||||
|
||||
const SendButton = styled(Button)`
|
||||
min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')};
|
||||
font-size: 13px;
|
||||
|
||||
@media screen and (max-width: ${SmallScreenWidth}) {
|
||||
margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)};
|
||||
}
|
||||
`;
|
||||
|
||||
const AdvancedSettingsIcon = styled(Icon)`
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
// render helpers
|
||||
const getAddressInputState = (address: string, addressErrors: string, addressWarnings: string): string => {
|
||||
let state = '';
|
||||
if (address && !addressErrors) {
|
||||
state = 'success';
|
||||
}
|
||||
if (addressWarnings && !addressErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (addressErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const getAmountInputState = (amountErrors: string, amountWarnings: string): string => {
|
||||
let state = '';
|
||||
if (amountWarnings && !amountErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (amountErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const getTokensSelectData = (tokens: Array<Token>, accountNetwork: any): Array<{ value: string, label: string }> => {
|
||||
const tokensSelectData: Array<{ value: string, label: string }> = tokens.map(t => ({ value: t.symbol, label: t.symbol }));
|
||||
tokensSelectData.unshift({ value: accountNetwork.symbol, label: accountNetwork.symbol });
|
||||
|
||||
return tokensSelectData;
|
||||
};
|
||||
|
||||
// stateless component
|
||||
const AccountSend = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
discovery,
|
||||
tokens,
|
||||
shouldRender,
|
||||
loader,
|
||||
} = props.selectedAccount;
|
||||
const {
|
||||
address,
|
||||
amount,
|
||||
setMax,
|
||||
networkSymbol,
|
||||
currency,
|
||||
feeLevels,
|
||||
selectedFeeLevel,
|
||||
gasPriceNeedsUpdate,
|
||||
total,
|
||||
errors,
|
||||
warnings,
|
||||
infos,
|
||||
sending,
|
||||
advanced,
|
||||
} = props.sendForm;
|
||||
|
||||
const {
|
||||
toggleAdvanced,
|
||||
onAddressChange,
|
||||
onAmountChange,
|
||||
onSetMax,
|
||||
onCurrencyChange,
|
||||
onFeeLevelChange,
|
||||
updateFeeLevels,
|
||||
onSend,
|
||||
} = props.sendFormActions;
|
||||
const { type, title, message } = loader;
|
||||
if (!device || !account || !discovery || !network || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
|
||||
|
||||
const isCurrentCurrencyToken = networkSymbol !== currency;
|
||||
|
||||
let selectedTokenBalance = 0;
|
||||
const selectedToken = tokens.find(t => t.symbol === currency);
|
||||
if (selectedToken) {
|
||||
selectedTokenBalance = selectedToken.balance;
|
||||
}
|
||||
|
||||
let isSendButtonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending;
|
||||
let sendButtonText: string = 'Send';
|
||||
if (networkSymbol !== currency && amount.length > 0 && !errors.amount) {
|
||||
sendButtonText += ` ${amount} ${currency.toUpperCase()}`;
|
||||
} else if (networkSymbol === currency && total !== '0') {
|
||||
sendButtonText += ` ${total} ${network.symbol}`;
|
||||
}
|
||||
|
||||
if (!device.connected) {
|
||||
sendButtonText = 'Device is not connected';
|
||||
isSendButtonDisabled = true;
|
||||
} else if (!device.available) {
|
||||
sendButtonText = 'Device is unavailable';
|
||||
isSendButtonDisabled = true;
|
||||
} else if (!discovery.completed) {
|
||||
sendButtonText = 'Loading accounts';
|
||||
isSendButtonDisabled = true;
|
||||
}
|
||||
|
||||
const tokensSelectData = getTokensSelectData(tokens, network);
|
||||
const tokensSelectValue = tokensSelectData.find(t => t.value === currency);
|
||||
const isAdvancedSettingsHidden = !advanced;
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<Title>Send Ethereum or tokens</Title>
|
||||
<InputRow>
|
||||
<Input
|
||||
state={getAddressInputState(address, errors.address, warnings.address)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel="Address"
|
||||
bottomText={errors.address || warnings.address || infos.address}
|
||||
value={address}
|
||||
onChange={event => onAddressChange(event.target.value)}
|
||||
/>
|
||||
</InputRow>
|
||||
<InputRow>
|
||||
<Input
|
||||
state={getAmountInputState(errors.amount, warnings.amount)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel={(
|
||||
<AmountInputLabelWrapper>
|
||||
<AmountInputLabel>Amount</AmountInputLabel>
|
||||
{(isCurrentCurrencyToken && selectedToken) && (
|
||||
<AmountInputLabel>You have: {selectedTokenBalance} {selectedToken.symbol}</AmountInputLabel>
|
||||
)}
|
||||
</AmountInputLabelWrapper>
|
||||
)}
|
||||
value={amount}
|
||||
onChange={event => onAmountChange(event.target.value)}
|
||||
bottomText={errors.amount || warnings.amount || infos.amount}
|
||||
sideAddons={[
|
||||
(
|
||||
<SetMaxAmountButton
|
||||
key="icon"
|
||||
onClick={() => onSetMax()}
|
||||
isActive={setMax}
|
||||
>
|
||||
{!setMax && (
|
||||
<Icon
|
||||
icon={ICONS.TOP}
|
||||
size={25}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
/>
|
||||
)}
|
||||
{setMax && (
|
||||
<Icon
|
||||
icon={ICONS.CHECKED}
|
||||
size={25}
|
||||
color={colors.WHITE}
|
||||
/>
|
||||
)}
|
||||
Set max
|
||||
</SetMaxAmountButton>
|
||||
),
|
||||
(
|
||||
<CurrencySelect
|
||||
key="currency"
|
||||
isSearchable={false}
|
||||
isClearable={false}
|
||||
value={tokensSelectValue}
|
||||
isDisabled={tokensSelectData.length < 2}
|
||||
onChange={onCurrencyChange}
|
||||
options={tokensSelectData}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
<InputRow>
|
||||
<FeeLabelWrapper>
|
||||
<FeeLabel>Fee</FeeLabel>
|
||||
{gasPriceNeedsUpdate && (
|
||||
<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}
|
||||
>
|
||||
<ToggleAdvancedSettingsButton
|
||||
isTransparent
|
||||
onClick={toggleAdvanced}
|
||||
>
|
||||
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
|
||||
pending={props.selectedAccount.pending}
|
||||
tokens={props.selectedAccount.tokens}
|
||||
network={network}
|
||||
/>
|
||||
)}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountSend;
|
@ -1,403 +1,28 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
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 type { Token } from 'flowtype';
|
||||
import AdvancedForm from './components/AdvancedForm';
|
||||
import PendingTransactions from './components/PendingTransactions';
|
||||
|
||||
import type { Props } from './Container';
|
||||
|
||||
// TODO: Decide on a small screen width for the whole app
|
||||
// and put it inside config/variables.js
|
||||
const SmallScreenWidth = '850px';
|
||||
|
||||
const AmountInputLabelWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const AmountInputLabel = styled.span`
|
||||
text-align: right;
|
||||
color: ${colors.TEXT_SECONDARY};
|
||||
`;
|
||||
|
||||
const InputRow = styled.div`
|
||||
padding-bottom: 28px;
|
||||
`;
|
||||
|
||||
const SetMaxAmountButton = styled(Button)`
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: ${FONT_SIZE.SMALLER};
|
||||
font-weight: ${FONT_WEIGHT.SMALLEST};
|
||||
color: ${colors.TEXT_SECONDARY};
|
||||
|
||||
border-radius: 0;
|
||||
border: 1px solid ${colors.DIVIDER};
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
background: transparent;
|
||||
transition: ${TRANSITION.HOVER};
|
||||
|
||||
&:hover {
|
||||
background: ${colors.GRAY_LIGHT};
|
||||
}
|
||||
|
||||
${props => props.isActive && css`
|
||||
color: ${colors.WHITE};
|
||||
background: ${colors.GREEN_PRIMARY};
|
||||
border-color: ${colors.GREEN_PRIMARY};
|
||||
|
||||
&:hover {
|
||||
background: ${colors.GREEN_SECONDARY};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: ${colors.GREEN_TERTIARY};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const CurrencySelect = styled(Select)`
|
||||
min-width: 77px;
|
||||
height: 40px;
|
||||
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.SMALLER};
|
||||
color: ${colors.WARNING_PRIMARY};
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
margin-left: 4px;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const ToggleAdvancedSettingsWrapper = styled.div`
|
||||
min-height: 40px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
@media screen and (max-width: ${SmallScreenWidth}) {
|
||||
${props => (props.isAdvancedSettingsHidden && css`
|
||||
flex-direction: column;
|
||||
`)}
|
||||
}
|
||||
`;
|
||||
|
||||
const ToggleAdvancedSettingsButton = styled(Button)`
|
||||
min-height: 40px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: ${FONT_WEIGHT.BIGGER};
|
||||
`;
|
||||
|
||||
const SendButton = styled(Button)`
|
||||
min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')};
|
||||
font-size: 13px;
|
||||
|
||||
@media screen and (max-width: ${SmallScreenWidth}) {
|
||||
margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)};
|
||||
}
|
||||
`;
|
||||
|
||||
const AdvancedSettingsIcon = styled(Icon)`
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
// render helpers
|
||||
const getAddressInputState = (address: string, addressErrors: string, addressWarnings: string): string => {
|
||||
let state = '';
|
||||
if (address && !addressErrors) {
|
||||
state = 'success';
|
||||
}
|
||||
if (addressWarnings && !addressErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (addressErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const getAmountInputState = (amountErrors: string, amountWarnings: string): string => {
|
||||
let state = '';
|
||||
if (amountWarnings && !amountErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (amountErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const getTokensSelectData = (tokens: Array<Token>, accountNetwork: any): Array<{ value: string, label: string }> => {
|
||||
const tokensSelectData: Array<{ value: string, label: string }> = tokens.map(t => ({ value: t.symbol, label: t.symbol }));
|
||||
tokensSelectData.unshift({ value: accountNetwork.symbol, label: accountNetwork.symbol });
|
||||
|
||||
return tokensSelectData;
|
||||
};
|
||||
|
||||
// stateless component
|
||||
const AccountSend = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
discovery,
|
||||
tokens,
|
||||
shouldRender,
|
||||
loader,
|
||||
} = props.selectedAccount;
|
||||
const {
|
||||
address,
|
||||
amount,
|
||||
setMax,
|
||||
networkSymbol,
|
||||
currency,
|
||||
feeLevels,
|
||||
selectedFeeLevel,
|
||||
gasPriceNeedsUpdate,
|
||||
total,
|
||||
errors,
|
||||
warnings,
|
||||
infos,
|
||||
sending,
|
||||
advanced,
|
||||
} = props.sendForm;
|
||||
|
||||
const {
|
||||
toggleAdvanced,
|
||||
onAddressChange,
|
||||
onAmountChange,
|
||||
onSetMax,
|
||||
onCurrencyChange,
|
||||
onFeeLevelChange,
|
||||
updateFeeLevels,
|
||||
onSend,
|
||||
} = props.sendFormActions;
|
||||
const { type, title, message } = loader;
|
||||
if (!device || !account || !discovery || !network || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
|
||||
|
||||
const isCurrentCurrencyToken = networkSymbol !== currency;
|
||||
|
||||
let selectedTokenBalance = 0;
|
||||
const selectedToken = tokens.find(t => t.symbol === currency);
|
||||
if (selectedToken) {
|
||||
selectedTokenBalance = selectedToken.balance;
|
||||
}
|
||||
|
||||
let isSendButtonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending;
|
||||
let sendButtonText: string = 'Send';
|
||||
if (networkSymbol !== currency && amount.length > 0 && !errors.amount) {
|
||||
sendButtonText += ` ${amount} ${currency.toUpperCase()}`;
|
||||
} else if (networkSymbol === currency && total !== '0') {
|
||||
sendButtonText += ` ${total} ${network.symbol}`;
|
||||
}
|
||||
|
||||
if (!device.connected) {
|
||||
sendButtonText = 'Device is not connected';
|
||||
isSendButtonDisabled = true;
|
||||
} else if (!device.available) {
|
||||
sendButtonText = 'Device is unavailable';
|
||||
isSendButtonDisabled = true;
|
||||
} else if (!discovery.completed) {
|
||||
sendButtonText = 'Loading accounts';
|
||||
isSendButtonDisabled = true;
|
||||
}
|
||||
|
||||
const tokensSelectData = getTokensSelectData(tokens, network);
|
||||
const tokensSelectValue = tokensSelectData.find(t => t.value === currency);
|
||||
const isAdvancedSettingsHidden = !advanced;
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<Title>Send Ethereum or tokens</Title>
|
||||
<InputRow>
|
||||
<Input
|
||||
state={getAddressInputState(address, errors.address, warnings.address)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel="Address"
|
||||
bottomText={errors.address || warnings.address || infos.address}
|
||||
value={address}
|
||||
onChange={event => onAddressChange(event.target.value)}
|
||||
/>
|
||||
</InputRow>
|
||||
<InputRow>
|
||||
<Input
|
||||
state={getAmountInputState(errors.amount, warnings.amount)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel={(
|
||||
<AmountInputLabelWrapper>
|
||||
<AmountInputLabel>Amount</AmountInputLabel>
|
||||
{(isCurrentCurrencyToken && selectedToken) && (
|
||||
<AmountInputLabel>You have: {selectedTokenBalance} {selectedToken.symbol}</AmountInputLabel>
|
||||
)}
|
||||
</AmountInputLabelWrapper>
|
||||
)}
|
||||
value={amount}
|
||||
onChange={event => onAmountChange(event.target.value)}
|
||||
bottomText={errors.amount || warnings.amount || infos.amount}
|
||||
sideAddons={[
|
||||
(
|
||||
<SetMaxAmountButton
|
||||
key="icon"
|
||||
onClick={() => onSetMax()}
|
||||
isActive={setMax}
|
||||
>
|
||||
{!setMax && (
|
||||
<Icon
|
||||
icon={ICONS.TOP}
|
||||
size={25}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
/>
|
||||
)}
|
||||
{setMax && (
|
||||
<Icon
|
||||
icon={ICONS.CHECKED}
|
||||
size={25}
|
||||
color={colors.WHITE}
|
||||
/>
|
||||
)}
|
||||
Set max
|
||||
</SetMaxAmountButton>
|
||||
),
|
||||
(
|
||||
<CurrencySelect
|
||||
key="currency"
|
||||
isSearchable={false}
|
||||
isClearable={false}
|
||||
value={tokensSelectValue}
|
||||
isDisabled={tokensSelectData.length < 2}
|
||||
onChange={onCurrencyChange}
|
||||
options={tokensSelectData}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
<InputRow>
|
||||
<FeeLabelWrapper>
|
||||
<FeeLabel>Fee</FeeLabel>
|
||||
{gasPriceNeedsUpdate && (
|
||||
<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}
|
||||
>
|
||||
<ToggleAdvancedSettingsButton
|
||||
isTransparent
|
||||
onClick={toggleAdvanced}
|
||||
>
|
||||
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
|
||||
pending={props.selectedAccount.pending}
|
||||
tokens={props.selectedAccount.tokens}
|
||||
network={network}
|
||||
/>
|
||||
)}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountSend;
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import type { State } from 'flowtype';
|
||||
import EthereumTypeSendForm from './ethereum/Container';
|
||||
import RippleTypeSendForm from './ripple/Container';
|
||||
|
||||
export type BaseProps = {
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||
}
|
||||
|
||||
// return container for requested network type
|
||||
export default connect((state: State): BaseProps => ({
|
||||
selectedAccount: state.selectedAccount,
|
||||
}), null)((props) => {
|
||||
const { network } = props.selectedAccount;
|
||||
if (!network) return null;
|
||||
|
||||
switch (network.type) {
|
||||
case 'ethereum':
|
||||
return <EthereumTypeSendForm />;
|
||||
case 'ripple':
|
||||
return <RippleTypeSendForm />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
@ -0,0 +1,255 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { Select } from 'components/Select';
|
||||
import Button from 'components/Button';
|
||||
import Input from 'components/inputs/Input';
|
||||
import Icon from 'components/Icon';
|
||||
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 Content from 'views/Wallet/components/Content';
|
||||
import PendingTransactions from '../components/PendingTransactions';
|
||||
|
||||
import type { Props } from './Container';
|
||||
|
||||
// TODO: Decide on a small screen width for the whole app
|
||||
// and put it inside config/variables.js
|
||||
const SmallScreenWidth = '850px';
|
||||
|
||||
const InputRow = styled.div`
|
||||
padding-bottom: 28px;
|
||||
`;
|
||||
|
||||
const SetMaxAmountButton = styled(Button)`
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: ${FONT_SIZE.SMALLER};
|
||||
font-weight: ${FONT_WEIGHT.SMALLEST};
|
||||
color: ${colors.TEXT_SECONDARY};
|
||||
|
||||
border-radius: 0;
|
||||
border: 1px solid ${colors.DIVIDER};
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
background: transparent;
|
||||
transition: ${TRANSITION.HOVER};
|
||||
|
||||
&:hover {
|
||||
background: ${colors.GRAY_LIGHT};
|
||||
}
|
||||
|
||||
${props => props.isActive && css`
|
||||
color: ${colors.WHITE};
|
||||
background: ${colors.GREEN_PRIMARY};
|
||||
border-color: ${colors.GREEN_PRIMARY};
|
||||
|
||||
&:hover {
|
||||
background: ${colors.GREEN_SECONDARY};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: ${colors.GREEN_TERTIARY};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const CurrencySelect = styled(Select)`
|
||||
min-width: 77px;
|
||||
height: 40px;
|
||||
flex: 0.2;
|
||||
`;
|
||||
|
||||
const ToggleAdvancedSettingsWrapper = styled.div`
|
||||
min-height: 40px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
@media screen and (max-width: ${SmallScreenWidth}) {
|
||||
${props => (props.isAdvancedSettingsHidden && css`
|
||||
flex-direction: column;
|
||||
`)}
|
||||
}
|
||||
`;
|
||||
|
||||
const SendButton = styled(Button)`
|
||||
min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')};
|
||||
font-size: 13px;
|
||||
|
||||
@media screen and (max-width: ${SmallScreenWidth}) {
|
||||
margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)};
|
||||
}
|
||||
`;
|
||||
|
||||
// render helpers
|
||||
const getAddressInputState = (address: string, addressErrors: string, addressWarnings: string): string => {
|
||||
let state = '';
|
||||
if (address && !addressErrors) {
|
||||
state = 'success';
|
||||
}
|
||||
if (addressWarnings && !addressErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (addressErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const getAmountInputState = (amountErrors: string, amountWarnings: string): string => {
|
||||
let state = '';
|
||||
if (amountWarnings && !amountErrors) {
|
||||
state = 'warning';
|
||||
}
|
||||
if (amountErrors) {
|
||||
state = 'error';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
// stateless component
|
||||
const AccountSend = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
discovery,
|
||||
shouldRender,
|
||||
loader,
|
||||
} = props.selectedAccount;
|
||||
const {
|
||||
address,
|
||||
amount,
|
||||
setMax,
|
||||
total,
|
||||
errors,
|
||||
warnings,
|
||||
infos,
|
||||
sending,
|
||||
advanced,
|
||||
} = props.sendForm;
|
||||
|
||||
const {
|
||||
onAddressChange,
|
||||
onAmountChange,
|
||||
onSetMax,
|
||||
onSend,
|
||||
} = props.sendFormActions;
|
||||
const { type, title, message } = loader;
|
||||
if (!device || !account || !discovery || !network || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
|
||||
|
||||
let isSendButtonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending;
|
||||
let sendButtonText: string = ` ${total} ${network.symbol}`;
|
||||
|
||||
if (!device.connected) {
|
||||
sendButtonText = 'Device is not connected';
|
||||
isSendButtonDisabled = true;
|
||||
} else if (!device.available) {
|
||||
sendButtonText = 'Device is unavailable';
|
||||
isSendButtonDisabled = true;
|
||||
} else if (!discovery.completed) {
|
||||
sendButtonText = 'Loading accounts';
|
||||
isSendButtonDisabled = true;
|
||||
}
|
||||
|
||||
const tokensSelectData: Array<{ value: string, label: string }> = [{ value: network.symbol, label: network.symbol }];
|
||||
const tokensSelectValue = tokensSelectData[0];
|
||||
const isAdvancedSettingsHidden = !advanced;
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<Title>Send Ripple</Title>
|
||||
<InputRow>
|
||||
<Input
|
||||
state={getAddressInputState(address, errors.address, warnings.address)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
topLabel="Address"
|
||||
bottomText={errors.address || warnings.address || infos.address}
|
||||
value={address}
|
||||
onChange={event => onAddressChange(event.target.value)}
|
||||
/>
|
||||
</InputRow>
|
||||
<InputRow>
|
||||
<Input
|
||||
state={getAmountInputState(errors.amount, warnings.amount)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={amount}
|
||||
onChange={event => onAmountChange(event.target.value)}
|
||||
bottomText={errors.amount || warnings.amount || infos.amount}
|
||||
sideAddons={[
|
||||
(
|
||||
<SetMaxAmountButton
|
||||
key="icon"
|
||||
onClick={() => onSetMax()}
|
||||
isActive={setMax}
|
||||
>
|
||||
{!setMax && (
|
||||
<Icon
|
||||
icon={ICONS.TOP}
|
||||
size={25}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
/>
|
||||
)}
|
||||
{setMax && (
|
||||
<Icon
|
||||
icon={ICONS.CHECKED}
|
||||
size={25}
|
||||
color={colors.WHITE}
|
||||
/>
|
||||
)}
|
||||
Set max
|
||||
</SetMaxAmountButton>
|
||||
),
|
||||
(
|
||||
<CurrencySelect
|
||||
key="currency"
|
||||
isSearchable={false}
|
||||
isClearable={false}
|
||||
value={tokensSelectValue}
|
||||
isDisabled
|
||||
options={tokensSelectData}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
<ToggleAdvancedSettingsWrapper
|
||||
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
|
||||
>
|
||||
<SendButton
|
||||
isDisabled={isSendButtonDisabled}
|
||||
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
|
||||
onClick={() => onSend()}
|
||||
>
|
||||
{sendButtonText}
|
||||
</SendButton>
|
||||
</ToggleAdvancedSettingsWrapper>
|
||||
|
||||
|
||||
{props.selectedAccount.pending.length > 0 && (
|
||||
<PendingTransactions
|
||||
pending={props.selectedAccount.pending}
|
||||
tokens={props.selectedAccount.tokens}
|
||||
network={network}
|
||||
/>
|
||||
)}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountSend;
|
@ -0,0 +1,165 @@
|
||||
/* @flow */
|
||||
import styled from 'styled-components';
|
||||
import React from 'react';
|
||||
import { H2 } from 'components/Heading';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import Icon from 'components/Icon';
|
||||
import { AsyncSelect } from 'components/Select';
|
||||
import ICONS from 'config/icons';
|
||||
import colors from 'config/colors';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Content from 'views/Wallet/components/Content';
|
||||
|
||||
import CoinLogo from 'components/images/CoinLogo';
|
||||
import * as stateUtils from 'reducers/utils';
|
||||
import Link from 'components/Link';
|
||||
import { FONT_WEIGHT, FONT_SIZE } from 'config/variables';
|
||||
import AccountBalance from '../components/Balance';
|
||||
import AddedToken from '../components/Token';
|
||||
import AddTokenMessage from '../components/AddTokenMessage';
|
||||
|
||||
import type { Props } from './Container';
|
||||
|
||||
const AccountHeading = styled.div`
|
||||
padding-bottom: 35px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const H2Wrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
`;
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
position: relative;
|
||||
top: 2px;
|
||||
`;
|
||||
|
||||
const AccountName = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const AccountTitle = styled.div`
|
||||
font-size: ${FONT_SIZE.WALLET_TITLE};
|
||||
font-weight: ${FONT_WEIGHT.BASE};
|
||||
color: ${colors.WALLET_TITLE};
|
||||
`;
|
||||
|
||||
const StyledCoinLogo = styled(CoinLogo)`
|
||||
margin-right: 10px;
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
position: relative;
|
||||
top: -7px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const AsyncSelectWrapper = styled.div`
|
||||
padding-bottom: 32px;
|
||||
`;
|
||||
|
||||
const AddedTokensWrapper = styled.div``;
|
||||
|
||||
const AccountSummary = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
tokens,
|
||||
pending,
|
||||
loader,
|
||||
shouldRender,
|
||||
} = props.selectedAccount;
|
||||
|
||||
const { type, title, message } = loader;
|
||||
|
||||
if (!device || !account || !network || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
|
||||
|
||||
const explorerLink: string = `${network.explorer.address}${account.address}`;
|
||||
const pendingAmount: BigNumber = stateUtils.getPendingAmount(pending, network.symbol);
|
||||
const balance: string = new BigNumber(account.balance).minus(pendingAmount).toString(10);
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<React.Fragment>
|
||||
<AccountHeading>
|
||||
<AccountName>
|
||||
<StyledCoinLogo network={account.network} />
|
||||
<AccountTitle>Account #{parseInt(account.index, 10) + 1}</AccountTitle>
|
||||
</AccountName>
|
||||
<Link href={explorerLink} isGray>See full transaction history</Link>
|
||||
</AccountHeading>
|
||||
<AccountBalance
|
||||
network={network}
|
||||
balance={balance}
|
||||
fiat={props.fiat}
|
||||
/>
|
||||
<H2Wrapper>
|
||||
<H2>Tokens</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>
|
||||
<AsyncSelectWrapper>
|
||||
<AsyncSelect
|
||||
isSearchable
|
||||
defaultOptions
|
||||
value={null}
|
||||
isMulti={false}
|
||||
placeholder="Search for the token"
|
||||
loadingMessage={() => 'Loading...'}
|
||||
noOptionsMessage={() => 'Token not found'}
|
||||
onChange={(token) => {
|
||||
if (token.name) {
|
||||
const isAdded = tokens.find(t => t.symbol === token.symbol);
|
||||
if (!isAdded) {
|
||||
props.addToken(token, account);
|
||||
}
|
||||
}
|
||||
}}
|
||||
loadOptions={input => props.loadTokens(input, account.network)}
|
||||
formatOptionLabel={(option) => {
|
||||
const isAdded = tokens.find(t => t.symbol === option.symbol);
|
||||
if (isAdded) {
|
||||
return `${option.name} (Already added)`;
|
||||
}
|
||||
return option.name;
|
||||
}}
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option.symbol}
|
||||
/>
|
||||
</AsyncSelectWrapper>
|
||||
<AddedTokensWrapper>
|
||||
{ tokens.length < 1 && (<AddTokenMessage />) }
|
||||
{tokens.map(token => (
|
||||
<AddedToken
|
||||
key={token.symbol}
|
||||
token={token}
|
||||
pending={pending}
|
||||
removeToken={props.removeToken}
|
||||
/>
|
||||
))}
|
||||
</AddedTokensWrapper>
|
||||
</React.Fragment>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountSummary;
|
@ -1,165 +1,28 @@
|
||||
/* @flow */
|
||||
import styled from 'styled-components';
|
||||
import React from 'react';
|
||||
import { H2 } from 'components/Heading';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import Icon from 'components/Icon';
|
||||
import { AsyncSelect } from 'components/Select';
|
||||
import ICONS from 'config/icons';
|
||||
import colors from 'config/colors';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Content from 'views/Wallet/components/Content';
|
||||
|
||||
import CoinLogo from 'components/images/CoinLogo';
|
||||
import * as stateUtils from 'reducers/utils';
|
||||
import Link from 'components/Link';
|
||||
import { FONT_WEIGHT, FONT_SIZE } from 'config/variables';
|
||||
import AccountBalance from './components/Balance';
|
||||
import AddedToken from './components/Token';
|
||||
import AddTokenMessage from './components/AddTokenMessage';
|
||||
|
||||
import type { Props } from './Container';
|
||||
|
||||
const AccountHeading = styled.div`
|
||||
padding-bottom: 35px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const H2Wrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
`;
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
position: relative;
|
||||
top: 2px;
|
||||
`;
|
||||
|
||||
const AccountName = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const AccountTitle = styled.div`
|
||||
font-size: ${FONT_SIZE.WALLET_TITLE};
|
||||
font-weight: ${FONT_WEIGHT.BASE};
|
||||
color: ${colors.WALLET_TITLE};
|
||||
`;
|
||||
|
||||
const StyledCoinLogo = styled(CoinLogo)`
|
||||
margin-right: 10px;
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
position: relative;
|
||||
top: -7px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import type { State } from 'flowtype';
|
||||
import EthereumTypeSummary from './ethereum/Container';
|
||||
import RippleTypeSummary from './ripple/Container';
|
||||
|
||||
type WrapperProps = {
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||
}
|
||||
|
||||
// return container for requested network type
|
||||
export default connect((state: State): WrapperProps => ({
|
||||
selectedAccount: state.selectedAccount,
|
||||
}), null)((props) => {
|
||||
const { network } = props.selectedAccount;
|
||||
if (!network) return null;
|
||||
|
||||
switch (network.type) {
|
||||
case 'ethereum':
|
||||
return <EthereumTypeSummary />;
|
||||
case 'ripple':
|
||||
return <RippleTypeSummary />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
`;
|
||||
|
||||
const AsyncSelectWrapper = styled.div`
|
||||
padding-bottom: 32px;
|
||||
`;
|
||||
|
||||
const AddedTokensWrapper = styled.div``;
|
||||
|
||||
const AccountSummary = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
tokens,
|
||||
pending,
|
||||
loader,
|
||||
shouldRender,
|
||||
} = props.selectedAccount;
|
||||
|
||||
const { type, title, message } = loader;
|
||||
|
||||
if (!device || !account || !network || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
|
||||
|
||||
const explorerLink: string = `${network.explorer.address}${account.address}`;
|
||||
const pendingAmount: BigNumber = stateUtils.getPendingAmount(pending, network.symbol);
|
||||
const balance: string = new BigNumber(account.balance).minus(pendingAmount).toString(10);
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<React.Fragment>
|
||||
<AccountHeading>
|
||||
<AccountName>
|
||||
<StyledCoinLogo network={account.network} />
|
||||
<AccountTitle>Account #{parseInt(account.index, 10) + 1}</AccountTitle>
|
||||
</AccountName>
|
||||
<Link href={explorerLink} isGray>See full transaction history</Link>
|
||||
</AccountHeading>
|
||||
<AccountBalance
|
||||
network={network}
|
||||
balance={balance}
|
||||
fiat={props.fiat}
|
||||
/>
|
||||
<H2Wrapper>
|
||||
<H2>Tokens</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>
|
||||
<AsyncSelectWrapper>
|
||||
<AsyncSelect
|
||||
isSearchable
|
||||
defaultOptions
|
||||
value={null}
|
||||
isMulti={false}
|
||||
placeholder="Search for the token"
|
||||
loadingMessage={() => 'Loading...'}
|
||||
noOptionsMessage={() => 'Token not found'}
|
||||
onChange={(token) => {
|
||||
if (token.name) {
|
||||
const isAdded = tokens.find(t => t.symbol === token.symbol);
|
||||
if (!isAdded) {
|
||||
props.addToken(token, account);
|
||||
}
|
||||
}
|
||||
}}
|
||||
loadOptions={input => props.loadTokens(input, account.network)}
|
||||
formatOptionLabel={(option) => {
|
||||
const isAdded = tokens.find(t => t.symbol === option.symbol);
|
||||
if (isAdded) {
|
||||
return `${option.name} (Already added)`;
|
||||
}
|
||||
return option.name;
|
||||
}}
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option.symbol}
|
||||
/>
|
||||
</AsyncSelectWrapper>
|
||||
<AddedTokensWrapper>
|
||||
{ tokens.length < 1 && (<AddTokenMessage />) }
|
||||
{tokens.map(token => (
|
||||
<AddedToken
|
||||
key={token.symbol}
|
||||
token={token}
|
||||
pending={pending}
|
||||
removeToken={props.removeToken}
|
||||
/>
|
||||
))}
|
||||
</AddedTokensWrapper>
|
||||
</React.Fragment>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountSummary;
|
||||
});
|
||||
|
@ -0,0 +1,45 @@
|
||||
/* @flow */
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
|
||||
import * as TokenActions from 'actions/TokenActions';
|
||||
|
||||
import type { State, Dispatch } from 'flowtype';
|
||||
import Summary from './index';
|
||||
|
||||
type OwnProps = { }
|
||||
|
||||
type StateProps = {
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||
summary: $ElementType<State, 'summary'>,
|
||||
wallet: $ElementType<State, 'wallet'>,
|
||||
tokens: $ElementType<State, 'tokens'>,
|
||||
fiat: $ElementType<State, 'fiat'>,
|
||||
localStorage: $ElementType<State, 'localStorage'>,
|
||||
};
|
||||
|
||||
type DispatchProps = {
|
||||
addToken: typeof TokenActions.add,
|
||||
loadTokens: typeof TokenActions.load,
|
||||
removeToken: typeof TokenActions.remove,
|
||||
}
|
||||
|
||||
export type Props = StateProps & DispatchProps;
|
||||
|
||||
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
|
||||
selectedAccount: state.selectedAccount,
|
||||
summary: state.summary,
|
||||
wallet: state.wallet,
|
||||
tokens: state.tokens,
|
||||
fiat: state.fiat,
|
||||
localStorage: state.localStorage,
|
||||
});
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
|
||||
addToken: bindActionCreators(TokenActions.add, dispatch),
|
||||
loadTokens: bindActionCreators(TokenActions.load, dispatch),
|
||||
removeToken: bindActionCreators(TokenActions.remove, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Summary);
|
@ -0,0 +1,115 @@
|
||||
/* @flow */
|
||||
import styled from 'styled-components';
|
||||
import React from 'react';
|
||||
import { H2 } from 'components/Heading';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import Icon from 'components/Icon';
|
||||
import ICONS from 'config/icons';
|
||||
import colors from 'config/colors';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Content from 'views/Wallet/components/Content';
|
||||
|
||||
import CoinLogo from 'components/images/CoinLogo';
|
||||
import * as stateUtils from 'reducers/utils';
|
||||
import Link from 'components/Link';
|
||||
import { FONT_WEIGHT, FONT_SIZE } from 'config/variables';
|
||||
import AccountBalance from '../components/Balance';
|
||||
|
||||
import type { Props } from './Container';
|
||||
|
||||
const AccountHeading = styled.div`
|
||||
padding-bottom: 35px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const H2Wrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
`;
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
position: relative;
|
||||
top: 2px;
|
||||
`;
|
||||
|
||||
const AccountName = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const AccountTitle = styled.div`
|
||||
font-size: ${FONT_SIZE.WALLET_TITLE};
|
||||
font-weight: ${FONT_WEIGHT.BASE};
|
||||
color: ${colors.WALLET_TITLE};
|
||||
`;
|
||||
|
||||
const StyledCoinLogo = styled(CoinLogo)`
|
||||
margin-right: 10px;
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
position: relative;
|
||||
top: -7px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccountSummary = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
pending,
|
||||
loader,
|
||||
shouldRender,
|
||||
} = props.selectedAccount;
|
||||
|
||||
const { type, title, message } = loader;
|
||||
|
||||
if (!device || !account || !network || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
|
||||
|
||||
const explorerLink: string = `${network.explorer.address}${account.address}`;
|
||||
const pendingAmount: BigNumber = stateUtils.getPendingAmount(pending, network.symbol);
|
||||
const balance: string = new BigNumber(account.balance).minus(pendingAmount).toString(10);
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<React.Fragment>
|
||||
<AccountHeading>
|
||||
<AccountName>
|
||||
<StyledCoinLogo network={account.network} />
|
||||
<AccountTitle>Account #{parseInt(account.index, 10) + 1}</AccountTitle>
|
||||
</AccountName>
|
||||
<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>
|
||||
</React.Fragment>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountSummary;
|