mirror of
https://github.com/trezor/trezor-wallet
synced 2025-01-05 13:51:01 +00:00
commit
a7690f765f
@ -1,3 +1,11 @@
|
|||||||
|
## feature/ripple
|
||||||
|
__changed__
|
||||||
|
- Split code to coin specific components. actions and reducers
|
||||||
|
- Use TrezorConnect to communicate with trezor-blockchain-link
|
||||||
|
|
||||||
|
__added__
|
||||||
|
- Ripple support
|
||||||
|
|
||||||
## 1.0.2-beta
|
## 1.0.2-beta
|
||||||
__changed__
|
__changed__
|
||||||
- Fiat rates from coingecko (https://github.com/trezor/trezor-wallet/pull/242)
|
- Fiat rates from coingecko (https://github.com/trezor/trezor-wallet/pull/242)
|
||||||
|
@ -68,8 +68,9 @@
|
|||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^2.2.0",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"styled-components": "^4.1.2",
|
"styled-components": "^4.1.2",
|
||||||
|
"styled-media-query": "^2.0.2",
|
||||||
"styled-normalize": "^8.0.4",
|
"styled-normalize": "^8.0.4",
|
||||||
"trezor-connect": "6.0.2",
|
"trezor-connect": "6.0.3-beta.4",
|
||||||
"web3": "1.0.0-beta.35",
|
"web3": "1.0.0-beta.35",
|
||||||
"webpack": "^4.16.3",
|
"webpack": "^4.16.3",
|
||||||
"webpack-build-notifier": "^0.1.29",
|
"webpack-build-notifier": "^0.1.29",
|
||||||
|
@ -1,6 +1,19 @@
|
|||||||
{
|
{
|
||||||
"networks": [
|
"networks": [
|
||||||
{
|
{
|
||||||
|
"type": "ripple",
|
||||||
|
"name": "Ripple Testnet",
|
||||||
|
"testnet": true,
|
||||||
|
"symbol": "XRP",
|
||||||
|
"shortcut": "xrp",
|
||||||
|
"bip44": "m/44'/144'/a'/0/0",
|
||||||
|
"explorer": {
|
||||||
|
"tx": "https://sisyfos.trezor.io/ripple-testnet-explorer/tx/",
|
||||||
|
"address": "https://sisyfos.trezor.io/ripple-testnet-explorer/address/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ethereum",
|
||||||
"name": "Ethereum",
|
"name": "Ethereum",
|
||||||
"symbol": "ETH",
|
"symbol": "ETH",
|
||||||
"shortcut": "eth",
|
"shortcut": "eth",
|
||||||
@ -19,6 +32,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"type": "ethereum",
|
||||||
"name": "Ethereum Classic",
|
"name": "Ethereum Classic",
|
||||||
"symbol": "ETC",
|
"symbol": "ETC",
|
||||||
"shortcut": "etc",
|
"shortcut": "etc",
|
||||||
@ -37,7 +51,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"type": "ethereum",
|
||||||
"name": "Ethereum Ropsten",
|
"name": "Ethereum Ropsten",
|
||||||
|
"testnet": true,
|
||||||
"symbol": "tROP",
|
"symbol": "tROP",
|
||||||
"shortcut": "trop",
|
"shortcut": "trop",
|
||||||
"chainId": 3,
|
"chainId": 3,
|
||||||
@ -64,6 +80,10 @@
|
|||||||
{
|
{
|
||||||
"network": "etc",
|
"network": "etc",
|
||||||
"url": "https://api.coingecko.com/api/v3/coins/ethereum-classic"
|
"url": "https://api.coingecko.com/api/v3/coins/ethereum-classic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"network": "xrp",
|
||||||
|
"url": "https://api.coingecko.com/api/v3/coins/ripple"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -1,184 +1,25 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import TrezorConnect from 'trezor-connect';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import * as BLOCKCHAIN from 'actions/constants/blockchain';
|
import * as BLOCKCHAIN from 'actions/constants/blockchain';
|
||||||
|
import * as EthereumBlockchainActions from 'actions/ethereum/BlockchainActions';
|
||||||
|
import * as RippleBlockchainActions from 'actions/ripple/BlockchainActions';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
TrezorDevice,
|
|
||||||
Dispatch,
|
Dispatch,
|
||||||
GetState,
|
GetState,
|
||||||
PromiseAction,
|
PromiseAction,
|
||||||
} from 'flowtype';
|
} from 'flowtype';
|
||||||
import type { EthereumAccount } from 'trezor-connect';
|
import type { BlockchainBlock, BlockchainNotification, BlockchainError } from 'trezor-connect';
|
||||||
import type { Token } from 'reducers/TokensReducer';
|
|
||||||
import type { NetworkToken } from 'reducers/LocalStorageReducer';
|
|
||||||
import * as Web3Actions from './Web3Actions';
|
|
||||||
import * as AccountsActions from './AccountsActions';
|
|
||||||
|
|
||||||
export type BlockchainAction = {
|
export type BlockchainAction = {
|
||||||
type: typeof BLOCKCHAIN.READY,
|
type: typeof BLOCKCHAIN.READY,
|
||||||
|
} | {
|
||||||
|
type: typeof BLOCKCHAIN.UPDATE_FEE,
|
||||||
|
shortcut: string,
|
||||||
|
fee: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const discoverAccount = (device: TrezorDevice, address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
|
|
||||||
// get data from connect
|
|
||||||
// Temporary disabled, enable after trezor-connect@5.0.32 release
|
|
||||||
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 { transactions: txs.payload, ...web3account };
|
|
||||||
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 onBlockMined = (coinInfo: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
|
||||||
// incoming "coinInfo" from TrezorConnect is CoinInfo | EthereumNetwork type
|
|
||||||
const network: string = coinInfo.shortcut.toLowerCase();
|
|
||||||
|
|
||||||
// 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]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// not used for now, waiting for fix in blockbook
|
|
||||||
/*
|
|
||||||
export const onNotification = (payload: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
|
||||||
// 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 subscribe = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
|
||||||
const addresses: Array<string> = getState().accounts.filter(a => a.network === network).map(a => a.address); // eslint-disable-line no-unused-vars
|
|
||||||
await TrezorConnect.blockchainSubscribe({
|
|
||||||
// accounts: addresses,
|
|
||||||
accounts: [],
|
|
||||||
coin: network,
|
|
||||||
});
|
|
||||||
// init web3 instance if not exists
|
|
||||||
await dispatch(Web3Actions.initWeb3(network));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Conditionally subscribe to blockchain backend
|
// Conditionally subscribe to blockchain backend
|
||||||
// called after TrezorConnect.init successfully emits TRANSPORT.START event
|
// called after TrezorConnect.init successfully emits TRANSPORT.START event
|
||||||
// checks if there are discovery processes loaded from LocalStorage
|
// checks if there are discovery processes loaded from LocalStorage
|
||||||
@ -205,8 +46,75 @@ export const init = (): PromiseAction<void> => async (dispatch: Dispatch, getSta
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const subscribe = (networkName: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
|
const { config } = getState().localStorage;
|
||||||
|
const network = config.networks.find(c => c.shortcut === networkName);
|
||||||
|
if (!network) return;
|
||||||
|
|
||||||
|
switch (network.type) {
|
||||||
|
case 'ethereum':
|
||||||
|
await dispatch(EthereumBlockchainActions.subscribe(networkName));
|
||||||
|
break;
|
||||||
|
case 'ripple':
|
||||||
|
await dispatch(RippleBlockchainActions.subscribe(networkName));
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onBlockMined = (payload: $ElementType<BlockchainBlock, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
|
const shortcut = payload.coin.shortcut.toLowerCase();
|
||||||
|
if (getState().router.location.state.network !== shortcut) return;
|
||||||
|
|
||||||
|
const { config } = getState().localStorage;
|
||||||
|
const network = config.networks.find(c => c.shortcut === shortcut);
|
||||||
|
if (!network) return;
|
||||||
|
|
||||||
|
switch (network.type) {
|
||||||
|
case 'ethereum':
|
||||||
|
await dispatch(EthereumBlockchainActions.onBlockMined(shortcut));
|
||||||
|
break;
|
||||||
|
case 'ripple':
|
||||||
|
await dispatch(RippleBlockchainActions.onBlockMined(shortcut));
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onNotification = (payload: $ElementType<BlockchainNotification, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
|
const shortcut = payload.coin.shortcut.toLowerCase();
|
||||||
|
const { config } = getState().localStorage;
|
||||||
|
const network = config.networks.find(c => c.shortcut === shortcut);
|
||||||
|
if (!network) return;
|
||||||
|
|
||||||
|
switch (network.type) {
|
||||||
|
case 'ethereum':
|
||||||
|
// this is not working until blockchain-link will start support blockbook backends
|
||||||
|
await dispatch(EthereumBlockchainActions.onNotification());
|
||||||
|
break;
|
||||||
|
case 'ripple':
|
||||||
|
await dispatch(RippleBlockchainActions.onNotification(payload));
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle BLOCKCHAIN.ERROR event from TrezorConnect
|
// Handle BLOCKCHAIN.ERROR event from TrezorConnect
|
||||||
// disconnect and remove Web3 webscocket instance if exists
|
// disconnect and remove Web3 webscocket instance if exists
|
||||||
export const error = (payload: any): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
export const onError = (payload: $ElementType<BlockchainError, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
dispatch(Web3Actions.disconnect(payload.coin));
|
const shortcut = payload.coin.shortcut.toLowerCase();
|
||||||
|
const { config } = getState().localStorage;
|
||||||
|
const network = config.networks.find(c => c.shortcut === shortcut);
|
||||||
|
if (!network) return;
|
||||||
|
|
||||||
|
switch (network.type) {
|
||||||
|
case 'ethereum':
|
||||||
|
await dispatch(EthereumBlockchainActions.onError(shortcut));
|
||||||
|
break;
|
||||||
|
case 'ripple':
|
||||||
|
// this error is handled in BlockchainReducer
|
||||||
|
// await dispatch(RippleBlockchainActions.onBlockMined(shortcut));
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
};
|
};
|
@ -1,7 +1,6 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import TrezorConnect from 'trezor-connect';
|
import TrezorConnect, { UI } from 'trezor-connect';
|
||||||
import EthereumjsUtil from 'ethereumjs-util';
|
|
||||||
import * as DISCOVERY from 'actions/constants/discovery';
|
import * as DISCOVERY from 'actions/constants/discovery';
|
||||||
import * as ACCOUNT from 'actions/constants/account';
|
import * as ACCOUNT from 'actions/constants/account';
|
||||||
import * as NOTIFICATION from 'actions/constants/notification';
|
import * as NOTIFICATION from 'actions/constants/notification';
|
||||||
@ -14,29 +13,25 @@ import type {
|
|||||||
GetState,
|
GetState,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
TrezorDevice,
|
TrezorDevice,
|
||||||
|
Account,
|
||||||
} from 'flowtype';
|
} from 'flowtype';
|
||||||
import type { Discovery, State } from 'reducers/DiscoveryReducer';
|
import type { Discovery, State } from 'reducers/DiscoveryReducer';
|
||||||
import * as BlockchainActions from './BlockchainActions';
|
import * as BlockchainActions from './BlockchainActions';
|
||||||
|
import * as EthereumDiscoveryActions from './ethereum/DiscoveryActions';
|
||||||
|
import * as RippleDiscoveryActions from './ripple/DiscoveryActions';
|
||||||
|
|
||||||
export type DiscoveryStartAction = {
|
export type DiscoveryStartAction = EthereumDiscoveryActions.DiscoveryStartAction | RippleDiscoveryActions.DiscoveryStartAction;
|
||||||
type: typeof DISCOVERY.START,
|
|
||||||
device: TrezorDevice,
|
|
||||||
network: string,
|
|
||||||
publicKey: string,
|
|
||||||
chainCode: string,
|
|
||||||
basePath: Array<number>,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DiscoveryWaitingAction = {
|
export type DiscoveryWaitingAction = {
|
||||||
type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN,
|
type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN | typeof DISCOVERY.FIRMWARE_NOT_SUPPORTED | typeof DISCOVERY.FIRMWARE_OUTDATED,
|
||||||
device: TrezorDevice,
|
device: TrezorDevice,
|
||||||
network: string
|
network: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DiscoveryCompleteAction = {
|
export type DiscoveryCompleteAction = {
|
||||||
type: typeof DISCOVERY.COMPLETE,
|
type: typeof DISCOVERY.COMPLETE,
|
||||||
device: TrezorDevice,
|
device: TrezorDevice,
|
||||||
network: string
|
network: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DiscoveryAction = {
|
export type DiscoveryAction = {
|
||||||
@ -122,44 +117,43 @@ const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean)
|
|||||||
// first iteration
|
// first iteration
|
||||||
// generate public key for this account
|
// generate public key for this account
|
||||||
// start discovery process
|
// start discovery process
|
||||||
const begin = (device: TrezorDevice, network: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
const begin = (device: TrezorDevice, networkName: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
const { config } = getState().localStorage;
|
const { config } = getState().localStorage;
|
||||||
const networkData = config.networks.find(c => c.shortcut === network);
|
const network = config.networks.find(c => c.shortcut === networkName);
|
||||||
if (!networkData) return;
|
if (!network) return;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: DISCOVERY.WAITING_FOR_DEVICE,
|
type: DISCOVERY.WAITING_FOR_DEVICE,
|
||||||
device,
|
device,
|
||||||
network,
|
network: networkName,
|
||||||
});
|
});
|
||||||
|
|
||||||
// get xpub from TREZOR
|
let startAction: DiscoveryStartAction;
|
||||||
const response = await TrezorConnect.getPublicKey({
|
|
||||||
device: {
|
|
||||||
path: device.path,
|
|
||||||
instance: device.instance,
|
|
||||||
state: device.state,
|
|
||||||
},
|
|
||||||
path: networkData.bip44,
|
|
||||||
keepSession: true, // acquire and hold session
|
|
||||||
//useEmptyPassphrase: !device.instance,
|
|
||||||
useEmptyPassphrase: device.useEmptyPassphrase,
|
|
||||||
});
|
|
||||||
|
|
||||||
// handle TREZOR response error
|
try {
|
||||||
if (!response.success) {
|
switch (network.type) {
|
||||||
|
case 'ethereum':
|
||||||
|
startAction = await dispatch(EthereumDiscoveryActions.begin(device, network));
|
||||||
|
break;
|
||||||
|
case 'ripple':
|
||||||
|
startAction = await dispatch(RippleDiscoveryActions.begin(device, network));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`DiscoveryActions.begin: Unknown network type: ${network.type}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: NOTIFICATION.ADD,
|
type: NOTIFICATION.ADD,
|
||||||
payload: {
|
payload: {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Discovery error',
|
title: 'Discovery error',
|
||||||
message: response.payload.error,
|
message: error.message,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: 'Try again',
|
label: 'Try again',
|
||||||
callback: () => {
|
callback: () => {
|
||||||
dispatch(start(device, network));
|
dispatch(start(device, networkName));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -174,64 +168,52 @@ const begin = (device: TrezorDevice, network: string): AsyncAction => async (dis
|
|||||||
const discoveryProcess = getState().discovery.find(d => d.deviceState === device.state && d.network === network);
|
const discoveryProcess = getState().discovery.find(d => d.deviceState === device.state && d.network === network);
|
||||||
if (dispatch(isProcessInterrupted(discoveryProcess))) return;
|
if (dispatch(isProcessInterrupted(discoveryProcess))) return;
|
||||||
|
|
||||||
const basePath: Array<number> = response.payload.path;
|
|
||||||
|
|
||||||
// send data to reducer
|
// send data to reducer
|
||||||
dispatch({
|
dispatch(startAction);
|
||||||
type: DISCOVERY.START,
|
|
||||||
network,
|
|
||||||
device,
|
|
||||||
publicKey: response.payload.publicKey,
|
|
||||||
chainCode: response.payload.chainCode,
|
|
||||||
basePath,
|
|
||||||
});
|
|
||||||
|
|
||||||
// next iteration
|
// next iteration
|
||||||
dispatch(start(device, network));
|
dispatch(start(device, networkName));
|
||||||
};
|
};
|
||||||
|
|
||||||
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch): Promise<void> => {
|
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
const { completed } = discoveryProcess;
|
const { config } = getState().localStorage;
|
||||||
|
const network = config.networks.find(c => c.shortcut === discoveryProcess.network);
|
||||||
|
if (!network) return;
|
||||||
|
|
||||||
const derivedKey = discoveryProcess.hdKey.derive(`m/${discoveryProcess.accountIndex}`);
|
const { completed, accountIndex } = discoveryProcess;
|
||||||
const path = discoveryProcess.basePath.concat(discoveryProcess.accountIndex);
|
let account: Account;
|
||||||
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
|
|
||||||
try {
|
try {
|
||||||
const account = await dispatch(BlockchainActions.discoverAccount(device, ethAddress, network));
|
switch (network.type) {
|
||||||
// check for interruption
|
case 'ethereum':
|
||||||
if (dispatch(isProcessInterrupted(discoveryProcess))) return;
|
account = await dispatch(EthereumDiscoveryActions.discoverAccount(device, discoveryProcess));
|
||||||
|
break;
|
||||||
// const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0';
|
case 'ripple':
|
||||||
const accountIsEmpty = account.nonce <= 0 && account.balance === '0';
|
account = await dispatch(RippleDiscoveryActions.discoverAccount(device, discoveryProcess));
|
||||||
if (!accountIsEmpty || (accountIsEmpty && completed) || (accountIsEmpty && discoveryProcess.accountIndex === 0)) {
|
break;
|
||||||
dispatch({
|
default:
|
||||||
type: ACCOUNT.CREATE,
|
throw new Error(`DiscoveryActions.discoverAccount: Unknown network type: ${network.type}`);
|
||||||
payload: {
|
|
||||||
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,
|
|
||||||
nonce: account.nonce,
|
|
||||||
block: account.block,
|
|
||||||
transactions: account.transactions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accountIsEmpty) {
|
|
||||||
dispatch(finish(device, discoveryProcess));
|
|
||||||
} else if (!completed) {
|
|
||||||
dispatch(discoverAccount(device, discoveryProcess));
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// handle not supported firmware error
|
||||||
|
if (error.message === UI.FIRMWARE_NOT_SUPPORTED) {
|
||||||
|
dispatch({
|
||||||
|
type: DISCOVERY.FIRMWARE_NOT_SUPPORTED,
|
||||||
|
device,
|
||||||
|
network: discoveryProcess.network,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle outdated firmware error
|
||||||
|
if (error.message === UI.FIRMWARE) {
|
||||||
|
dispatch({
|
||||||
|
type: DISCOVERY.FIRMWARE_OUTDATED,
|
||||||
|
device,
|
||||||
|
network: discoveryProcess.network,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: DISCOVERY.STOP,
|
type: DISCOVERY.STOP,
|
||||||
device,
|
device,
|
||||||
@ -254,6 +236,23 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dispatch(isProcessInterrupted(discoveryProcess))) return;
|
||||||
|
|
||||||
|
const accountIsEmpty = account.empty;
|
||||||
|
if (!accountIsEmpty || (accountIsEmpty && completed) || (accountIsEmpty && accountIndex === 0)) {
|
||||||
|
dispatch({
|
||||||
|
type: ACCOUNT.CREATE,
|
||||||
|
payload: account,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountIsEmpty) {
|
||||||
|
dispatch(finish(device, discoveryProcess));
|
||||||
|
} else if (!completed) {
|
||||||
|
dispatch(discoverAccount(device, discoveryProcess));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -141,10 +141,9 @@ const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise<void> =>
|
|||||||
try {
|
try {
|
||||||
const config: Config = await httpRequest(AppConfigJSON, 'json');
|
const config: Config = await httpRequest(AppConfigJSON, 'json');
|
||||||
|
|
||||||
// remove ropsten testnet from config networks
|
// remove testnets from config networks
|
||||||
if (!buildUtils.isDev()) {
|
if (!buildUtils.isDev()) {
|
||||||
const index = config.networks.findIndex(c => c.shortcut === 'trop');
|
config.networks = config.networks.filter(n => !n.testnet);
|
||||||
delete config.networks[index];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ERC20Abi = await httpRequest(Erc20AbiJSON, 'json');
|
const ERC20Abi = await httpRequest(Erc20AbiJSON, 'json');
|
||||||
@ -156,8 +155,10 @@ const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise<void> =>
|
|||||||
// load tokens
|
// load tokens
|
||||||
const tokens = await config.networks.reduce(async (promise: Promise<TokensCollection>, network: Network): Promise<TokensCollection> => {
|
const tokens = await config.networks.reduce(async (promise: Promise<TokensCollection>, network: Network): Promise<TokensCollection> => {
|
||||||
const collection: TokensCollection = await promise;
|
const collection: TokensCollection = await promise;
|
||||||
const json = await httpRequest(network.tokens, 'json');
|
if (network.tokens) {
|
||||||
collection[network.shortcut] = json;
|
const json = await httpRequest(network.tokens, 'json');
|
||||||
|
collection[network.shortcut] = json;
|
||||||
|
}
|
||||||
return collection;
|
return collection;
|
||||||
}, Promise.resolve({}));
|
}, Promise.resolve({}));
|
||||||
|
|
||||||
@ -175,7 +176,7 @@ const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise<void> =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const VERSION: string = '1';
|
const VERSION: string = '2';
|
||||||
|
|
||||||
const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => {
|
const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => {
|
||||||
// validate version
|
// validate version
|
||||||
|
@ -13,12 +13,11 @@ export type PendingTxAction = {
|
|||||||
payload: PendingTx
|
payload: PendingTx
|
||||||
} | {
|
} | {
|
||||||
type: typeof PENDING.TX_RESOLVED,
|
type: typeof PENDING.TX_RESOLVED,
|
||||||
tx: PendingTx,
|
hash: string,
|
||||||
receipt?: Object,
|
|
||||||
} | {
|
} | {
|
||||||
type: typeof PENDING.TX_REJECTED,
|
type: typeof PENDING.TX_REJECTED,
|
||||||
tx: PendingTx,
|
hash: string,
|
||||||
} | {
|
} | {
|
||||||
type: typeof PENDING.TX_TOKEN_ERROR,
|
type: typeof PENDING.TX_TOKEN_ERROR,
|
||||||
tx: PendingTx,
|
hash: string,
|
||||||
}
|
}
|
@ -50,7 +50,9 @@ export const showUnverifiedAddress = (): Action => ({
|
|||||||
//export const showAddress = (address_n: string): AsyncAction => {
|
//export const showAddress = (address_n: string): AsyncAction => {
|
||||||
export const showAddress = (path: Array<number>): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
export const showAddress = (path: Array<number>): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||||
const selected = getState().wallet.selectedDevice;
|
const selected = getState().wallet.selectedDevice;
|
||||||
if (!selected) return;
|
const { network } = getState().selectedAccount;
|
||||||
|
|
||||||
|
if (!selected || !network) return;
|
||||||
|
|
||||||
if (selected && (!selected.connected || !selected.available)) {
|
if (selected && (!selected.connected || !selected.available)) {
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -60,18 +62,30 @@ export const showAddress = (path: Array<number>): AsyncAction => async (dispatch
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await TrezorConnect.ethereumGetAddress({
|
const params = {
|
||||||
device: {
|
device: {
|
||||||
path: selected.path,
|
path: selected.path,
|
||||||
instance: selected.instance,
|
instance: selected.instance,
|
||||||
state: selected.state,
|
state: selected.state,
|
||||||
},
|
},
|
||||||
path,
|
path,
|
||||||
// useEmptyPassphrase: !selected.instance,
|
|
||||||
useEmptyPassphrase: selected.useEmptyPassphrase,
|
useEmptyPassphrase: selected.useEmptyPassphrase,
|
||||||
});
|
};
|
||||||
|
|
||||||
if (response && response.success) {
|
let response;
|
||||||
|
switch (network.type) {
|
||||||
|
case 'ethereum':
|
||||||
|
response = await TrezorConnect.ethereumGetAddress(params);
|
||||||
|
break;
|
||||||
|
case 'ripple':
|
||||||
|
response = await TrezorConnect.rippleGetAddress(params);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
response = { payload: { error: `ReceiveActions.showAddress: Unknown network type: ${network.type}` } };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: RECEIVE.SHOW_ADDRESS,
|
type: RECEIVE.SHOW_ADDRESS,
|
||||||
});
|
});
|
||||||
|
@ -6,7 +6,6 @@ import * as ACCOUNT from 'actions/constants/account';
|
|||||||
import * as DISCOVERY from 'actions/constants/discovery';
|
import * as DISCOVERY from 'actions/constants/discovery';
|
||||||
import * as TOKEN from 'actions/constants/token';
|
import * as TOKEN from 'actions/constants/token';
|
||||||
import * as PENDING from 'actions/constants/pendingTx';
|
import * as PENDING from 'actions/constants/pendingTx';
|
||||||
import * as SEND from 'actions/constants/send';
|
|
||||||
|
|
||||||
import * as reducerUtils from 'reducers/utils';
|
import * as reducerUtils from 'reducers/utils';
|
||||||
|
|
||||||
@ -67,6 +66,24 @@ const getAccountLoader = (state: State, selectedAccount: SelectedAccountState):
|
|||||||
if (account) return null;
|
if (account) return null;
|
||||||
// account not found (yet). checking why...
|
// account not found (yet). checking why...
|
||||||
|
|
||||||
|
if (discovery && discovery.fwOutdated) {
|
||||||
|
return {
|
||||||
|
type: 'info',
|
||||||
|
title: `Device ${device.instanceLabel} firmware is outdated`,
|
||||||
|
message: 'TODO: update firmware explanation',
|
||||||
|
shouldRender: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discovery && discovery.fwNotSupported) {
|
||||||
|
return {
|
||||||
|
type: 'info',
|
||||||
|
title: `Device ${device.instanceLabel} is not supported`,
|
||||||
|
message: 'TODO: model T1 not supported explanation',
|
||||||
|
shouldRender: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!discovery || (discovery.waitingForDevice || discovery.interrupted)) {
|
if (!discovery || (discovery.waitingForDevice || discovery.interrupted)) {
|
||||||
if (device.connected) {
|
if (device.connected) {
|
||||||
// case 1: device is connected but discovery not started yet (probably waiting for auth)
|
// case 1: device is connected but discovery not started yet (probably waiting for auth)
|
||||||
@ -166,7 +183,6 @@ const getAccountNotification = (state: State, selectedAccount: SelectedAccountSt
|
|||||||
const actions = [
|
const actions = [
|
||||||
LOCATION_CHANGE,
|
LOCATION_CHANGE,
|
||||||
...Object.values(BLOCKCHAIN).filter(v => typeof v === 'string'),
|
...Object.values(BLOCKCHAIN).filter(v => typeof v === 'string'),
|
||||||
SEND.TX_COMPLETE,
|
|
||||||
WALLET.SET_SELECTED_DEVICE,
|
WALLET.SET_SELECTED_DEVICE,
|
||||||
WALLET.UPDATE_SELECTED_DEVICE,
|
WALLET.UPDATE_SELECTED_DEVICE,
|
||||||
...Object.values(ACCOUNT).filter(v => typeof v === 'string' && v !== ACCOUNT.UPDATE_SELECTED_ACCOUNT && v !== ACCOUNT.DISPOSE), // exported values got unwanted "__esModule: true" as first element
|
...Object.values(ACCOUNT).filter(v => typeof v === 'string' && v !== ACCOUNT.UPDATE_SELECTED_ACCOUNT && v !== ACCOUNT.DISPOSE), // exported values got unwanted "__esModule: true" as first element
|
||||||
|
@ -1,18 +1,7 @@
|
|||||||
/* @flow */
|
/* @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 ACCOUNT from 'actions/constants/account';
|
||||||
import * as NOTIFICATION from 'actions/constants/notification';
|
|
||||||
import * as SEND from 'actions/constants/send';
|
import * as SEND from 'actions/constants/send';
|
||||||
import * as WEB3 from 'actions/constants/web3';
|
import * as WEB3 from 'actions/constants/web3';
|
||||||
import * as ValidationActions from 'actions/SendFormValidationActions';
|
|
||||||
|
|
||||||
import { initialState } from 'reducers/SendFormReducer';
|
|
||||||
import { findToken } from 'reducers/TokensReducer';
|
|
||||||
import * as reducerUtils from 'reducers/utils';
|
|
||||||
import * as ethUtils from 'utils/ethUtils';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
@ -20,33 +9,27 @@ import type {
|
|||||||
State as ReducersState,
|
State as ReducersState,
|
||||||
Action,
|
Action,
|
||||||
ThunkAction,
|
ThunkAction,
|
||||||
AsyncAction,
|
|
||||||
TrezorDevice,
|
|
||||||
} from 'flowtype';
|
} from 'flowtype';
|
||||||
import type { State, FeeLevel } from 'reducers/SendFormReducer';
|
import type { State as EthereumState } from 'reducers/SendFormEthereumReducer';
|
||||||
import type { Account } from 'reducers/AccountsReducer';
|
import type { State as RippleState } from 'reducers/SendFormRippleReducer';
|
||||||
import * as SessionStorageActions from './SessionStorageActions';
|
|
||||||
import { prepareEthereumTx, serializeEthereumTx } from './TxActions';
|
|
||||||
import * as BlockchainActions from './BlockchainActions';
|
|
||||||
|
|
||||||
export type SendTxAction = {
|
import * as EthereumSendFormActions from './ethereum/SendFormActions';
|
||||||
type: typeof SEND.TX_COMPLETE,
|
import * as RippleSendFormActions from './ripple/SendFormActions';
|
||||||
account: Account,
|
|
||||||
selectedCurrency: string,
|
|
||||||
amount: string,
|
|
||||||
total: string,
|
|
||||||
tx: any,
|
|
||||||
nonce: number,
|
|
||||||
txid: string,
|
|
||||||
txData: any,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SendFormAction = {
|
export type SendFormAction = {
|
||||||
type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE,
|
type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE,
|
||||||
state: State,
|
networkType: 'ethereum',
|
||||||
|
state: EthereumState,
|
||||||
|
} | {
|
||||||
|
type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE,
|
||||||
|
networkType: 'ripple',
|
||||||
|
state: RippleState,
|
||||||
} | {
|
} | {
|
||||||
type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR,
|
type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR,
|
||||||
} | SendTxAction;
|
} | {
|
||||||
|
type: typeof SEND.TX_COMPLETE,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// list of all actions which has influence on "sendForm" reducer
|
// list of all actions which has influence on "sendForm" reducer
|
||||||
// other actions will be ignored
|
// other actions will be ignored
|
||||||
@ -67,521 +50,16 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction =
|
|||||||
// do not proceed if it's not "send" url
|
// do not proceed if it's not "send" url
|
||||||
if (!currentState.router.location.state.send) return;
|
if (!currentState.router.location.state.send) return;
|
||||||
|
|
||||||
// if action type is SEND.VALIDATION which is called as result of this process
|
const { network } = currentState.selectedAccount;
|
||||||
// save data to session storage
|
|
||||||
if (action.type === SEND.VALIDATION) {
|
|
||||||
dispatch(SessionStorageActions.saveDraftTransaction());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if send form was not initialized
|
|
||||||
if (currentState.sendForm.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.sendForm.currency !== currentState.sendForm.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.sendForm.currency, account.deviceState);
|
|
||||||
if (!token) {
|
|
||||||
// token not found, re-init form
|
|
||||||
dispatch(init());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// check if "sendForm" reducer changed
|
|
||||||
if (!shouldUpdate) {
|
|
||||||
shouldUpdate = reducerUtils.observeChanges(prevState.sendForm, currentState.sendForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldUpdate) {
|
|
||||||
const validated = dispatch(ValidationActions.validation());
|
|
||||||
dispatch({
|
|
||||||
type: SEND.VALIDATION,
|
|
||||||
state: validated,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called from "observe" action
|
|
||||||
* Initialize "sendForm" 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.loadDraftTransaction());
|
|
||||||
if (stateFromStorage) {
|
|
||||||
// TODO: consider if current gasPrice should be set here as "recommendedGasPrice"
|
|
||||||
dispatch({
|
|
||||||
type: SEND.INIT,
|
|
||||||
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,
|
|
||||||
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().sendForm;
|
|
||||||
dispatch({
|
|
||||||
type: SEND.CHANGE,
|
|
||||||
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().sendForm;
|
|
||||||
dispatch({
|
|
||||||
type: SEND.CHANGE,
|
|
||||||
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().sendForm;
|
|
||||||
|
|
||||||
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,
|
|
||||||
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().sendForm;
|
|
||||||
dispatch({
|
|
||||||
type: SEND.CHANGE,
|
|
||||||
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().sendForm;
|
|
||||||
|
|
||||||
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,
|
|
||||||
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().sendForm;
|
|
||||||
const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, state.gasLimit, state.selectedFeeLevel);
|
|
||||||
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: SEND.CHANGE,
|
|
||||||
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().sendForm;
|
|
||||||
// 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,
|
|
||||||
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().sendForm;
|
|
||||||
// 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,
|
|
||||||
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().sendForm;
|
|
||||||
dispatch({
|
|
||||||
type: SEND.CHANGE,
|
|
||||||
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().sendForm;
|
|
||||||
dispatch({
|
|
||||||
type: SEND.CHANGE,
|
|
||||||
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().sendForm;
|
|
||||||
const { network } = getState().selectedAccount;
|
|
||||||
if (!network) return;
|
if (!network) return;
|
||||||
|
|
||||||
const isToken = state.currency !== state.networkSymbol;
|
switch (network.type) {
|
||||||
const gasLimit = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString();
|
case 'ethereum':
|
||||||
|
dispatch(EthereumSendFormActions.observe(prevState, action));
|
||||||
dispatch({
|
break;
|
||||||
type: SEND.CHANGE,
|
case 'ripple':
|
||||||
state: {
|
dispatch(RippleSendFormActions.observe(prevState, action));
|
||||||
...state,
|
break;
|
||||||
calculatingGasLimit: false,
|
default: break;
|
||||||
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().sendForm;
|
|
||||||
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().sendForm.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().sendForm;
|
|
||||||
|
|
||||||
const isToken: boolean = currentState.currency !== currentState.networkSymbol;
|
|
||||||
const pendingNonce: number = reducerUtils.getPendingNonce(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,
|
|
||||||
account,
|
|
||||||
selectedCurrency: currentState.currency,
|
|
||||||
amount: currentState.amount,
|
|
||||||
total: currentState.total,
|
|
||||||
tx: txData,
|
|
||||||
nonce,
|
|
||||||
txid,
|
|
||||||
txData,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
};
|
|
@ -2,7 +2,8 @@
|
|||||||
import * as storageUtils from 'utils/storage';
|
import * as storageUtils from 'utils/storage';
|
||||||
import { findToken } from 'reducers/TokensReducer';
|
import { findToken } from 'reducers/TokensReducer';
|
||||||
|
|
||||||
import type { State as SendFormState } from 'reducers/SendFormReducer';
|
import type { State as EthereumSendFormState } from 'reducers/SendFormEthereumReducer';
|
||||||
|
import type { State as RippleSendFormState } from 'reducers/SendFormRippleReducer';
|
||||||
import type {
|
import type {
|
||||||
ThunkAction,
|
ThunkAction,
|
||||||
PayloadAction,
|
PayloadAction,
|
||||||
@ -20,18 +21,18 @@ const getTxDraftKey = (getState: GetState): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const saveDraftTransaction = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
export const saveDraftTransaction = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||||
const state = getState().sendForm;
|
const state = getState().sendFormEthereum;
|
||||||
if (state.untouched) return;
|
if (state.untouched) return;
|
||||||
|
|
||||||
const key = getTxDraftKey(getState);
|
const key = getTxDraftKey(getState);
|
||||||
storageUtils.set(TYPE, key, JSON.stringify(state));
|
storageUtils.set(TYPE, key, JSON.stringify(state));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadDraftTransaction = (): PayloadAction<?SendFormState> => (dispatch: Dispatch, getState: GetState): ?SendFormState => {
|
export const loadEthereumDraftTransaction = (): PayloadAction<?EthereumSendFormState> => (dispatch: Dispatch, getState: GetState): ?EthereumSendFormState => {
|
||||||
const key = getTxDraftKey(getState);
|
const key = getTxDraftKey(getState);
|
||||||
const value: ?string = storageUtils.get(TYPE, key);
|
const value: ?string = storageUtils.get(TYPE, key);
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const state: ?SendFormState = JSON.parse(value);
|
const state: ?EthereumSendFormState = JSON.parse(value);
|
||||||
if (!state) return null;
|
if (!state) return null;
|
||||||
// decide if draft is valid and should be returned
|
// decide if draft is valid and should be returned
|
||||||
// ignore this draft if has any error
|
// ignore this draft if has any error
|
||||||
@ -52,6 +53,21 @@ export const loadDraftTransaction = (): PayloadAction<?SendFormState> => (dispat
|
|||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const loadRippleDraftTransaction = (): PayloadAction<?RippleSendFormState> => (dispatch: Dispatch, getState: GetState): ?RippleSendFormState => {
|
||||||
|
const key = getTxDraftKey(getState);
|
||||||
|
const value: ?string = storageUtils.get(TYPE, key);
|
||||||
|
if (!value) return null;
|
||||||
|
const state: ?RippleSendFormState = JSON.parse(value);
|
||||||
|
if (!state) return null;
|
||||||
|
// decide if draft is valid and should be returned
|
||||||
|
// ignore this draft if has any error
|
||||||
|
if (Object.keys(state.errors).length > 0) {
|
||||||
|
storageUtils.remove(TYPE, key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
export const clear = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
export const clear = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||||
const key = getTxDraftKey(getState);
|
const key = getTxDraftKey(getState);
|
||||||
storageUtils.remove(TYPE, key);
|
storageUtils.remove(TYPE, key);
|
||||||
|
@ -9,7 +9,7 @@ import type {
|
|||||||
import type { State, Token } from 'reducers/TokensReducer';
|
import type { State, Token } from 'reducers/TokensReducer';
|
||||||
import type { Account } from 'reducers/AccountsReducer';
|
import type { Account } from 'reducers/AccountsReducer';
|
||||||
import type { NetworkToken } from 'reducers/LocalStorageReducer';
|
import type { NetworkToken } from 'reducers/LocalStorageReducer';
|
||||||
import * as BlockchainActions from './BlockchainActions';
|
import * as BlockchainActions from 'actions/ethereum/BlockchainActions';
|
||||||
|
|
||||||
export type TokenAction = {
|
export type TokenAction = {
|
||||||
type: typeof TOKEN.FROM_STORAGE,
|
type: typeof TOKEN.FROM_STORAGE,
|
||||||
|
@ -17,8 +17,7 @@ import type {
|
|||||||
UiMessageType,
|
UiMessageType,
|
||||||
TransportMessage,
|
TransportMessage,
|
||||||
TransportMessageType,
|
TransportMessageType,
|
||||||
BlockchainMessage,
|
BlockchainEvent,
|
||||||
BlockchainMessageType,
|
|
||||||
} from 'trezor-connect';
|
} from 'trezor-connect';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -114,13 +113,9 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
TrezorConnect.on(BLOCKCHAIN_EVENT, (event: BlockchainMessage): void => {
|
// post event to reducers
|
||||||
// post event to reducers
|
TrezorConnect.on(BLOCKCHAIN_EVENT, (event: BlockchainEvent): void => {
|
||||||
const type: BlockchainMessageType = event.type; // eslint-disable-line prefer-destructuring
|
dispatch(event);
|
||||||
dispatch({
|
|
||||||
type,
|
|
||||||
payload: event.payload,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (buildUtils.isDev()) {
|
if (buildUtils.isDev()) {
|
||||||
|
@ -137,25 +137,24 @@ export const resolvePendingTransactions = (network: string): PromiseAction<void>
|
|||||||
const instance: Web3Instance = await dispatch(initWeb3(network));
|
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||||
const pending = getState().pending.filter(p => p.network === network);
|
const pending = getState().pending.filter(p => p.network === network);
|
||||||
pending.forEach(async (tx) => {
|
pending.forEach(async (tx) => {
|
||||||
const status = await instance.web3.eth.getTransaction(tx.id);
|
const status = await instance.web3.eth.getTransaction(tx.hash);
|
||||||
if (!status) {
|
if (!status) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: PENDING.TX_REJECTED,
|
type: PENDING.TX_REJECTED,
|
||||||
tx,
|
hash: tx.hash,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const receipt = await instance.web3.eth.getTransactionReceipt(tx.id);
|
const receipt = await instance.web3.eth.getTransactionReceipt(tx.hash);
|
||||||
if (receipt) {
|
if (receipt) {
|
||||||
if (status.gas !== receipt.gasUsed) {
|
if (status.gas !== receipt.gasUsed) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: PENDING.TX_TOKEN_ERROR,
|
type: PENDING.TX_TOKEN_ERROR,
|
||||||
tx,
|
hash: tx.hash,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
dispatch({
|
dispatch({
|
||||||
type: PENDING.TX_RESOLVED,
|
type: PENDING.TX_RESOLVED,
|
||||||
tx,
|
hash: tx.hash,
|
||||||
receipt,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,7 +196,11 @@ export const updateAccount = (account: Account, newAccount: EthereumAccount, net
|
|||||||
const balance = await instance.web3.eth.getBalance(account.address);
|
const balance = await instance.web3.eth.getBalance(account.address);
|
||||||
const nonce = await instance.web3.eth.getTransactionCount(account.address);
|
const nonce = await instance.web3.eth.getTransactionCount(account.address);
|
||||||
dispatch(AccountsActions.update({
|
dispatch(AccountsActions.update({
|
||||||
...account, ...newAccount, balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), nonce,
|
...account,
|
||||||
|
...newAccount,
|
||||||
|
nonce,
|
||||||
|
balance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
|
||||||
|
availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// update tokens for this account
|
// update tokens for this account
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
export const READY: 'blockchain__ready' = 'blockchain__ready';
|
export const READY: 'blockchain__ready' = 'blockchain__ready';
|
||||||
export const CONNECTING: 'blockchain__connecting' = 'blockchain__connecting';
|
export const UPDATE_FEE: 'blockchain__update_fee' = 'blockchain__update_fee';
|
||||||
export const CONNECTED: 'blockchain__connected' = 'blockchain__connected';
|
|
||||||
export const DISCONNECTED: 'blockchain__disconnected' = 'blockchain__disconnected';
|
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
export const START: 'discovery__start' = 'discovery__start';
|
export const START: 'discovery__start' = 'discovery__start';
|
||||||
export const STOP: 'discovery__stop' = 'discovery__stop';
|
export const STOP: 'discovery__stop' = 'discovery__stop';
|
||||||
|
export const FIRMWARE_NOT_SUPPORTED: 'discovery__fw_not_supported' = 'discovery__fw_not_supported';
|
||||||
|
export const FIRMWARE_OUTDATED: 'discovery__fw_outdated' = 'discovery__fw_outdated';
|
||||||
export const COMPLETE: 'discovery__complete' = 'discovery__complete';
|
export const COMPLETE: 'discovery__complete' = 'discovery__complete';
|
||||||
export const WAITING_FOR_DEVICE: 'discovery__waiting_for_device' = 'discovery__waiting_for_device';
|
export const WAITING_FOR_DEVICE: 'discovery__waiting_for_device' = 'discovery__waiting_for_device';
|
||||||
export const WAITING_FOR_BLOCKCHAIN: 'discovery__waiting_for_blockchain' = 'discovery__waiting_for_blockchain';
|
export const WAITING_FOR_BLOCKCHAIN: 'discovery__waiting_for_blockchain' = 'discovery__waiting_for_blockchain';
|
||||||
|
@ -7,5 +7,4 @@ export const CREATE: 'web3__create' = 'web3__create';
|
|||||||
export const READY: 'web3__ready' = 'web3__ready';
|
export const READY: 'web3__ready' = 'web3__ready';
|
||||||
export const BLOCK_UPDATED: 'web3__block_updated' = 'web3__block_updated';
|
export const BLOCK_UPDATED: 'web3__block_updated' = 'web3__block_updated';
|
||||||
export const GAS_PRICE_UPDATED: 'web3__gas_price_updated' = 'web3__gas_price_updated';
|
export const GAS_PRICE_UPDATED: 'web3__gas_price_updated' = 'web3__gas_price_updated';
|
||||||
export const PENDING_TX_RESOLVED: 'web3__pending_tx_resolved' = 'web3__pending_tx_resolved';
|
|
||||||
export const DISCONNECT: 'web3__disconnect' = 'web3__disconnect';
|
export const DISCONNECT: 'web3__disconnect' = 'web3__disconnect';
|
171
src/actions/ethereum/BlockchainActions.js
Normal file
171
src/actions/ethereum/BlockchainActions.js
Normal file
@ -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));
|
||||||
|
};
|
93
src/actions/ethereum/DiscoveryActions.js
Normal file
93
src/actions/ethereum/DiscoveryActions.js
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
};
|
587
src/actions/ethereum/SendFormActions.js
Normal file
587
src/actions/ethereum/SendFormActions.js
Normal file
@ -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,
|
||||||
|
};
|
@ -13,7 +13,7 @@ import type {
|
|||||||
GetState,
|
GetState,
|
||||||
PayloadAction,
|
PayloadAction,
|
||||||
} from 'flowtype';
|
} from 'flowtype';
|
||||||
import type { State, FeeLevel } from 'reducers/SendFormReducer';
|
import type { State, FeeLevel } from 'reducers/SendFormEthereumReducer';
|
||||||
|
|
||||||
// general regular expressions
|
// general regular expressions
|
||||||
const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$');
|
const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$');
|
||||||
@ -38,7 +38,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct
|
|||||||
// }
|
// }
|
||||||
// const newPrice = getRandomInt(10, 50).toString();
|
// const newPrice = getRandomInt(10, 50).toString();
|
||||||
|
|
||||||
const state = getState().sendForm;
|
const state = getState().sendFormEthereum;
|
||||||
if (network === state.networkSymbol) return;
|
if (network === state.networkSymbol) return;
|
||||||
|
|
||||||
// check if new price is different then currently recommended
|
// check if new price is different then currently recommended
|
||||||
@ -50,6 +50,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct
|
|||||||
// and let him update manually
|
// and let him update manually
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SEND.CHANGE,
|
type: SEND.CHANGE,
|
||||||
|
networkType: 'ethereum',
|
||||||
state: {
|
state: {
|
||||||
...state,
|
...state,
|
||||||
gasPriceNeedsUpdate: true,
|
gasPriceNeedsUpdate: true,
|
||||||
@ -62,6 +63,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct
|
|||||||
const selectedFeeLevel = getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
|
const selectedFeeLevel = getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SEND.CHANGE,
|
type: SEND.CHANGE,
|
||||||
|
networkType: 'ethereum',
|
||||||
state: {
|
state: {
|
||||||
...state,
|
...state,
|
||||||
gasPriceNeedsUpdate: false,
|
gasPriceNeedsUpdate: false,
|
||||||
@ -81,7 +83,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct
|
|||||||
export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
export const validation = (): PayloadAction<State> => (dispatch: Dispatch, getState: GetState): State => {
|
||||||
// clone deep nested object
|
// clone deep nested object
|
||||||
// to avoid overrides across state history
|
// to avoid overrides across state history
|
||||||
let state: State = JSON.parse(JSON.stringify(getState().sendForm));
|
let state: State = JSON.parse(JSON.stringify(getState().sendFormEthereum));
|
||||||
// reset errors
|
// reset errors
|
||||||
state.errors = {};
|
state.errors = {};
|
||||||
state.warnings = {};
|
state.warnings = {};
|
90
src/actions/ripple/BlockchainActions.js
Normal file
90
src/actions/ripple/BlockchainActions.js
Normal file
@ -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,
|
||||||
|
}));
|
||||||
|
};
|
77
src/actions/ripple/DiscoveryActions.js
Normal file
77
src/actions/ripple/DiscoveryActions.js
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
};
|
265
src/actions/ripple/SendFormActions.js
Normal file
265
src/actions/ripple/SendFormActions.js
Normal file
@ -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,
|
||||||
|
};
|
254
src/actions/ripple/SendFormValidationActions.js
Normal file
254
src/actions/ripple/SendFormValidationActions.js
Normal file
@ -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;
|
||||||
|
};
|
BIN
src/components/images/CoinLogo/images/xrp.png
Normal file
BIN
src/components/images/CoinLogo/images/xrp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
@ -11,7 +11,7 @@ import type { State, Dispatch } from 'flowtype';
|
|||||||
|
|
||||||
import Modal from './index';
|
import Modal from './index';
|
||||||
|
|
||||||
type OwnProps = { }
|
type OwnProps = {};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
modal: $ElementType<State, 'modal'>,
|
modal: $ElementType<State, 'modal'>,
|
||||||
@ -19,16 +19,17 @@ type StateProps = {
|
|||||||
devices: $ElementType<State, 'devices'>,
|
devices: $ElementType<State, 'devices'>,
|
||||||
connect: $ElementType<State, 'connect'>,
|
connect: $ElementType<State, 'connect'>,
|
||||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||||
sendForm: $ElementType<State, 'sendForm'>,
|
sendFormEthereum: $ElementType<State, 'sendFormEthereum'>,
|
||||||
|
sendFormRipple: $ElementType<State, 'sendFormRipple'>,
|
||||||
receive: $ElementType<State, 'receive'>,
|
receive: $ElementType<State, 'receive'>,
|
||||||
localStorage: $ElementType<State, 'localStorage'>,
|
localStorage: $ElementType<State, 'localStorage'>,
|
||||||
wallet: $ElementType<State, 'wallet'>,
|
wallet: $ElementType<State, 'wallet'>,
|
||||||
}
|
};
|
||||||
|
|
||||||
type DispatchProps = {
|
type DispatchProps = {
|
||||||
modalActions: typeof ModalActions,
|
modalActions: typeof ModalActions,
|
||||||
receiveActions: typeof ReceiveActions,
|
receiveActions: typeof ReceiveActions,
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Props = StateProps & DispatchProps;
|
export type Props = StateProps & DispatchProps;
|
||||||
|
|
||||||
@ -38,7 +39,8 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
|
|||||||
devices: state.devices,
|
devices: state.devices,
|
||||||
connect: state.connect,
|
connect: state.connect,
|
||||||
selectedAccount: state.selectedAccount,
|
selectedAccount: state.selectedAccount,
|
||||||
sendForm: state.sendForm,
|
sendFormEthereum: state.sendFormEthereum,
|
||||||
|
sendFormRipple: state.sendFormRipple,
|
||||||
receive: state.receive,
|
receive: state.receive,
|
||||||
localStorage: state.localStorage,
|
localStorage: state.localStorage,
|
||||||
wallet: state.wallet,
|
wallet: state.wallet,
|
||||||
|
@ -12,12 +12,11 @@ import P from 'components/Paragraph';
|
|||||||
import Icon from 'components/Icon';
|
import Icon from 'components/Icon';
|
||||||
import { H3 } from 'components/Heading';
|
import { H3 } from 'components/Heading';
|
||||||
|
|
||||||
import type { TrezorDevice } from 'flowtype';
|
import type { TrezorDevice, State } from 'flowtype';
|
||||||
import type { Props as BaseProps } from '../../Container';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
device: TrezorDevice;
|
device: TrezorDevice;
|
||||||
sendForm: $ElementType<BaseProps, 'sendForm'>;
|
sendForm: $ElementType<State, 'sendFormEthereum'> | $ElementType<State, 'sendFormRipple'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
@ -51,10 +50,11 @@ const ConfirmSignTx = (props: Props) => {
|
|||||||
const {
|
const {
|
||||||
amount,
|
amount,
|
||||||
address,
|
address,
|
||||||
currency,
|
|
||||||
selectedFeeLevel,
|
selectedFeeLevel,
|
||||||
} = props.sendForm;
|
} = props.sendForm;
|
||||||
|
|
||||||
|
const currency: string = typeof props.sendForm.currency === 'string' ? props.sendForm.currency : props.sendForm.networkSymbol;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Header>
|
<Header>
|
||||||
|
@ -88,13 +88,22 @@ const getDeviceContextModal = (props: Props) => {
|
|||||||
case 'ButtonRequest_PassphraseType':
|
case 'ButtonRequest_PassphraseType':
|
||||||
return <PassphraseType device={modal.device} />;
|
return <PassphraseType device={modal.device} />;
|
||||||
|
|
||||||
case 'ButtonRequest_SignTx':
|
case 'ButtonRequest_SignTx': {
|
||||||
return <ConfirmSignTx device={modal.device} sendForm={props.sendForm} />;
|
if (!props.selectedAccount.network) return null;
|
||||||
|
switch (props.selectedAccount.network.type) {
|
||||||
|
case 'ethereum':
|
||||||
|
return <ConfirmSignTx device={modal.device} sendForm={props.sendFormEthereum} />;
|
||||||
|
case 'ripple':
|
||||||
|
return <ConfirmSignTx device={modal.device} sendForm={props.sendFormRipple} />;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case 'ButtonRequest_ProtectCall':
|
case 'ButtonRequest_ProtectCall':
|
||||||
return <ConfirmAction />;
|
return <ConfirmAction />;
|
||||||
|
|
||||||
case 'ButtonRequest_Other':
|
case 'ButtonRequest_Other':
|
||||||
|
case 'ButtonRequest_ConfirmOutput':
|
||||||
return <ConfirmAction />;
|
return <ConfirmAction />;
|
||||||
|
|
||||||
case RECEIVE.REQUEST_UNVERIFIED:
|
case RECEIVE.REQUEST_UNVERIFIED:
|
||||||
|
@ -42,8 +42,8 @@ import type {
|
|||||||
DeviceMode,
|
DeviceMode,
|
||||||
DeviceMessageType,
|
DeviceMessageType,
|
||||||
TransportMessageType,
|
TransportMessageType,
|
||||||
BlockchainMessageType,
|
|
||||||
UiMessageType,
|
UiMessageType,
|
||||||
|
BlockchainEvent,
|
||||||
} from 'trezor-connect';
|
} from 'trezor-connect';
|
||||||
|
|
||||||
import type { RouterAction, LocationState } from 'react-router-redux';
|
import type { RouterAction, LocationState } from 'react-router-redux';
|
||||||
@ -111,11 +111,6 @@ type UiEventAction = {
|
|||||||
// },
|
// },
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlockchainEventAction = {
|
|
||||||
type: BlockchainMessageType,
|
|
||||||
payload: any,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: join this message with uiMessage
|
// TODO: join this message with uiMessage
|
||||||
type IFrameHandshake = {
|
type IFrameHandshake = {
|
||||||
type: 'iframe_handshake',
|
type: 'iframe_handshake',
|
||||||
@ -128,7 +123,7 @@ export type Action =
|
|||||||
| TransportEventAction
|
| TransportEventAction
|
||||||
| DeviceEventAction
|
| DeviceEventAction
|
||||||
| UiEventAction
|
| UiEventAction
|
||||||
| BlockchainEventAction
|
| BlockchainEvent
|
||||||
|
|
||||||
| SelectedAccountAction
|
| SelectedAccountAction
|
||||||
| AccountAction
|
| AccountAction
|
||||||
|
@ -20,9 +20,12 @@ export type Account = {
|
|||||||
+addressPath: Array<number>;
|
+addressPath: Array<number>;
|
||||||
+address: string;
|
+address: string;
|
||||||
balance: string;
|
balance: string;
|
||||||
|
availableBalance: string;
|
||||||
|
sequence: number;
|
||||||
nonce: number;
|
nonce: number;
|
||||||
block: number;
|
block: number;
|
||||||
transactions: number;
|
transactions: number;
|
||||||
|
empty: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type State = Array<Account>;
|
export type State = Array<Account>;
|
||||||
|
@ -1,59 +1,94 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import { BLOCKCHAIN } from 'trezor-connect';
|
import { BLOCKCHAIN as BLOCKCHAIN_EVENT } from 'trezor-connect';
|
||||||
|
import * as BLOCKCHAIN_ACTION from 'actions/constants/blockchain';
|
||||||
|
|
||||||
import type { Action } from 'flowtype';
|
import type { Action } from 'flowtype';
|
||||||
|
import type { BlockchainConnect, BlockchainError, BlockchainBlock } from 'trezor-connect';
|
||||||
|
|
||||||
export type BlockchainNetwork = {
|
export type BlockchainNetwork = {
|
||||||
+shortcut: string;
|
+shortcut: string,
|
||||||
connected: boolean;
|
connected: boolean,
|
||||||
}
|
fee: string,
|
||||||
|
block: number,
|
||||||
|
};
|
||||||
|
|
||||||
export type State = Array<BlockchainNetwork>;
|
export type State = Array<BlockchainNetwork>;
|
||||||
|
|
||||||
export const initialState: State = [];
|
export const initialState: State = [];
|
||||||
|
|
||||||
const find = (state: State, shortcut: string): number => state.findIndex(b => b.shortcut === shortcut);
|
const onConnect = (state: State, action: BlockchainConnect): State => {
|
||||||
|
|
||||||
const connect = (state: State, action: any): State => {
|
|
||||||
const shortcut = action.payload.coin.shortcut.toLowerCase();
|
const shortcut = action.payload.coin.shortcut.toLowerCase();
|
||||||
const network: BlockchainNetwork = {
|
const network = state.find(b => b.shortcut === shortcut);
|
||||||
|
const { info } = action.payload;
|
||||||
|
if (network) {
|
||||||
|
const others = state.filter(b => b !== network);
|
||||||
|
return others.concat([{
|
||||||
|
...network,
|
||||||
|
connected: true,
|
||||||
|
fee: info.fee,
|
||||||
|
block: info.block,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.concat([{
|
||||||
shortcut,
|
shortcut,
|
||||||
connected: true,
|
connected: true,
|
||||||
};
|
fee: info.fee,
|
||||||
const newState: State = [...state];
|
block: info.block,
|
||||||
const index: number = find(newState, shortcut);
|
}]);
|
||||||
if (index >= 0) {
|
|
||||||
newState[index] = network;
|
|
||||||
} else {
|
|
||||||
newState.push(network);
|
|
||||||
}
|
|
||||||
return newState;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const disconnect = (state: State, action: any): State => {
|
const onError = (state: State, action: BlockchainError): State => {
|
||||||
const shortcut = action.payload.coin.shortcut.toLowerCase();
|
const shortcut = action.payload.coin.shortcut.toLowerCase();
|
||||||
const network: BlockchainNetwork = {
|
const network = state.find(b => b.shortcut === shortcut);
|
||||||
shortcut,
|
if (network) {
|
||||||
connected: false,
|
const others = state.filter(b => b !== network);
|
||||||
};
|
return others.concat([{
|
||||||
const newState: State = [...state];
|
...network,
|
||||||
const index: number = find(newState, shortcut);
|
connected: false,
|
||||||
if (index >= 0) {
|
}]);
|
||||||
newState[index] = network;
|
|
||||||
} else {
|
|
||||||
newState.push(network);
|
|
||||||
}
|
}
|
||||||
return newState;
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBlock = (state: State, action: BlockchainBlock): State => {
|
||||||
|
const shortcut = action.payload.coin.shortcut.toLowerCase();
|
||||||
|
const network = state.find(b => b.shortcut === shortcut);
|
||||||
|
if (network) {
|
||||||
|
const others = state.filter(b => b !== network);
|
||||||
|
return others.concat([{
|
||||||
|
...network,
|
||||||
|
block: action.payload.block,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFee = (state: State, shortcut: string, fee: string): State => {
|
||||||
|
const network = state.find(b => b.shortcut === shortcut);
|
||||||
|
if (!network) return state;
|
||||||
|
|
||||||
|
const others = state.filter(b => b !== network);
|
||||||
|
return others.concat([{
|
||||||
|
...network,
|
||||||
|
fee,
|
||||||
|
}]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default (state: State = initialState, action: Action): State => {
|
export default (state: State = initialState, action: Action): State => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case BLOCKCHAIN.CONNECT:
|
case BLOCKCHAIN_EVENT.CONNECT:
|
||||||
return connect(state, action);
|
return onConnect(state, action);
|
||||||
case BLOCKCHAIN.ERROR:
|
case BLOCKCHAIN_EVENT.ERROR:
|
||||||
return disconnect(state, action);
|
return onError(state, action);
|
||||||
|
case BLOCKCHAIN_EVENT.BLOCK:
|
||||||
|
return onBlock(state, action);
|
||||||
|
case BLOCKCHAIN_ACTION.UPDATE_FEE:
|
||||||
|
return updateFee(state, action.shortcut, action.fee);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
@ -19,9 +19,6 @@ import type { Account } from './AccountsReducer';
|
|||||||
|
|
||||||
export type Discovery = {
|
export type Discovery = {
|
||||||
network: string;
|
network: string;
|
||||||
publicKey: string;
|
|
||||||
chainCode: string;
|
|
||||||
hdKey: HDKey;
|
|
||||||
basePath: Array<number>;
|
basePath: Array<number>;
|
||||||
deviceState: string;
|
deviceState: string;
|
||||||
accountIndex: number;
|
accountIndex: number;
|
||||||
@ -29,34 +26,57 @@ export type Discovery = {
|
|||||||
completed: boolean;
|
completed: boolean;
|
||||||
waitingForDevice: boolean;
|
waitingForDevice: boolean;
|
||||||
waitingForBlockchain: boolean;
|
waitingForBlockchain: boolean;
|
||||||
}
|
fwNotSupported: boolean;
|
||||||
|
fwOutdated: boolean;
|
||||||
|
|
||||||
|
publicKey: string; // used in ethereum only
|
||||||
|
chainCode: string; // used in ethereum only
|
||||||
|
hdKey: HDKey; // used in ethereum only
|
||||||
|
};
|
||||||
|
|
||||||
export type State = Array<Discovery>;
|
export type State = Array<Discovery>;
|
||||||
const initialState: State = [];
|
const initialState: State = [];
|
||||||
|
const defaultDiscovery: Discovery = {
|
||||||
|
network: '',
|
||||||
|
deviceState: '',
|
||||||
|
basePath: [],
|
||||||
|
accountIndex: 0,
|
||||||
|
interrupted: false,
|
||||||
|
completed: false,
|
||||||
|
waitingForDevice: false,
|
||||||
|
waitingForBlockchain: false,
|
||||||
|
fwNotSupported: false,
|
||||||
|
fwOutdated: false,
|
||||||
|
|
||||||
|
publicKey: '',
|
||||||
|
chainCode: '',
|
||||||
|
hdKey: null,
|
||||||
|
};
|
||||||
|
|
||||||
const findIndex = (state: State, network: string, deviceState: string): number => state.findIndex(d => d.network === network && d.deviceState === deviceState);
|
const findIndex = (state: State, network: string, deviceState: string): number => state.findIndex(d => d.network === network && d.deviceState === deviceState);
|
||||||
|
|
||||||
const start = (state: State, action: DiscoveryStartAction): State => {
|
const start = (state: State, action: DiscoveryStartAction): State => {
|
||||||
const deviceState: string = action.device.state || '0';
|
const deviceState: string = action.device.state || '0';
|
||||||
const hdKey: HDKey = new HDKey();
|
|
||||||
hdKey.publicKey = Buffer.from(action.publicKey, 'hex');
|
|
||||||
hdKey.chainCode = Buffer.from(action.chainCode, 'hex');
|
|
||||||
const instance: Discovery = {
|
const instance: Discovery = {
|
||||||
network: action.network,
|
...defaultDiscovery,
|
||||||
publicKey: action.publicKey,
|
network: action.network.shortcut,
|
||||||
chainCode: action.chainCode,
|
|
||||||
hdKey,
|
|
||||||
basePath: action.basePath,
|
|
||||||
deviceState,
|
deviceState,
|
||||||
accountIndex: 0,
|
|
||||||
interrupted: false,
|
|
||||||
completed: false,
|
|
||||||
waitingForDevice: false,
|
|
||||||
waitingForBlockchain: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (action.networkType === 'ethereum') {
|
||||||
|
const hdKey = new HDKey();
|
||||||
|
hdKey.publicKey = Buffer.from(action.publicKey, 'hex');
|
||||||
|
hdKey.chainCode = Buffer.from(action.chainCode, 'hex');
|
||||||
|
|
||||||
|
instance.hdKey = hdKey;
|
||||||
|
instance.publicKey = action.publicKey;
|
||||||
|
instance.chainCode = action.chainCode;
|
||||||
|
|
||||||
|
instance.basePath = action.basePath;
|
||||||
|
}
|
||||||
|
|
||||||
const newState: State = [...state];
|
const newState: State = [...state];
|
||||||
const index: number = findIndex(state, action.network, deviceState);
|
const index: number = findIndex(state, action.network.shortcut, deviceState);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
newState[index] = instance;
|
newState[index] = instance;
|
||||||
} else {
|
} else {
|
||||||
@ -105,17 +125,10 @@ const stop = (state: State, device: TrezorDevice): State => {
|
|||||||
const waitingForDevice = (state: State, action: DiscoveryWaitingAction): State => {
|
const waitingForDevice = (state: State, action: DiscoveryWaitingAction): State => {
|
||||||
const deviceState: string = action.device.state || '0';
|
const deviceState: string = action.device.state || '0';
|
||||||
const instance: Discovery = {
|
const instance: Discovery = {
|
||||||
|
...defaultDiscovery,
|
||||||
network: action.network,
|
network: action.network,
|
||||||
deviceState,
|
deviceState,
|
||||||
publicKey: '',
|
|
||||||
chainCode: '',
|
|
||||||
hdKey: null,
|
|
||||||
basePath: [],
|
|
||||||
accountIndex: 0,
|
|
||||||
interrupted: false,
|
|
||||||
completed: false,
|
|
||||||
waitingForDevice: true,
|
waitingForDevice: true,
|
||||||
waitingForBlockchain: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const index: number = findIndex(state, action.network, deviceState);
|
const index: number = findIndex(state, action.network, deviceState);
|
||||||
@ -132,16 +145,9 @@ const waitingForDevice = (state: State, action: DiscoveryWaitingAction): State =
|
|||||||
const waitingForBlockchain = (state: State, action: DiscoveryWaitingAction): State => {
|
const waitingForBlockchain = (state: State, action: DiscoveryWaitingAction): State => {
|
||||||
const deviceState: string = action.device.state || '0';
|
const deviceState: string = action.device.state || '0';
|
||||||
const instance: Discovery = {
|
const instance: Discovery = {
|
||||||
|
...defaultDiscovery,
|
||||||
network: action.network,
|
network: action.network,
|
||||||
deviceState,
|
deviceState,
|
||||||
publicKey: '',
|
|
||||||
chainCode: '',
|
|
||||||
hdKey: null,
|
|
||||||
basePath: [],
|
|
||||||
accountIndex: 0,
|
|
||||||
interrupted: false,
|
|
||||||
completed: false,
|
|
||||||
waitingForDevice: false,
|
|
||||||
waitingForBlockchain: true,
|
waitingForBlockchain: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -156,6 +162,19 @@ const waitingForBlockchain = (state: State, action: DiscoveryWaitingAction): Sta
|
|||||||
return newState;
|
return newState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const notSupported = (state: State, action: DiscoveryWaitingAction): State => {
|
||||||
|
const affectedProcesses = state.filter(d => d.deviceState === action.device.state && d.network === action.network);
|
||||||
|
const otherProcesses = state.filter(d => affectedProcesses.indexOf(d) === -1);
|
||||||
|
|
||||||
|
const changedProcesses = affectedProcesses.map(d => ({
|
||||||
|
...d,
|
||||||
|
fwOutdated: action.type === DISCOVERY.FIRMWARE_OUTDATED,
|
||||||
|
fwNotSupported: action.type === DISCOVERY.FIRMWARE_NOT_SUPPORTED,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return otherProcesses.concat(changedProcesses);
|
||||||
|
};
|
||||||
|
|
||||||
export default function discovery(state: State = initialState, action: Action): State {
|
export default function discovery(state: State = initialState, action: Action): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case DISCOVERY.START:
|
case DISCOVERY.START:
|
||||||
@ -170,6 +189,10 @@ export default function discovery(state: State = initialState, action: Action):
|
|||||||
return waitingForDevice(state, action);
|
return waitingForDevice(state, action);
|
||||||
case DISCOVERY.WAITING_FOR_BLOCKCHAIN:
|
case DISCOVERY.WAITING_FOR_BLOCKCHAIN:
|
||||||
return waitingForBlockchain(state, action);
|
return waitingForBlockchain(state, action);
|
||||||
|
case DISCOVERY.FIRMWARE_NOT_SUPPORTED:
|
||||||
|
return notSupported(state, action);
|
||||||
|
case DISCOVERY.FIRMWARE_OUTDATED:
|
||||||
|
return notSupported(state, action);
|
||||||
case DISCOVERY.FROM_STORAGE:
|
case DISCOVERY.FROM_STORAGE:
|
||||||
return action.payload.map((d) => {
|
return action.payload.map((d) => {
|
||||||
const hdKey: HDKey = new HDKey();
|
const hdKey: HDKey = new HDKey();
|
||||||
|
@ -6,7 +6,9 @@ import * as STORAGE from 'actions/constants/localStorage';
|
|||||||
import type { Action } from 'flowtype';
|
import type { Action } from 'flowtype';
|
||||||
|
|
||||||
export type Network = {
|
export type Network = {
|
||||||
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
testnet?: boolean;
|
||||||
shortcut: string;
|
shortcut: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
bip44: string;
|
bip44: string;
|
||||||
|
@ -1,62 +1,39 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
import * as CONNECT from 'actions/constants/TrezorConnect';
|
import * as CONNECT from 'actions/constants/TrezorConnect';
|
||||||
import * as PENDING from 'actions/constants/pendingTx';
|
import * as PENDING from 'actions/constants/pendingTx';
|
||||||
import * as SEND from 'actions/constants/send';
|
|
||||||
|
|
||||||
import type { TrezorDevice, Action } from 'flowtype';
|
import type { Action } from 'flowtype';
|
||||||
import type { SendTxAction } from 'actions/SendFormActions';
|
|
||||||
|
|
||||||
export type PendingTx = {
|
export type PendingTx = {
|
||||||
+type: 'send' | 'receive';
|
+type: 'send' | 'recv',
|
||||||
+id: string;
|
+deviceState: string,
|
||||||
+network: string;
|
+sequence: number,
|
||||||
+address: string;
|
+hash: string,
|
||||||
+deviceState: string;
|
+network: string,
|
||||||
+currency: string;
|
+address: string,
|
||||||
+amount: string;
|
+currency: string,
|
||||||
+total: string;
|
+amount: string,
|
||||||
+tx: any;
|
+total: string,
|
||||||
+nonce: number;
|
+fee: string,
|
||||||
rejected: boolean;
|
rejected?: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
export type State = Array<PendingTx>;
|
export type State = Array<PendingTx>;
|
||||||
|
|
||||||
const initialState: State = [];
|
const initialState: State = [];
|
||||||
|
|
||||||
const add = (state: State, action: SendTxAction): State => {
|
const add = (state: State, payload: PendingTx): State => {
|
||||||
const newState = [...state];
|
|
||||||
newState.push({
|
|
||||||
type: 'send',
|
|
||||||
id: action.txid,
|
|
||||||
network: action.account.network,
|
|
||||||
address: action.account.address,
|
|
||||||
deviceState: action.account.deviceState,
|
|
||||||
|
|
||||||
currency: action.selectedCurrency,
|
|
||||||
amount: action.amount,
|
|
||||||
total: action.total,
|
|
||||||
tx: action.tx,
|
|
||||||
nonce: action.nonce,
|
|
||||||
rejected: false,
|
|
||||||
});
|
|
||||||
return newState;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
const addFromBloockbokNotifiaction = (state: State, payload: any): State => {
|
|
||||||
const newState = [...state];
|
const newState = [...state];
|
||||||
newState.push(payload);
|
newState.push(payload);
|
||||||
return newState;
|
return newState;
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
const clear = (state: State, device: TrezorDevice): State => state.filter(tx => tx.deviceState !== device.state);
|
const removeByDeviceState = (state: State, deviceState: ?string): State => state.filter(tx => tx.deviceState !== deviceState);
|
||||||
|
|
||||||
const remove = (state: State, id: string): State => state.filter(tx => tx.id !== id);
|
const removeByHash = (state: State, hash: string): State => state.filter(tx => tx.hash !== hash);
|
||||||
|
|
||||||
const reject = (state: State, id: string): State => state.map((tx) => {
|
const reject = (state: State, hash: string): State => state.map((tx) => {
|
||||||
if (tx.id === id && !tx.rejected) {
|
if (tx.hash === hash && !tx.rejected) {
|
||||||
return { ...tx, rejected: true };
|
return { ...tx, rejected: true };
|
||||||
}
|
}
|
||||||
return tx;
|
return tx;
|
||||||
@ -64,21 +41,18 @@ const reject = (state: State, id: string): State => state.map((tx) => {
|
|||||||
|
|
||||||
export default function pending(state: State = initialState, action: Action): State {
|
export default function pending(state: State = initialState, action: Action): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SEND.TX_COMPLETE:
|
|
||||||
return add(state, action);
|
|
||||||
|
|
||||||
case CONNECT.FORGET:
|
case CONNECT.FORGET:
|
||||||
case CONNECT.FORGET_SINGLE:
|
case CONNECT.FORGET_SINGLE:
|
||||||
case CONNECT.FORGET_SILENT:
|
case CONNECT.FORGET_SILENT:
|
||||||
case CONNECT.RECEIVE_WALLET_TYPE:
|
case CONNECT.RECEIVE_WALLET_TYPE:
|
||||||
return clear(state, action.device);
|
return removeByDeviceState(state, action.device.state);
|
||||||
|
|
||||||
// case PENDING.ADD:
|
case PENDING.ADD:
|
||||||
// return add(state, action.payload);
|
return add(state, action.payload);
|
||||||
case PENDING.TX_RESOLVED:
|
case PENDING.TX_RESOLVED:
|
||||||
return remove(state, action.tx.id);
|
return removeByHash(state, action.hash);
|
||||||
case PENDING.TX_REJECTED:
|
case PENDING.TX_REJECTED:
|
||||||
return reject(state, action.tx.id);
|
return reject(state, action.hash);
|
||||||
|
|
||||||
case PENDING.FROM_STORAGE:
|
case PENDING.FROM_STORAGE:
|
||||||
return action.payload;
|
return action.payload;
|
||||||
|
@ -75,16 +75,15 @@ export const initialState: State = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default (state: State = initialState, action: Action): State => {
|
export default (state: State = initialState, action: Action): State => {
|
||||||
|
if (action.type === ACCOUNT.DISPOSE) return initialState;
|
||||||
|
if (!action.networkType || action.networkType !== 'ethereum') return state;
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SEND.INIT:
|
case SEND.INIT:
|
||||||
case SEND.CHANGE:
|
case SEND.CHANGE:
|
||||||
case SEND.VALIDATION:
|
case SEND.VALIDATION:
|
||||||
return action.state;
|
return action.state;
|
||||||
|
|
||||||
case ACCOUNT.DISPOSE:
|
|
||||||
return initialState;
|
|
||||||
|
|
||||||
|
|
||||||
case SEND.TOGGLE_ADVANCED:
|
case SEND.TOGGLE_ADVANCED:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
99
src/reducers/SendFormRippleReducer.js
Normal file
99
src/reducers/SendFormRippleReducer.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
};
|
@ -10,7 +10,8 @@ import modal from 'reducers/ModalReducer';
|
|||||||
import web3 from 'reducers/Web3Reducer';
|
import web3 from 'reducers/Web3Reducer';
|
||||||
import accounts from 'reducers/AccountsReducer';
|
import accounts from 'reducers/AccountsReducer';
|
||||||
import selectedAccount from 'reducers/SelectedAccountReducer';
|
import selectedAccount from 'reducers/SelectedAccountReducer';
|
||||||
import sendForm from 'reducers/SendFormReducer';
|
import sendFormEthereum from 'reducers/SendFormEthereumReducer';
|
||||||
|
import sendFormRipple from 'reducers/SendFormRippleReducer';
|
||||||
import receive from 'reducers/ReceiveReducer';
|
import receive from 'reducers/ReceiveReducer';
|
||||||
import summary from 'reducers/SummaryReducer';
|
import summary from 'reducers/SummaryReducer';
|
||||||
import tokens from 'reducers/TokensReducer';
|
import tokens from 'reducers/TokensReducer';
|
||||||
@ -32,7 +33,8 @@ const reducers = {
|
|||||||
web3,
|
web3,
|
||||||
accounts,
|
accounts,
|
||||||
selectedAccount,
|
selectedAccount,
|
||||||
sendForm,
|
sendFormEthereum,
|
||||||
|
sendFormRipple,
|
||||||
receive,
|
receive,
|
||||||
summary,
|
summary,
|
||||||
tokens,
|
tokens,
|
||||||
|
@ -89,9 +89,9 @@ export const getAccountPendingTx = (pending: Array<PendingTx>, account: ?Account
|
|||||||
return pending.filter(p => p.network === a.network && p.address === a.address);
|
return pending.filter(p => p.network === a.network && p.address === a.address);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPendingNonce = (pending: Array<PendingTx>): number => pending.reduce((value: number, tx: PendingTx) => {
|
export const getPendingSequence = (pending: Array<PendingTx>): number => pending.reduce((value: number, tx: PendingTx) => {
|
||||||
if (tx.rejected) return value;
|
if (tx.rejected) return value;
|
||||||
return Math.max(value, tx.nonce + 1);
|
return Math.max(value, tx.sequence + 1);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
export const getPendingAmount = (pending: Array<PendingTx>, currency: string, token: boolean = false): BigNumber => pending.reduce((value: BigNumber, tx: PendingTx) => {
|
export const getPendingAmount = (pending: Array<PendingTx>, currency: string, token: boolean = false): BigNumber => pending.reduce((value: BigNumber, tx: PendingTx) => {
|
||||||
|
@ -6,7 +6,6 @@ import * as CONNECT from 'actions/constants/TrezorConnect';
|
|||||||
import * as TOKEN from 'actions/constants/token';
|
import * as TOKEN from 'actions/constants/token';
|
||||||
import * as ACCOUNT from 'actions/constants/account';
|
import * as ACCOUNT from 'actions/constants/account';
|
||||||
import * as DISCOVERY from 'actions/constants/discovery';
|
import * as DISCOVERY from 'actions/constants/discovery';
|
||||||
import * as SEND from 'actions/constants/send';
|
|
||||||
import * as PENDING from 'actions/constants/pendingTx';
|
import * as PENDING from 'actions/constants/pendingTx';
|
||||||
import * as WALLET from 'actions/constants/wallet';
|
import * as WALLET from 'actions/constants/wallet';
|
||||||
|
|
||||||
@ -59,7 +58,7 @@ const LocalStorageService: Middleware = (api: MiddlewareAPI) => (next: Middlewar
|
|||||||
api.dispatch(LocalStorageActions.save());
|
api.dispatch(LocalStorageActions.save());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SEND.TX_COMPLETE:
|
case PENDING.ADD:
|
||||||
case PENDING.TX_RESOLVED:
|
case PENDING.TX_RESOLVED:
|
||||||
case PENDING.TX_REJECTED:
|
case PENDING.TX_REJECTED:
|
||||||
api.dispatch(LocalStorageActions.save());
|
api.dispatch(LocalStorageActions.save());
|
||||||
|
@ -58,11 +58,11 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
|
|||||||
} else if (action.type === CONNECT.DUPLICATE) {
|
} else if (action.type === CONNECT.DUPLICATE) {
|
||||||
api.dispatch(RouterActions.selectDevice(action.device));
|
api.dispatch(RouterActions.selectDevice(action.device));
|
||||||
} else if (action.type === BLOCKCHAIN.BLOCK) {
|
} else if (action.type === BLOCKCHAIN.BLOCK) {
|
||||||
api.dispatch(BlockchainActions.onBlockMined(action.payload.coin));
|
api.dispatch(BlockchainActions.onBlockMined(action.payload));
|
||||||
} else if (action.type === BLOCKCHAIN.NOTIFICATION) {
|
} else if (action.type === BLOCKCHAIN.NOTIFICATION) {
|
||||||
// api.dispatch(BlockchainActions.onNotification(action.payload));
|
api.dispatch(BlockchainActions.onNotification(action.payload));
|
||||||
} else if (action.type === BLOCKCHAIN.ERROR) {
|
} else if (action.type === BLOCKCHAIN.ERROR) {
|
||||||
api.dispatch(BlockchainActions.error(action.payload));
|
api.dispatch(BlockchainActions.onError(action.payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
|
@ -9,7 +9,7 @@ import * as NotificationActions from 'actions/NotificationActions';
|
|||||||
import * as LocalStorageActions from 'actions/LocalStorageActions';
|
import * as LocalStorageActions from 'actions/LocalStorageActions';
|
||||||
import * as TrezorConnectActions from 'actions/TrezorConnectActions';
|
import * as TrezorConnectActions from 'actions/TrezorConnectActions';
|
||||||
import * as SelectedAccountActions from 'actions/SelectedAccountActions';
|
import * as SelectedAccountActions from 'actions/SelectedAccountActions';
|
||||||
import * as SendFormActionActions from 'actions/SendFormActions';
|
import * as SendFormActions from 'actions/SendFormActions';
|
||||||
import * as DiscoveryActions from 'actions/DiscoveryActions';
|
import * as DiscoveryActions from 'actions/DiscoveryActions';
|
||||||
import * as RouterActions from 'actions/RouterActions';
|
import * as RouterActions from 'actions/RouterActions';
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
|
|||||||
// if "selectedDevice" didn't change observe common values in SelectedAccountReducer
|
// if "selectedDevice" didn't change observe common values in SelectedAccountReducer
|
||||||
if (!await api.dispatch(SelectedAccountActions.observe(prevState, action))) {
|
if (!await api.dispatch(SelectedAccountActions.observe(prevState, action))) {
|
||||||
// if "selectedAccount" didn't change observe send form props changes
|
// if "selectedAccount" didn't change observe send form props changes
|
||||||
api.dispatch(SendFormActionActions.observe(prevState, action));
|
api.dispatch(SendFormActions.observe(prevState, action));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// no changes in common values
|
// no changes in common values
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
// TODO: chagne currency units
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
const currencyUnitsConstant: string = 'mbtc2';
|
const currencyUnitsConstant: string = 'mbtc2';
|
||||||
|
|
||||||
export const formatAmount = (n: number, coinInfo: any, currencyUnits: string = currencyUnitsConstant): string => {
|
export const formatAmount = (n: number, coinInfo: any, currencyUnits: string = currencyUnitsConstant): string => {
|
||||||
@ -52,3 +53,7 @@ export const hexToString = (hex: string): string => {
|
|||||||
}
|
}
|
||||||
return str;
|
return str;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toDecimalAmount = (amount: string, decimals: number): string => new BigNumber(amount).div(10 ** decimals).toString(10);
|
||||||
|
|
||||||
|
export const fromDecimalAmount = (amount: string, decimals: number): string => new BigNumber(amount).times(10 ** decimals).toString(10);
|
||||||
|
@ -229,6 +229,10 @@ const AccountMenu = (props: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (discovery && (discovery.fwNotSupported || discovery.fwOutdated)) {
|
||||||
|
discoveryStatus = null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<NavLink to={baseUrl}>
|
<NavLink to={baseUrl}>
|
||||||
|
183
src/views/Wallet/views/Account/Receive/ethereum/index.js
Normal file
183
src/views/Wallet/views/Account/Receive/ethereum/index.js
Normal file
@ -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 */
|
/* @flow */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { QRCode } from 'react-qr-svg';
|
import { connect } from 'react-redux';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import Title from 'views/Wallet/components/Title';
|
import type { State } from 'flowtype';
|
||||||
import Button from 'components/Button';
|
import EthereumTypeReceiveForm from './ethereum/Container';
|
||||||
import Icon from 'components/Icon';
|
import RippleTypeReceiveForm from './ripple/Container';
|
||||||
import Tooltip from 'components/Tooltip';
|
|
||||||
import Input from 'components/inputs/Input';
|
|
||||||
|
|
||||||
import ICONS from 'config/icons';
|
export type BaseProps = {
|
||||||
import colors from 'config/colors';
|
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||||
import { CONTEXT_DEVICE } from 'actions/constants/modal';
|
}
|
||||||
|
|
||||||
import Content from 'views/Wallet/components/Content';
|
// return container for requested network type
|
||||||
import VerifyAddressTooltip from './components/VerifyAddressTooltip';
|
export default connect((state: State): BaseProps => ({
|
||||||
|
selectedAccount: state.selectedAccount,
|
||||||
|
}), null)((props) => {
|
||||||
|
const { network } = props.selectedAccount;
|
||||||
|
if (!network) return null;
|
||||||
|
|
||||||
import type { Props } from './Container';
|
switch (network.type) {
|
||||||
|
case 'ethereum':
|
||||||
const Label = styled.div`
|
return <EthereumTypeReceiveForm />;
|
||||||
padding-bottom: 10px;
|
case 'ripple':
|
||||||
color: ${colors.TEXT_SECONDARY};
|
return <RippleTypeReceiveForm />;
|
||||||
`;
|
default:
|
||||||
|
return null;
|
||||||
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;
|
|
36
src/views/Wallet/views/Account/Receive/ripple/Container.js
Normal file
36
src/views/Wallet/views/Account/Receive/ripple/Container.js
Normal file
@ -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);
|
183
src/views/Wallet/views/Account/Receive/ripple/index.js
Normal file
183
src/views/Wallet/views/Account/Receive/ripple/index.js
Normal file
@ -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;
|
@ -11,7 +11,7 @@ import Icon from 'components/Icon';
|
|||||||
import ICONS from 'config/icons';
|
import ICONS from 'config/icons';
|
||||||
import { FONT_SIZE } from 'config/variables';
|
import { FONT_SIZE } from 'config/variables';
|
||||||
|
|
||||||
import type { Props as BaseProps } from '../../Container';
|
import type { Props as BaseProps } from '../../ethereum/Container';
|
||||||
|
|
||||||
type Props = BaseProps & {
|
type Props = BaseProps & {
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
|
@ -9,7 +9,7 @@ import ScaleText from 'react-scale-text';
|
|||||||
|
|
||||||
import type { Network } from 'reducers/LocalStorageReducer';
|
import type { Network } from 'reducers/LocalStorageReducer';
|
||||||
import type { Token } from 'reducers/TokensReducer';
|
import type { Token } from 'reducers/TokensReducer';
|
||||||
import type { Props as BaseProps } from '../../Container';
|
import type { BaseProps } from '../../index';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pending: $PropertyType<$ElementType<BaseProps, 'selectedAccount'>, 'pending'>,
|
pending: $PropertyType<$ElementType<BaseProps, 'selectedAccount'>, 'pending'>,
|
||||||
@ -140,7 +140,7 @@ class PendingTransactions extends PureComponent<Props> {
|
|||||||
<H2>Pending transactions</H2>
|
<H2>Pending transactions</H2>
|
||||||
{this.getPendingTransactions().map(tx => (
|
{this.getPendingTransactions().map(tx => (
|
||||||
<TransactionWrapper
|
<TransactionWrapper
|
||||||
key={tx.id}
|
key={tx.hash}
|
||||||
>
|
>
|
||||||
<TransactionIcon
|
<TransactionIcon
|
||||||
textColor={() => this.getTransactionIconColors(tx).textColor}
|
textColor={() => this.getTransactionIconColors(tx).textColor}
|
||||||
@ -154,7 +154,7 @@ class PendingTransactions extends PureComponent<Props> {
|
|||||||
|
|
||||||
<TransactionName>
|
<TransactionName>
|
||||||
<StyledLink
|
<StyledLink
|
||||||
href={`${this.props.network.explorer.tx}${tx.id}`}
|
href={`${this.props.network.explorer.tx}${tx.hash}`}
|
||||||
isGray
|
isGray
|
||||||
>
|
>
|
||||||
{this.getTransactionName(tx)}
|
{this.getTransactionName(tx)}
|
||||||
|
39
src/views/Wallet/views/Account/Send/ethereum/Container.js
Normal file
39
src/views/Wallet/views/Account/Send/ethereum/Container.js
Normal file
@ -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);
|
403
src/views/Wallet/views/Account/Send/ethereum/index.js
Normal file
403
src/views/Wallet/views/Account/Send/ethereum/index.js
Normal file
@ -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 */
|
/* @flow */
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled, { css } from 'styled-components';
|
import { connect } from 'react-redux';
|
||||||
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';
|
import type { State } from 'flowtype';
|
||||||
|
import EthereumTypeSendForm from './ethereum/Container';
|
||||||
|
import RippleTypeSendForm from './ripple/Container';
|
||||||
|
|
||||||
// TODO: Decide on a small screen width for the whole app
|
export type BaseProps = {
|
||||||
// and put it inside config/variables.js
|
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||||
const SmallScreenWidth = '850px';
|
}
|
||||||
|
|
||||||
const AmountInputLabelWrapper = styled.div`
|
// return container for requested network type
|
||||||
display: flex;
|
export default connect((state: State): BaseProps => ({
|
||||||
justify-content: space-between;
|
selectedAccount: state.selectedAccount,
|
||||||
`;
|
}), null)((props) => {
|
||||||
|
const { network } = props.selectedAccount;
|
||||||
|
if (!network) return null;
|
||||||
|
|
||||||
const AmountInputLabel = styled.span`
|
switch (network.type) {
|
||||||
text-align: right;
|
case 'ethereum':
|
||||||
color: ${colors.TEXT_SECONDARY};
|
return <EthereumTypeSendForm />;
|
||||||
`;
|
case 'ripple':
|
||||||
|
return <RippleTypeSendForm />;
|
||||||
const InputRow = styled.div`
|
default:
|
||||||
padding-bottom: 28px;
|
return null;
|
||||||
`;
|
|
||||||
|
|
||||||
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;
|
|
@ -3,7 +3,7 @@
|
|||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import SendFormActions from 'actions/SendFormActions';
|
import SendFormActions from 'actions/ripple/SendFormActions';
|
||||||
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
|
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
|
||||||
import type { State, Dispatch } from 'flowtype';
|
import type { State, Dispatch } from 'flowtype';
|
||||||
import AccountSend from './index';
|
import AccountSend from './index';
|
||||||
@ -12,7 +12,7 @@ type OwnProps = {}
|
|||||||
|
|
||||||
export type StateProps = {
|
export type StateProps = {
|
||||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||||
sendForm: $ElementType<State, 'sendForm'>,
|
sendForm: $ElementType<State, 'sendFormRipple'>,
|
||||||
wallet: $ElementType<State, 'wallet'>,
|
wallet: $ElementType<State, 'wallet'>,
|
||||||
fiat: $ElementType<State, 'fiat'>,
|
fiat: $ElementType<State, 'fiat'>,
|
||||||
localStorage: $ElementType<State, 'localStorage'>,
|
localStorage: $ElementType<State, 'localStorage'>,
|
||||||
@ -26,7 +26,7 @@ export type Props = StateProps & DispatchProps;
|
|||||||
|
|
||||||
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
|
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
|
||||||
selectedAccount: state.selectedAccount,
|
selectedAccount: state.selectedAccount,
|
||||||
sendForm: state.sendForm,
|
sendForm: state.sendFormRipple,
|
||||||
wallet: state.wallet,
|
wallet: state.wallet,
|
||||||
fiat: state.fiat,
|
fiat: state.fiat,
|
||||||
localStorage: state.localStorage,
|
localStorage: state.localStorage,
|
255
src/views/Wallet/views/Account/Send/ripple/index.js
Normal file
255
src/views/Wallet/views/Account/Send/ripple/index.js
Normal file
@ -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;
|
@ -75,7 +75,7 @@ class SignVerify extends Component <Props> {
|
|||||||
const verifyAddressError = this.getError('verifyAddress');
|
const verifyAddressError = this.getError('verifyAddress');
|
||||||
return (
|
return (
|
||||||
<Content>
|
<Content>
|
||||||
<Title>Sign & Verify</Title>
|
<Title>Sign & Verify</Title>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Sign>
|
<Sign>
|
||||||
<Row>
|
<Row>
|
||||||
|
@ -7,13 +7,12 @@ import colors from 'config/colors';
|
|||||||
import ICONS from 'config/icons';
|
import ICONS from 'config/icons';
|
||||||
import { FONT_SIZE, FONT_WEIGHT } from 'config/variables';
|
import { FONT_SIZE, FONT_WEIGHT } from 'config/variables';
|
||||||
|
|
||||||
import type { Network } from 'flowtype';
|
import type { Network, State as ReducersState } from 'flowtype';
|
||||||
import type { Props as BaseProps } from '../../Container';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
network: Network,
|
network: Network,
|
||||||
balance: string,
|
balance: string,
|
||||||
fiat: $ElementType<BaseProps, 'fiat'>,
|
fiat: $ElementType<ReducersState, 'fiat'>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
165
src/views/Wallet/views/Account/Summary/ethereum/index.js
Normal file
165
src/views/Wallet/views/Account/Summary/ethereum/index.js
Normal file
@ -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 */
|
/* @flow */
|
||||||
import styled from 'styled-components';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { H2 } from 'components/Heading';
|
import { connect } from 'react-redux';
|
||||||
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 type { State } from 'flowtype';
|
||||||
import * as stateUtils from 'reducers/utils';
|
import EthereumTypeSummary from './ethereum/Container';
|
||||||
import Link from 'components/Link';
|
import RippleTypeSummary from './ripple/Container';
|
||||||
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';
|
type WrapperProps = {
|
||||||
|
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||||
|
}
|
||||||
|
|
||||||
const AccountHeading = styled.div`
|
// return container for requested network type
|
||||||
padding-bottom: 35px;
|
export default connect((state: State): WrapperProps => ({
|
||||||
display: flex;
|
selectedAccount: state.selectedAccount,
|
||||||
justify-content: space-between;
|
}), null)((props) => {
|
||||||
align-items: center;
|
const { network } = props.selectedAccount;
|
||||||
`;
|
if (!network) return null;
|
||||||
|
|
||||||
const H2Wrapper = styled.div`
|
switch (network.type) {
|
||||||
display: flex;
|
case 'ethereum':
|
||||||
align-items: center;
|
return <EthereumTypeSummary />;
|
||||||
padding: 20px 0;
|
case 'ripple':
|
||||||
`;
|
return <RippleTypeSummary />;
|
||||||
|
default:
|
||||||
const StyledTooltip = styled(Tooltip)`
|
return null;
|
||||||
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;
|
|
||||||
|
45
src/views/Wallet/views/Account/Summary/ripple/Container.js
Normal file
45
src/views/Wallet/views/Account/Summary/ripple/Container.js
Normal file
@ -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);
|
115
src/views/Wallet/views/Account/Summary/ripple/index.js
Normal file
115
src/views/Wallet/views/Account/Summary/ripple/index.js
Normal file
@ -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;
|
@ -16,9 +16,9 @@ import ImportView from 'views/Landing/views/Import/Container';
|
|||||||
|
|
||||||
// wallet views
|
// wallet views
|
||||||
import WalletContainer from 'views/Wallet';
|
import WalletContainer from 'views/Wallet';
|
||||||
import AccountSummary from 'views/Wallet/views/Account/Summary/Container';
|
import AccountSummary from 'views/Wallet/views/Account/Summary';
|
||||||
import AccountSend from 'views/Wallet/views/Account/Send/Container';
|
import AccountSend from 'views/Wallet/views/Account/Send';
|
||||||
import AccountReceive from 'views/Wallet/views/Account/Receive/Container';
|
import AccountReceive from 'views/Wallet/views/Account/Receive';
|
||||||
import AccountSignVerify from 'views/Wallet/views/Account/SignVerify/Container';
|
import AccountSignVerify from 'views/Wallet/views/Account/SignVerify/Container';
|
||||||
|
|
||||||
import WalletDashboard from 'views/Wallet/views/Dashboard';
|
import WalletDashboard from 'views/Wallet/views/Dashboard';
|
||||||
|
@ -50,7 +50,7 @@ module.exports = {
|
|||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.js?$/,
|
test: /\.js?$/,
|
||||||
exclude: /node_modules/,
|
exclude: [/node_modules/, /trezor-blockchain-link\/build\/workers/],
|
||||||
use: ['babel-loader'],
|
use: ['babel-loader'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -11896,10 +11896,9 @@ tr46@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
punycode "^2.1.0"
|
||||||
|
|
||||||
trezor-connect@6.0.2:
|
trezor-connect@6.0.3-beta.4:
|
||||||
version "6.0.2"
|
version "6.0.3-beta.4"
|
||||||
resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-6.0.2.tgz#a4ca892cc4a167b34b97644e1404a56f6a110379"
|
resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-6.0.3-beta.4.tgz#c0b3dfe809756b455e5b5b34fb2a104846da4f72"
|
||||||
integrity sha512-oSYTPnQD9ZT3vO2hBRdgealHG7t8Wu3oTZXX/U4eRxJZ0WqdEcXG7Nvqe1BL6Rl3VTuj7ALT9DL1Uq3QFYAc3g==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-runtime "^6.26.0"
|
babel-runtime "^6.26.0"
|
||||||
events "^1.1.1"
|
events "^1.1.1"
|
||||||
|
Loading…
Reference in New Issue
Block a user