merge master
@ -79,6 +79,8 @@ deploy review:
|
||||
- mkdir -p "${DEPLOY_BASE_DIR}/${CI_BUILD_REF_NAME}"
|
||||
- echo "Copy dev build to web server ${DEPLOY_BASE_DIR}/${CI_BUILD_REF_NAME}..."
|
||||
- rsync --delete -va build/dev/ "${DEPLOY_BASE_DIR}/${CI_BUILD_REF_NAME}/"
|
||||
- curl "https://api.telegram.org/bot699197118:AAGXNTaC5Q-ljmy_dMvaIvAKy1XjlkA3Iss/sendMessage?chat_id=-1001354778014&text=https://trezor-wallet-dev.trezor.io/${CI_BUILD_REF_NAME}"
|
||||
- 'echo "Remove working dir, workaround for cache" && rm -r ./*'
|
||||
only:
|
||||
- branches
|
||||
tags:
|
||||
|
@ -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
|
||||
__changed__
|
||||
- Fiat rates from coingecko (https://github.com/trezor/trezor-wallet/pull/242)
|
||||
|
@ -28,14 +28,14 @@ At the root of the `src/` folder are all files or folders that are shared.
|
||||
Component is what you'd intuitively think it is. It's a regular React component (doesn't matter whether statefull or stateless).
|
||||
|
||||
### **Global components**
|
||||
All global components are are stored in `src/views/components/` folder.
|
||||
All global components are stored in `src/views/components/` folder.
|
||||
Global components are such components that are shared across multiple different components or views.
|
||||
- For example there's a `Button` component that is used in both `ConnectDevice` and `AccountSend`. `ConnectDevice` and `AccountSend` are both placed accross different views so the `Button` component they're both using must be stored in the global `components` folder.
|
||||
|
||||
### **Naming & structure convention**
|
||||
Each component has it's own folder. Name of the folder is same as is the name of the component (camel case and first letter is capitalized, e.g.: *MyComponent*).
|
||||
|
||||
If you want to create multiple components of the same type you should put them into a common folder with a lowercase name like this `views/componets/type/MyComponent`.
|
||||
If you want to create multiple components of the same type you should put them into a common folder with a lowercase name like this `views/components/type/MyComponent`.
|
||||
|
||||
- For example there are different types of modals like `confirm` or `device`.
|
||||
Because the `confirm` and `device` modals are subtypes of modal the folder structure looks like this
|
||||
@ -64,7 +64,7 @@ Both naming and structure conventions are similar to components conventions.
|
||||
Each view has its own folder in `views/` folder. Name of this folder is same as is the view's name (camel case and first letter is capitalized, e.g.: *MyView*).
|
||||
Inside the view's folder is always an `index.js` file containing view's code itself.
|
||||
|
||||
View may contain own components inside view's folder - in the `components/` folder. One of the differences between a component and a view is that view can hav another views. Of course those views may have their own components and views, etc.
|
||||
View may contain own components inside view's folder - in the `components/` folder. One of the differences between a component and a view is that view can have another views. Of course those views may have their own components and views, etc.
|
||||
|
||||
```
|
||||
views/
|
||||
|
@ -68,8 +68,9 @@
|
||||
"redux-thunk": "^2.2.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"styled-components": "^4.1.2",
|
||||
"styled-media-query": "^2.0.2",
|
||||
"styled-normalize": "^8.0.4",
|
||||
"trezor-connect": "6.0.2",
|
||||
"trezor-connect": "6.0.3-beta.4",
|
||||
"web3": "1.0.0-beta.35",
|
||||
"webpack": "^4.16.3",
|
||||
"webpack-build-notifier": "^0.1.29",
|
||||
|
@ -1,6 +1,19 @@
|
||||
{
|
||||
"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",
|
||||
"symbol": "ETH",
|
||||
"shortcut": "eth",
|
||||
@ -19,6 +32,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ethereum",
|
||||
"name": "Ethereum Classic",
|
||||
"symbol": "ETC",
|
||||
"shortcut": "etc",
|
||||
@ -37,7 +51,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ethereum",
|
||||
"name": "Ethereum Ropsten",
|
||||
"testnet": true,
|
||||
"symbol": "tROP",
|
||||
"shortcut": "trop",
|
||||
"chainId": 3,
|
||||
@ -64,6 +80,10 @@
|
||||
{
|
||||
"network": "etc",
|
||||
"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 */
|
||||
|
||||
import TrezorConnect from 'trezor-connect';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as BLOCKCHAIN from 'actions/constants/blockchain';
|
||||
import * as EthereumBlockchainActions from 'actions/ethereum/BlockchainActions';
|
||||
import * as RippleBlockchainActions from 'actions/ripple/BlockchainActions';
|
||||
|
||||
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 './Web3Actions';
|
||||
import * as AccountsActions from './AccountsActions';
|
||||
import type { BlockchainBlock, BlockchainNotification, BlockchainError } from 'trezor-connect';
|
||||
|
||||
|
||||
export type BlockchainAction = {
|
||||
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
|
||||
// called after TrezorConnect.init successfully emits TRANSPORT.START event
|
||||
// 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
|
||||
// disconnect and remove Web3 webscocket instance if exists
|
||||
export const error = (payload: any): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
|
||||
dispatch(Web3Actions.disconnect(payload.coin));
|
||||
export const onError = (payload: $ElementType<BlockchainError, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const shortcut = payload.coin.shortcut.toLowerCase();
|
||||
const { config } = getState().localStorage;
|
||||
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 */
|
||||
|
||||
import TrezorConnect from 'trezor-connect';
|
||||
import EthereumjsUtil from 'ethereumjs-util';
|
||||
import TrezorConnect, { UI } from 'trezor-connect';
|
||||
import * as DISCOVERY from 'actions/constants/discovery';
|
||||
import * as ACCOUNT from 'actions/constants/account';
|
||||
import * as NOTIFICATION from 'actions/constants/notification';
|
||||
@ -14,29 +13,25 @@ import type {
|
||||
GetState,
|
||||
Dispatch,
|
||||
TrezorDevice,
|
||||
Account,
|
||||
} from 'flowtype';
|
||||
import type { Discovery, State } from 'reducers/DiscoveryReducer';
|
||||
import * as BlockchainActions from './BlockchainActions';
|
||||
import * as EthereumDiscoveryActions from './ethereum/DiscoveryActions';
|
||||
import * as RippleDiscoveryActions from './ripple/DiscoveryActions';
|
||||
|
||||
export type DiscoveryStartAction = {
|
||||
type: typeof DISCOVERY.START,
|
||||
device: TrezorDevice,
|
||||
network: string,
|
||||
publicKey: string,
|
||||
chainCode: string,
|
||||
basePath: Array<number>,
|
||||
}
|
||||
export type DiscoveryStartAction = EthereumDiscoveryActions.DiscoveryStartAction | RippleDiscoveryActions.DiscoveryStartAction;
|
||||
|
||||
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,
|
||||
network: string
|
||||
network: string,
|
||||
}
|
||||
|
||||
export type DiscoveryCompleteAction = {
|
||||
type: typeof DISCOVERY.COMPLETE,
|
||||
device: TrezorDevice,
|
||||
network: string
|
||||
network: string,
|
||||
}
|
||||
|
||||
export type DiscoveryAction = {
|
||||
@ -122,44 +117,43 @@ const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean)
|
||||
// first iteration
|
||||
// generate public key for this account
|
||||
// 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 networkData = config.networks.find(c => c.shortcut === network);
|
||||
if (!networkData) return;
|
||||
const network = config.networks.find(c => c.shortcut === networkName);
|
||||
if (!network) return;
|
||||
|
||||
dispatch({
|
||||
type: DISCOVERY.WAITING_FOR_DEVICE,
|
||||
device,
|
||||
network,
|
||||
network: networkName,
|
||||
});
|
||||
|
||||
// get xpub from TREZOR
|
||||
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,
|
||||
});
|
||||
let startAction: DiscoveryStartAction;
|
||||
|
||||
// handle TREZOR response error
|
||||
if (!response.success) {
|
||||
try {
|
||||
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({
|
||||
type: NOTIFICATION.ADD,
|
||||
payload: {
|
||||
type: 'error',
|
||||
title: 'Discovery error',
|
||||
message: response.payload.error,
|
||||
message: error.message,
|
||||
cancelable: true,
|
||||
actions: [
|
||||
{
|
||||
label: 'Try again',
|
||||
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);
|
||||
if (dispatch(isProcessInterrupted(discoveryProcess))) return;
|
||||
|
||||
const basePath: Array<number> = response.payload.path;
|
||||
|
||||
// send data to reducer
|
||||
dispatch({
|
||||
type: DISCOVERY.START,
|
||||
network,
|
||||
device,
|
||||
publicKey: response.payload.publicKey,
|
||||
chainCode: response.payload.chainCode,
|
||||
basePath,
|
||||
});
|
||||
dispatch(startAction);
|
||||
|
||||
// next iteration
|
||||
dispatch(start(device, network));
|
||||
dispatch(start(device, networkName));
|
||||
};
|
||||
|
||||
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch): Promise<void> => {
|
||||
const { completed } = discoveryProcess;
|
||||
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
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 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 { completed, accountIndex } = discoveryProcess;
|
||||
let account: Account;
|
||||
try {
|
||||
const account = await dispatch(BlockchainActions.discoverAccount(device, ethAddress, network));
|
||||
// check for interruption
|
||||
if (dispatch(isProcessInterrupted(discoveryProcess))) return;
|
||||
|
||||
// const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0';
|
||||
const accountIsEmpty = account.nonce <= 0 && account.balance === '0';
|
||||
if (!accountIsEmpty || (accountIsEmpty && completed) || (accountIsEmpty && discoveryProcess.accountIndex === 0)) {
|
||||
dispatch({
|
||||
type: ACCOUNT.CREATE,
|
||||
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));
|
||||
switch (network.type) {
|
||||
case 'ethereum':
|
||||
account = await dispatch(EthereumDiscoveryActions.discoverAccount(device, discoveryProcess));
|
||||
break;
|
||||
case 'ripple':
|
||||
account = await dispatch(RippleDiscoveryActions.discoverAccount(device, discoveryProcess));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`DiscoveryActions.discoverAccount: Unknown network type: ${network.type}`);
|
||||
}
|
||||
} 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({
|
||||
type: DISCOVERY.STOP,
|
||||
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 {
|
||||
const config: Config = await httpRequest(AppConfigJSON, 'json');
|
||||
|
||||
// remove ropsten testnet from config networks
|
||||
// remove testnets from config networks
|
||||
if (!buildUtils.isDev()) {
|
||||
const index = config.networks.findIndex(c => c.shortcut === 'trop');
|
||||
delete config.networks[index];
|
||||
config.networks = config.networks.filter(n => !n.testnet);
|
||||
}
|
||||
|
||||
const ERC20Abi = await httpRequest(Erc20AbiJSON, 'json');
|
||||
@ -156,8 +155,10 @@ const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise<void> =>
|
||||
// load tokens
|
||||
const tokens = await config.networks.reduce(async (promise: Promise<TokensCollection>, network: Network): Promise<TokensCollection> => {
|
||||
const collection: TokensCollection = await promise;
|
||||
const json = await httpRequest(network.tokens, 'json');
|
||||
collection[network.shortcut] = json;
|
||||
if (network.tokens) {
|
||||
const json = await httpRequest(network.tokens, 'json');
|
||||
collection[network.shortcut] = json;
|
||||
}
|
||||
return collection;
|
||||
}, 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 => {
|
||||
// validate version
|
||||
|
@ -13,12 +13,11 @@ export type PendingTxAction = {
|
||||
payload: PendingTx
|
||||
} | {
|
||||
type: typeof PENDING.TX_RESOLVED,
|
||||
tx: PendingTx,
|
||||
receipt?: Object,
|
||||
hash: string,
|
||||
} | {
|
||||
type: typeof PENDING.TX_REJECTED,
|
||||
tx: PendingTx,
|
||||
hash: string,
|
||||
} | {
|
||||
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 = (path: Array<number>): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const selected = getState().wallet.selectedDevice;
|
||||
if (!selected) return;
|
||||
const { network } = getState().selectedAccount;
|
||||
|
||||
if (!selected || !network) return;
|
||||
|
||||
if (selected && (!selected.connected || !selected.available)) {
|
||||
dispatch({
|
||||
@ -60,18 +62,30 @@ export const showAddress = (path: Array<number>): AsyncAction => async (dispatch
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await TrezorConnect.ethereumGetAddress({
|
||||
const params = {
|
||||
device: {
|
||||
path: selected.path,
|
||||
instance: selected.instance,
|
||||
state: selected.state,
|
||||
},
|
||||
path,
|
||||
// useEmptyPassphrase: !selected.instance,
|
||||
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({
|
||||
type: RECEIVE.SHOW_ADDRESS,
|
||||
});
|
||||
|
@ -6,7 +6,6 @@ import * as ACCOUNT from 'actions/constants/account';
|
||||
import * as DISCOVERY from 'actions/constants/discovery';
|
||||
import * as TOKEN from 'actions/constants/token';
|
||||
import * as PENDING from 'actions/constants/pendingTx';
|
||||
import * as SEND from 'actions/constants/send';
|
||||
|
||||
import * as reducerUtils from 'reducers/utils';
|
||||
|
||||
@ -67,6 +66,24 @@ const getAccountLoader = (state: State, selectedAccount: SelectedAccountState):
|
||||
if (account) return null;
|
||||
// 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 (device.connected) {
|
||||
// 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 = [
|
||||
LOCATION_CHANGE,
|
||||
...Object.values(BLOCKCHAIN).filter(v => typeof v === 'string'),
|
||||
SEND.TX_COMPLETE,
|
||||
WALLET.SET_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
|
||||
|
@ -1,18 +1,7 @@
|
||||
/* @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 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 {
|
||||
Dispatch,
|
||||
@ -20,33 +9,27 @@ import type {
|
||||
State as ReducersState,
|
||||
Action,
|
||||
ThunkAction,
|
||||
AsyncAction,
|
||||
TrezorDevice,
|
||||
} from 'flowtype';
|
||||
import type { State, FeeLevel } from 'reducers/SendFormReducer';
|
||||
import type { Account } from 'reducers/AccountsReducer';
|
||||
import * as SessionStorageActions from './SessionStorageActions';
|
||||
import { prepareEthereumTx, serializeEthereumTx } from './TxActions';
|
||||
import * as BlockchainActions from './BlockchainActions';
|
||||
import type { State as EthereumState } from 'reducers/SendFormEthereumReducer';
|
||||
import type { State as RippleState } from 'reducers/SendFormRippleReducer';
|
||||
|
||||
export type SendTxAction = {
|
||||
type: typeof SEND.TX_COMPLETE,
|
||||
account: Account,
|
||||
selectedCurrency: string,
|
||||
amount: string,
|
||||
total: string,
|
||||
tx: any,
|
||||
nonce: number,
|
||||
txid: string,
|
||||
txData: any,
|
||||
};
|
||||
import * as EthereumSendFormActions from './ethereum/SendFormActions';
|
||||
import * as RippleSendFormActions from './ripple/SendFormActions';
|
||||
|
||||
export type SendFormAction = {
|
||||
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,
|
||||
} | SendTxAction;
|
||||
} | {
|
||||
type: typeof SEND.TX_COMPLETE,
|
||||
};
|
||||
|
||||
|
||||
// list of all actions which has influence on "sendForm" reducer
|
||||
// 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
|
||||
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.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;
|
||||
const { network } = currentState.selectedAccount;
|
||||
if (!network) return;
|
||||
|
||||
const isToken = state.currency !== state.networkSymbol;
|
||||
const gasLimit = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString();
|
||||
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
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().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));
|
||||
switch (network.type) {
|
||||
case 'ethereum':
|
||||
dispatch(EthereumSendFormActions.observe(prevState, action));
|
||||
break;
|
||||
case 'ripple':
|
||||
dispatch(RippleSendFormActions.observe(prevState, action));
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* 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 { 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 {
|
||||
ThunkAction,
|
||||
PayloadAction,
|
||||
@ -20,18 +21,18 @@ const getTxDraftKey = (getState: GetState): string => {
|
||||
};
|
||||
|
||||
export const saveDraftTransaction = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const state = getState().sendForm;
|
||||
const state = getState().sendFormEthereum;
|
||||
if (state.untouched) return;
|
||||
|
||||
const key = getTxDraftKey(getState);
|
||||
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 value: ?string = storageUtils.get(TYPE, key);
|
||||
if (!value) return null;
|
||||
const state: ?SendFormState = JSON.parse(value);
|
||||
const state: ?EthereumSendFormState = JSON.parse(value);
|
||||
if (!state) return null;
|
||||
// decide if draft is valid and should be returned
|
||||
// ignore this draft if has any error
|
||||
@ -52,6 +53,21 @@ export const loadDraftTransaction = (): PayloadAction<?SendFormState> => (dispat
|
||||
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 => {
|
||||
const key = getTxDraftKey(getState);
|
||||
storageUtils.remove(TYPE, key);
|
||||
|
@ -9,7 +9,7 @@ import type {
|
||||
import type { State, Token } from 'reducers/TokensReducer';
|
||||
import type { Account } from 'reducers/AccountsReducer';
|
||||
import type { NetworkToken } from 'reducers/LocalStorageReducer';
|
||||
import * as BlockchainActions from './BlockchainActions';
|
||||
import * as BlockchainActions from 'actions/ethereum/BlockchainActions';
|
||||
|
||||
export type TokenAction = {
|
||||
type: typeof TOKEN.FROM_STORAGE,
|
||||
|
@ -17,8 +17,7 @@ import type {
|
||||
UiMessageType,
|
||||
TransportMessage,
|
||||
TransportMessageType,
|
||||
BlockchainMessage,
|
||||
BlockchainMessageType,
|
||||
BlockchainEvent,
|
||||
} from 'trezor-connect';
|
||||
|
||||
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
|
||||
const type: BlockchainMessageType = event.type; // eslint-disable-line prefer-destructuring
|
||||
dispatch({
|
||||
type,
|
||||
payload: event.payload,
|
||||
});
|
||||
// post event to reducers
|
||||
TrezorConnect.on(BLOCKCHAIN_EVENT, (event: BlockchainEvent): void => {
|
||||
dispatch(event);
|
||||
});
|
||||
|
||||
if (buildUtils.isDev()) {
|
||||
|
@ -137,25 +137,24 @@ export const resolvePendingTransactions = (network: string): PromiseAction<void>
|
||||
const instance: Web3Instance = await dispatch(initWeb3(network));
|
||||
const pending = getState().pending.filter(p => p.network === network);
|
||||
pending.forEach(async (tx) => {
|
||||
const status = await instance.web3.eth.getTransaction(tx.id);
|
||||
const status = await instance.web3.eth.getTransaction(tx.hash);
|
||||
if (!status) {
|
||||
dispatch({
|
||||
type: PENDING.TX_REJECTED,
|
||||
tx,
|
||||
hash: tx.hash,
|
||||
});
|
||||
} else {
|
||||
const receipt = await instance.web3.eth.getTransactionReceipt(tx.id);
|
||||
const receipt = await instance.web3.eth.getTransactionReceipt(tx.hash);
|
||||
if (receipt) {
|
||||
if (status.gas !== receipt.gasUsed) {
|
||||
dispatch({
|
||||
type: PENDING.TX_TOKEN_ERROR,
|
||||
tx,
|
||||
hash: tx.hash,
|
||||
});
|
||||
}
|
||||
dispatch({
|
||||
type: PENDING.TX_RESOLVED,
|
||||
tx,
|
||||
receipt,
|
||||
hash: tx.hash,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -197,7 +196,11 @@ export const updateAccount = (account: Account, newAccount: EthereumAccount, net
|
||||
const balance = await instance.web3.eth.getBalance(account.address);
|
||||
const nonce = await instance.web3.eth.getTransactionCount(account.address);
|
||||
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
|
||||
|
@ -1,6 +1,4 @@
|
||||
/* @flow */
|
||||
|
||||
export const READY: 'blockchain__ready' = 'blockchain__ready';
|
||||
export const CONNECTING: 'blockchain__connecting' = 'blockchain__connecting';
|
||||
export const CONNECTED: 'blockchain__connected' = 'blockchain__connected';
|
||||
export const DISCONNECTED: 'blockchain__disconnected' = 'blockchain__disconnected';
|
||||
export const UPDATE_FEE: 'blockchain__update_fee' = 'blockchain__update_fee';
|
@ -3,6 +3,8 @@
|
||||
|
||||
export const START: 'discovery__start' = 'discovery__start';
|
||||
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 WAITING_FOR_DEVICE: 'discovery__waiting_for_device' = 'discovery__waiting_for_device';
|
||||
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 BLOCK_UPDATED: 'web3__block_updated' = 'web3__block_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';
|
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
@ -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
@ -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,
|
||||
PayloadAction,
|
||||
} from 'flowtype';
|
||||
import type { State, FeeLevel } from 'reducers/SendFormReducer';
|
||||
import type { State, FeeLevel } from 'reducers/SendFormEthereumReducer';
|
||||
|
||||
// general regular expressions
|
||||
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 state = getState().sendForm;
|
||||
const state = getState().sendFormEthereum;
|
||||
if (network === state.networkSymbol) return;
|
||||
|
||||
// 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
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
gasPriceNeedsUpdate: true,
|
||||
@ -62,6 +63,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct
|
||||
const selectedFeeLevel = getSelectedFeeLevel(feeLevels, state.selectedFeeLevel);
|
||||
dispatch({
|
||||
type: SEND.CHANGE,
|
||||
networkType: 'ethereum',
|
||||
state: {
|
||||
...state,
|
||||
gasPriceNeedsUpdate: false,
|
||||
@ -81,7 +83,7 @@ export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAct
|
||||
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().sendForm));
|
||||
let state: State = JSON.parse(JSON.stringify(getState().sendFormEthereum));
|
||||
// reset errors
|
||||
state.errors = {};
|
||||
state.warnings = {};
|
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
@ -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
@ -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
@ -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/ada.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/components/images/CoinLogo/images/xlm.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
src/components/images/CoinLogo/images/xrp.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
@ -11,7 +11,7 @@ import type { State, Dispatch } from 'flowtype';
|
||||
|
||||
import Modal from './index';
|
||||
|
||||
type OwnProps = { }
|
||||
type OwnProps = {};
|
||||
|
||||
type StateProps = {
|
||||
modal: $ElementType<State, 'modal'>,
|
||||
@ -19,16 +19,17 @@ type StateProps = {
|
||||
devices: $ElementType<State, 'devices'>,
|
||||
connect: $ElementType<State, 'connect'>,
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||
sendForm: $ElementType<State, 'sendForm'>,
|
||||
sendFormEthereum: $ElementType<State, 'sendFormEthereum'>,
|
||||
sendFormRipple: $ElementType<State, 'sendFormRipple'>,
|
||||
receive: $ElementType<State, 'receive'>,
|
||||
localStorage: $ElementType<State, 'localStorage'>,
|
||||
wallet: $ElementType<State, 'wallet'>,
|
||||
}
|
||||
};
|
||||
|
||||
type DispatchProps = {
|
||||
modalActions: typeof ModalActions,
|
||||
receiveActions: typeof ReceiveActions,
|
||||
}
|
||||
};
|
||||
|
||||
export type Props = StateProps & DispatchProps;
|
||||
|
||||
@ -38,7 +39,8 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
|
||||
devices: state.devices,
|
||||
connect: state.connect,
|
||||
selectedAccount: state.selectedAccount,
|
||||
sendForm: state.sendForm,
|
||||
sendFormEthereum: state.sendFormEthereum,
|
||||
sendFormRipple: state.sendFormRipple,
|
||||
receive: state.receive,
|
||||
localStorage: state.localStorage,
|
||||
wallet: state.wallet,
|
||||
|
@ -12,12 +12,11 @@ import P from 'components/Paragraph';
|
||||
import Icon from 'components/Icon';
|
||||
import { H3 } from 'components/Heading';
|
||||
|
||||
import type { TrezorDevice } from 'flowtype';
|
||||
import type { Props as BaseProps } from '../../Container';
|
||||
import type { TrezorDevice, State } from 'flowtype';
|
||||
|
||||
type Props = {
|
||||
device: TrezorDevice;
|
||||
sendForm: $ElementType<BaseProps, 'sendForm'>;
|
||||
sendForm: $ElementType<State, 'sendFormEthereum'> | $ElementType<State, 'sendFormRipple'>;
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
@ -51,10 +50,11 @@ const ConfirmSignTx = (props: Props) => {
|
||||
const {
|
||||
amount,
|
||||
address,
|
||||
currency,
|
||||
selectedFeeLevel,
|
||||
} = props.sendForm;
|
||||
|
||||
const currency: string = typeof props.sendForm.currency === 'string' ? props.sendForm.currency : props.sendForm.networkSymbol;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Header>
|
||||
|
BIN
src/components/modals/external/Cardano/images/cardano.png
vendored
Normal file
After Width: | Height: | Size: 3.0 KiB |
70
src/components/modals/external/Cardano/index.js
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import colors from 'config/colors';
|
||||
import icons from 'config/icons';
|
||||
import Icon from 'components/Icon';
|
||||
import Link from 'components/Link';
|
||||
import Button from 'components/Button';
|
||||
import { H2 } from 'components/Heading';
|
||||
import P from 'components/Paragraph';
|
||||
import coins from 'constants/coins';
|
||||
|
||||
import CardanoImage from './images/cardano.png';
|
||||
import type { Props as BaseProps } from '../../Container';
|
||||
|
||||
type Props = {
|
||||
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>;
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
max-width: 620px;
|
||||
padding: 30px 48px;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
margin: 10px 0 10px 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 10px;
|
||||
`;
|
||||
|
||||
const Img = styled.img`
|
||||
display: block;
|
||||
max-width: 100px;
|
||||
margin: 0 auto;
|
||||
height: auto;
|
||||
padding-bottom: 20px;
|
||||
`;
|
||||
|
||||
const CardanoWallet = (props: Props) => (
|
||||
<Wrapper>
|
||||
<StyledLink onClick={props.onCancel}>
|
||||
<Icon
|
||||
size={20}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
icon={icons.CLOSE}
|
||||
/>
|
||||
</StyledLink>
|
||||
<Img src={CardanoImage} />
|
||||
<H2>Cardano wallet</H2>
|
||||
<P isSmaller>You will be redirected to external wallet</P>
|
||||
|
||||
<Link href={coins.find(i => i.id === 'ada').url}>
|
||||
<StyledButton>Go to external wallet</StyledButton>
|
||||
</Link>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
CardanoWallet.propTypes = {
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CardanoWallet;
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
BIN
src/components/modals/external/Stellar/images/xlm.png
vendored
Normal file
After Width: | Height: | Size: 6.3 KiB |
70
src/components/modals/external/Stellar/index.js
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import colors from 'config/colors';
|
||||
import icons from 'config/icons';
|
||||
import Icon from 'components/Icon';
|
||||
import Link from 'components/Link';
|
||||
import Button from 'components/Button';
|
||||
import { H2 } from 'components/Heading';
|
||||
import P from 'components/Paragraph';
|
||||
import coins from 'constants/coins';
|
||||
|
||||
import StellarImage from './images/xlm.png';
|
||||
import type { Props as BaseProps } from '../../Container';
|
||||
|
||||
type Props = {
|
||||
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>;
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
max-width: 620px;
|
||||
padding: 30px 48px;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
margin: 10px 0 10px 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 10px;
|
||||
`;
|
||||
|
||||
const Img = styled.img`
|
||||
display: block;
|
||||
max-width: 100px;
|
||||
margin: 0 auto;
|
||||
height: auto;
|
||||
padding-bottom: 20px;
|
||||
`;
|
||||
|
||||
const StellarWallet = (props: Props) => (
|
||||
<Wrapper>
|
||||
<StyledLink onClick={props.onCancel}>
|
||||
<Icon
|
||||
size={20}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
icon={icons.CLOSE}
|
||||
/>
|
||||
</StyledLink>
|
||||
<Img src={StellarImage} />
|
||||
<H2>Stellar wallet</H2>
|
||||
<P isSmaller>You will be redirected to external wallet</P>
|
||||
|
||||
<Link href={coins.find(i => i.id === 'xlm').url}>
|
||||
<StyledButton>Go to external wallet</StyledButton>
|
||||
</Link>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
StellarWallet.propTypes = {
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default StellarWallet;
|
@ -1,10 +1,10 @@
|
||||
/* @flow */
|
||||
|
||||
import * as React from 'react';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import colors from 'config/colors';
|
||||
import { FADE_IN } from 'config/animations';
|
||||
|
||||
import { UI } from 'trezor-connect';
|
||||
import * as MODAL from 'actions/constants/modal';
|
||||
@ -25,19 +25,12 @@ import DuplicateDevice from 'components/modals/device/Duplicate';
|
||||
import WalletType from 'components/modals/device/WalletType';
|
||||
|
||||
// external context
|
||||
import NemWallet from 'components/modals/external/NemWallet';
|
||||
import Nem from 'components/modals/external/Nem';
|
||||
import Cardano from 'components/modals/external/Cardano';
|
||||
import Stellar from 'components/modals/external/Stellar';
|
||||
|
||||
import type { Props } from './Container';
|
||||
|
||||
const Fade = (props: { children: React.Node}) => (
|
||||
<CSSTransition
|
||||
{...props}
|
||||
timeout={1000}
|
||||
classNames="fade"
|
||||
>{ props.children }
|
||||
</CSSTransition>
|
||||
);
|
||||
|
||||
const ModalContainer = styled.div`
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
@ -51,6 +44,7 @@ const ModalContainer = styled.div`
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
animation: ${FADE_IN} 0.3s;
|
||||
`;
|
||||
|
||||
const ModalWindow = styled.div`
|
||||
@ -88,13 +82,22 @@ const getDeviceContextModal = (props: Props) => {
|
||||
case 'ButtonRequest_PassphraseType':
|
||||
return <PassphraseType device={modal.device} />;
|
||||
|
||||
case 'ButtonRequest_SignTx':
|
||||
return <ConfirmSignTx device={modal.device} sendForm={props.sendForm} />;
|
||||
case 'ButtonRequest_SignTx': {
|
||||
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':
|
||||
return <ConfirmAction />;
|
||||
|
||||
case 'ButtonRequest_Other':
|
||||
case 'ButtonRequest_ConfirmOutput':
|
||||
return <ConfirmAction />;
|
||||
|
||||
case RECEIVE.REQUEST_UNVERIFIED:
|
||||
@ -153,7 +156,11 @@ const getExternalContextModal = (props: Props) => {
|
||||
|
||||
switch (modal.windowType) {
|
||||
case 'xem':
|
||||
return (<NemWallet onCancel={modalActions.onCancel} />);
|
||||
return (<Nem onCancel={modalActions.onCancel} />);
|
||||
case 'xlm':
|
||||
return (<Stellar onCancel={modalActions.onCancel} />);
|
||||
case 'ada':
|
||||
return (<Cardano onCancel={modalActions.onCancel} />);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -177,13 +184,11 @@ const Modal = (props: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Fade key="modal-fade">
|
||||
<ModalContainer>
|
||||
<ModalWindow>
|
||||
{ component }
|
||||
</ModalWindow>
|
||||
</ModalContainer>
|
||||
</Fade>
|
||||
<ModalContainer>
|
||||
<ModalWindow>
|
||||
{ component }
|
||||
</ModalWindow>
|
||||
</ModalContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -56,4 +56,13 @@ export const PULSATE = keyframes`
|
||||
50% {
|
||||
opacity: 1.0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FADE_IN = keyframes`
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
@ -55,4 +55,16 @@ export default [
|
||||
url: 'https://nem.io/downloads/',
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
id: 'xlm',
|
||||
coinName: 'Stellar',
|
||||
url: 'https://trezor.io/stellar',
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
id: 'ada',
|
||||
coinName: 'Cardano',
|
||||
url: 'https://adalite.io/app',
|
||||
external: true,
|
||||
},
|
||||
];
|
@ -42,8 +42,8 @@ import type {
|
||||
DeviceMode,
|
||||
DeviceMessageType,
|
||||
TransportMessageType,
|
||||
BlockchainMessageType,
|
||||
UiMessageType,
|
||||
BlockchainEvent,
|
||||
} from 'trezor-connect';
|
||||
|
||||
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
|
||||
type IFrameHandshake = {
|
||||
type: 'iframe_handshake',
|
||||
@ -128,7 +123,7 @@ export type Action =
|
||||
| TransportEventAction
|
||||
| DeviceEventAction
|
||||
| UiEventAction
|
||||
| BlockchainEventAction
|
||||
| BlockchainEvent
|
||||
|
||||
| SelectedAccountAction
|
||||
| AccountAction
|
||||
|
@ -20,9 +20,12 @@ export type Account = {
|
||||
+addressPath: Array<number>;
|
||||
+address: string;
|
||||
balance: string;
|
||||
availableBalance: string;
|
||||
sequence: number;
|
||||
nonce: number;
|
||||
block: number;
|
||||
transactions: number;
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
export type State = Array<Account>;
|
||||
|
@ -1,59 +1,94 @@
|
||||
/* @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 { BlockchainConnect, BlockchainError, BlockchainBlock } from 'trezor-connect';
|
||||
|
||||
export type BlockchainNetwork = {
|
||||
+shortcut: string;
|
||||
connected: boolean;
|
||||
}
|
||||
+shortcut: string,
|
||||
connected: boolean,
|
||||
fee: string,
|
||||
block: number,
|
||||
};
|
||||
|
||||
export type State = Array<BlockchainNetwork>;
|
||||
|
||||
export const initialState: State = [];
|
||||
|
||||
const find = (state: State, shortcut: string): number => state.findIndex(b => b.shortcut === shortcut);
|
||||
|
||||
const connect = (state: State, action: any): State => {
|
||||
const onConnect = (state: State, action: BlockchainConnect): State => {
|
||||
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,
|
||||
connected: true,
|
||||
};
|
||||
const newState: State = [...state];
|
||||
const index: number = find(newState, shortcut);
|
||||
if (index >= 0) {
|
||||
newState[index] = network;
|
||||
} else {
|
||||
newState.push(network);
|
||||
}
|
||||
return newState;
|
||||
fee: info.fee,
|
||||
block: info.block,
|
||||
}]);
|
||||
};
|
||||
|
||||
const disconnect = (state: State, action: any): State => {
|
||||
const onError = (state: State, action: BlockchainError): State => {
|
||||
const shortcut = action.payload.coin.shortcut.toLowerCase();
|
||||
const network: BlockchainNetwork = {
|
||||
shortcut,
|
||||
connected: false,
|
||||
};
|
||||
const newState: State = [...state];
|
||||
const index: number = find(newState, shortcut);
|
||||
if (index >= 0) {
|
||||
newState[index] = network;
|
||||
} else {
|
||||
newState.push(network);
|
||||
const network = state.find(b => b.shortcut === shortcut);
|
||||
if (network) {
|
||||
const others = state.filter(b => b !== network);
|
||||
return others.concat([{
|
||||
...network,
|
||||
connected: false,
|
||||
}]);
|
||||
}
|
||||
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 => {
|
||||
switch (action.type) {
|
||||
case BLOCKCHAIN.CONNECT:
|
||||
return connect(state, action);
|
||||
case BLOCKCHAIN.ERROR:
|
||||
return disconnect(state, action);
|
||||
case BLOCKCHAIN_EVENT.CONNECT:
|
||||
return onConnect(state, action);
|
||||
case BLOCKCHAIN_EVENT.ERROR:
|
||||
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:
|
||||
return state;
|
||||
|
@ -19,9 +19,6 @@ import type { Account } from './AccountsReducer';
|
||||
|
||||
export type Discovery = {
|
||||
network: string;
|
||||
publicKey: string;
|
||||
chainCode: string;
|
||||
hdKey: HDKey;
|
||||
basePath: Array<number>;
|
||||
deviceState: string;
|
||||
accountIndex: number;
|
||||
@ -29,34 +26,57 @@ export type Discovery = {
|
||||
completed: boolean;
|
||||
waitingForDevice: 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>;
|
||||
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 start = (state: State, action: DiscoveryStartAction): State => {
|
||||
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 = {
|
||||
network: action.network,
|
||||
publicKey: action.publicKey,
|
||||
chainCode: action.chainCode,
|
||||
hdKey,
|
||||
basePath: action.basePath,
|
||||
...defaultDiscovery,
|
||||
network: action.network.shortcut,
|
||||
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 index: number = findIndex(state, action.network, deviceState);
|
||||
const index: number = findIndex(state, action.network.shortcut, deviceState);
|
||||
if (index >= 0) {
|
||||
newState[index] = instance;
|
||||
} else {
|
||||
@ -105,17 +125,10 @@ const stop = (state: State, device: TrezorDevice): State => {
|
||||
const waitingForDevice = (state: State, action: DiscoveryWaitingAction): State => {
|
||||
const deviceState: string = action.device.state || '0';
|
||||
const instance: Discovery = {
|
||||
...defaultDiscovery,
|
||||
network: action.network,
|
||||
deviceState,
|
||||
publicKey: '',
|
||||
chainCode: '',
|
||||
hdKey: null,
|
||||
basePath: [],
|
||||
accountIndex: 0,
|
||||
interrupted: false,
|
||||
completed: false,
|
||||
waitingForDevice: true,
|
||||
waitingForBlockchain: false,
|
||||
};
|
||||
|
||||
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 deviceState: string = action.device.state || '0';
|
||||
const instance: Discovery = {
|
||||
...defaultDiscovery,
|
||||
network: action.network,
|
||||
deviceState,
|
||||
publicKey: '',
|
||||
chainCode: '',
|
||||
hdKey: null,
|
||||
basePath: [],
|
||||
accountIndex: 0,
|
||||
interrupted: false,
|
||||
completed: false,
|
||||
waitingForDevice: false,
|
||||
waitingForBlockchain: true,
|
||||
};
|
||||
|
||||
@ -156,6 +162,19 @@ const waitingForBlockchain = (state: State, action: DiscoveryWaitingAction): Sta
|
||||
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 {
|
||||
switch (action.type) {
|
||||
case DISCOVERY.START:
|
||||
@ -170,6 +189,10 @@ export default function discovery(state: State = initialState, action: Action):
|
||||
return waitingForDevice(state, action);
|
||||
case DISCOVERY.WAITING_FOR_BLOCKCHAIN:
|
||||
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:
|
||||
return action.payload.map((d) => {
|
||||
const hdKey: HDKey = new HDKey();
|
||||
|
@ -6,7 +6,9 @@ import * as STORAGE from 'actions/constants/localStorage';
|
||||
import type { Action } from 'flowtype';
|
||||
|
||||
export type Network = {
|
||||
type: string;
|
||||
name: string;
|
||||
testnet?: boolean;
|
||||
shortcut: string;
|
||||
symbol: string;
|
||||
bip44: string;
|
||||
|
@ -1,62 +1,39 @@
|
||||
/* @flow */
|
||||
import * as CONNECT from 'actions/constants/TrezorConnect';
|
||||
import * as PENDING from 'actions/constants/pendingTx';
|
||||
import * as SEND from 'actions/constants/send';
|
||||
|
||||
import type { TrezorDevice, Action } from 'flowtype';
|
||||
import type { SendTxAction } from 'actions/SendFormActions';
|
||||
import type { Action } from 'flowtype';
|
||||
|
||||
export type PendingTx = {
|
||||
+type: 'send' | 'receive';
|
||||
+id: string;
|
||||
+network: string;
|
||||
+address: string;
|
||||
+deviceState: string;
|
||||
+currency: string;
|
||||
+amount: string;
|
||||
+total: string;
|
||||
+tx: any;
|
||||
+nonce: number;
|
||||
rejected: boolean;
|
||||
}
|
||||
+type: 'send' | 'recv',
|
||||
+deviceState: string,
|
||||
+sequence: number,
|
||||
+hash: string,
|
||||
+network: string,
|
||||
+address: string,
|
||||
+currency: string,
|
||||
+amount: string,
|
||||
+total: string,
|
||||
+fee: string,
|
||||
rejected?: boolean,
|
||||
};
|
||||
|
||||
export type State = Array<PendingTx>;
|
||||
|
||||
const initialState: State = [];
|
||||
|
||||
const add = (state: State, action: SendTxAction): 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 add = (state: State, payload: PendingTx): State => {
|
||||
const newState = [...state];
|
||||
newState.push(payload);
|
||||
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) => {
|
||||
if (tx.id === id && !tx.rejected) {
|
||||
const reject = (state: State, hash: string): State => state.map((tx) => {
|
||||
if (tx.hash === hash && !tx.rejected) {
|
||||
return { ...tx, rejected: true };
|
||||
}
|
||||
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 {
|
||||
switch (action.type) {
|
||||
case SEND.TX_COMPLETE:
|
||||
return add(state, action);
|
||||
|
||||
case CONNECT.FORGET:
|
||||
case CONNECT.FORGET_SINGLE:
|
||||
case CONNECT.FORGET_SILENT:
|
||||
case CONNECT.RECEIVE_WALLET_TYPE:
|
||||
return clear(state, action.device);
|
||||
return removeByDeviceState(state, action.device.state);
|
||||
|
||||
// case PENDING.ADD:
|
||||
// return add(state, action.payload);
|
||||
case PENDING.ADD:
|
||||
return add(state, action.payload);
|
||||
case PENDING.TX_RESOLVED:
|
||||
return remove(state, action.tx.id);
|
||||
return removeByHash(state, action.hash);
|
||||
case PENDING.TX_REJECTED:
|
||||
return reject(state, action.tx.id);
|
||||
return reject(state, action.hash);
|
||||
|
||||
case PENDING.FROM_STORAGE:
|
||||
return action.payload;
|
||||
|
@ -75,16 +75,15 @@ export const initialState: 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) {
|
||||
case SEND.INIT:
|
||||
case SEND.CHANGE:
|
||||
case SEND.VALIDATION:
|
||||
return action.state;
|
||||
|
||||
case ACCOUNT.DISPOSE:
|
||||
return initialState;
|
||||
|
||||
|
||||
case SEND.TOGGLE_ADVANCED:
|
||||
return {
|
||||
...state,
|
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 accounts from 'reducers/AccountsReducer';
|
||||
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 summary from 'reducers/SummaryReducer';
|
||||
import tokens from 'reducers/TokensReducer';
|
||||
@ -32,7 +33,8 @@ const reducers = {
|
||||
web3,
|
||||
accounts,
|
||||
selectedAccount,
|
||||
sendForm,
|
||||
sendFormEthereum,
|
||||
sendFormRipple,
|
||||
receive,
|
||||
summary,
|
||||
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);
|
||||
};
|
||||
|
||||
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;
|
||||
return Math.max(value, tx.nonce + 1);
|
||||
return Math.max(value, tx.sequence + 1);
|
||||
}, 0);
|
||||
|
||||
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 ACCOUNT from 'actions/constants/account';
|
||||
import * as DISCOVERY from 'actions/constants/discovery';
|
||||
import * as SEND from 'actions/constants/send';
|
||||
import * as PENDING from 'actions/constants/pendingTx';
|
||||
import * as WALLET from 'actions/constants/wallet';
|
||||
|
||||
@ -59,7 +58,7 @@ const LocalStorageService: Middleware = (api: MiddlewareAPI) => (next: Middlewar
|
||||
api.dispatch(LocalStorageActions.save());
|
||||
break;
|
||||
|
||||
case SEND.TX_COMPLETE:
|
||||
case PENDING.ADD:
|
||||
case PENDING.TX_RESOLVED:
|
||||
case PENDING.TX_REJECTED:
|
||||
api.dispatch(LocalStorageActions.save());
|
||||
|
@ -58,11 +58,11 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
|
||||
} else if (action.type === CONNECT.DUPLICATE) {
|
||||
api.dispatch(RouterActions.selectDevice(action.device));
|
||||
} 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) {
|
||||
// api.dispatch(BlockchainActions.onNotification(action.payload));
|
||||
api.dispatch(BlockchainActions.onNotification(action.payload));
|
||||
} else if (action.type === BLOCKCHAIN.ERROR) {
|
||||
api.dispatch(BlockchainActions.error(action.payload));
|
||||
api.dispatch(BlockchainActions.onError(action.payload));
|
||||
}
|
||||
|
||||
return action;
|
||||
|
@ -9,7 +9,7 @@ import * as NotificationActions from 'actions/NotificationActions';
|
||||
import * as LocalStorageActions from 'actions/LocalStorageActions';
|
||||
import * as TrezorConnectActions from 'actions/TrezorConnectActions';
|
||||
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 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 (!await api.dispatch(SelectedAccountActions.observe(prevState, action))) {
|
||||
// if "selectedAccount" didn't change observe send form props changes
|
||||
api.dispatch(SendFormActionActions.observe(prevState, action));
|
||||
api.dispatch(SendFormActions.observe(prevState, action));
|
||||
}
|
||||
} else {
|
||||
// no changes in common values
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* @flow */
|
||||
|
||||
// TODO: chagne currency units
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
const currencyUnitsConstant: string = 'mbtc2';
|
||||
|
||||
export const formatAmount = (n: number, coinInfo: any, currencyUnits: string = currencyUnitsConstant): string => {
|
||||
@ -52,3 +53,7 @@ export const hexToString = (hex: string): string => {
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
export const toDecimalAmount = (amount: string, decimals: number): string => new BigNumber(amount).div(10 ** decimals).toString(10);
|
||||
|
||||
export const 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 (
|
||||
<Wrapper>
|
||||
<NavLink to={baseUrl}>
|
||||
|
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 */
|
||||
import React from 'react';
|
||||
import { QRCode } from 'react-qr-svg';
|
||||
import styled from 'styled-components';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
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 type { State } from 'flowtype';
|
||||
import EthereumTypeReceiveForm from './ethereum/Container';
|
||||
import RippleTypeReceiveForm from './ripple/Container';
|
||||
|
||||
import ICONS from 'config/icons';
|
||||
import colors from 'config/colors';
|
||||
import { CONTEXT_DEVICE } from 'actions/constants/modal';
|
||||
export type BaseProps = {
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||
}
|
||||
|
||||
import Content from 'views/Wallet/components/Content';
|
||||
import VerifyAddressTooltip from './components/VerifyAddressTooltip';
|
||||
// return container for requested network type
|
||||
export default connect((state: State): BaseProps => ({
|
||||
selectedAccount: state.selectedAccount,
|
||||
}), null)((props) => {
|
||||
const { network } = props.selectedAccount;
|
||||
if (!network) return null;
|
||||
|
||||
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;
|
||||
switch (network.type) {
|
||||
case 'ethereum':
|
||||
return <EthereumTypeReceiveForm />;
|
||||
case 'ripple':
|
||||
return <RippleTypeReceiveForm />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
`;
|
||||
|
||||
const ShowAddressIcon = styled(Icon)`
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
`;
|
||||
|
||||
const EyeButton = styled(Button)`
|
||||
z-index: 10001;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
background: transparent;
|
||||
top: 5px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding-bottom: 28px;
|
||||
|
||||
@media screen and (max-width: 795px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const QrWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const AccountReceive = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
discovery,
|
||||
shouldRender,
|
||||
loader,
|
||||
} = props.selectedAccount;
|
||||
const { type, title, message } = loader;
|
||||
if (!device || !account || !discovery || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
|
||||
|
||||
const {
|
||||
addressVerified,
|
||||
addressUnverified,
|
||||
} = props.receive;
|
||||
|
||||
const isAddressVerifying = props.modal.context === CONTEXT_DEVICE && props.modal.windowType === 'ButtonRequest_Address';
|
||||
const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified;
|
||||
|
||||
let address = `${account.address.substring(0, 20)}...`;
|
||||
if (addressVerified || addressUnverified || isAddressVerifying) {
|
||||
({ address } = account);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<React.Fragment>
|
||||
<Title>Receive Ethereum or tokens</Title>
|
||||
<AddressWrapper isShowingQrCode={addressVerified || addressUnverified}>
|
||||
<Row>
|
||||
<Input
|
||||
type="text"
|
||||
readOnly
|
||||
autoSelect
|
||||
topLabel="Address"
|
||||
value={address}
|
||||
isPartiallyHidden={isAddressHidden}
|
||||
trezorAction={isAddressVerifying ? (
|
||||
<React.Fragment>
|
||||
<Icon
|
||||
icon={ICONS.T1}
|
||||
color={colors.WHITE}
|
||||
/>
|
||||
Check address on your Trezor
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
icon={((addressVerified || addressUnverified) && !isAddressVerifying) && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
content={(
|
||||
<VerifyAddressTooltip
|
||||
isConnected={device.connected}
|
||||
isAvailable={device.available}
|
||||
addressUnverified={addressUnverified}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<EyeButton onClick={() => props.showAddress(account.addressPath)}>
|
||||
<Icon
|
||||
icon={addressUnverified ? ICONS.EYE_CROSSED : ICONS.EYE}
|
||||
color={addressUnverified ? colors.ERROR_PRIMARY : colors.TEXT_PRIMARY}
|
||||
/>
|
||||
</EyeButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
{!(addressVerified || addressUnverified) && (
|
||||
<ShowAddressButton onClick={() => props.showAddress(account.addressPath)} isDisabled={device.connected && !discovery.completed}>
|
||||
<ShowAddressIcon icon={ICONS.EYE} color={colors.WHITE} />Show full address
|
||||
</ShowAddressButton>
|
||||
)}
|
||||
</Row>
|
||||
{(addressVerified || addressUnverified) && !isAddressVerifying && (
|
||||
<QrWrapper>
|
||||
<Label>QR code</Label>
|
||||
<StyledQRCode
|
||||
bgColor="#FFFFFF"
|
||||
fgColor="#000000"
|
||||
level="Q"
|
||||
style={{ width: 150 }}
|
||||
value={account.address}
|
||||
/>
|
||||
</QrWrapper>
|
||||
)}
|
||||
</AddressWrapper>
|
||||
</React.Fragment>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountReceive;
|
||||
});
|
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
@ -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 { FONT_SIZE } from 'config/variables';
|
||||
|
||||
import type { Props as BaseProps } from '../../Container';
|
||||
import type { Props as BaseProps } from '../../ethereum/Container';
|
||||
|
||||
type Props = BaseProps & {
|
||||
children: React.Node,
|
||||
|
@ -9,7 +9,7 @@ import ScaleText from 'react-scale-text';
|
||||
|
||||
import type { Network } from 'reducers/LocalStorageReducer';
|
||||
import type { Token } from 'reducers/TokensReducer';
|
||||
import type { Props as BaseProps } from '../../Container';
|
||||
import type { BaseProps } from '../../index';
|
||||
|
||||
type Props = {
|
||||
pending: $PropertyType<$ElementType<BaseProps, 'selectedAccount'>, 'pending'>,
|
||||
@ -140,7 +140,7 @@ class PendingTransactions extends PureComponent<Props> {
|
||||
<H2>Pending transactions</H2>
|
||||
{this.getPendingTransactions().map(tx => (
|
||||
<TransactionWrapper
|
||||
key={tx.id}
|
||||
key={tx.hash}
|
||||
>
|
||||
<TransactionIcon
|
||||
textColor={() => this.getTransactionIconColors(tx).textColor}
|
||||
@ -154,7 +154,7 @@ class PendingTransactions extends PureComponent<Props> {
|
||||
|
||||
<TransactionName>
|
||||
<StyledLink
|
||||
href={`${this.props.network.explorer.tx}${tx.id}`}
|
||||
href={`${this.props.network.explorer.tx}${tx.hash}`}
|
||||
isGray
|
||||
>
|
||||
{this.getTransactionName(tx)}
|
||||
|
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
@ -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,402 +1,28 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { Select } from 'components/Select';
|
||||
import Button from 'components/Button';
|
||||
import Input from 'components/inputs/Input';
|
||||
import Icon from 'components/Icon';
|
||||
import Link from 'components/Link';
|
||||
import ICONS from 'config/icons';
|
||||
import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables';
|
||||
import colors from 'config/colors';
|
||||
import Title from 'views/Wallet/components/Title';
|
||||
import P from 'components/Paragraph';
|
||||
import Content from 'views/Wallet/components/Content';
|
||||
import type { Token } from 'flowtype';
|
||||
import AdvancedForm from './components/AdvancedForm';
|
||||
import PendingTransactions from './components/PendingTransactions';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
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
|
||||
// and put it inside config/variables.js
|
||||
const SmallScreenWidth = '850px';
|
||||
export type BaseProps = {
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||
}
|
||||
|
||||
const AmountInputLabelWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
// return container for requested network type
|
||||
export default connect((state: State): BaseProps => ({
|
||||
selectedAccount: state.selectedAccount,
|
||||
}), null)((props) => {
|
||||
const { network } = props.selectedAccount;
|
||||
if (!network) return null;
|
||||
|
||||
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.LIGHT};
|
||||
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};
|
||||
switch (network.type) {
|
||||
case 'ethereum':
|
||||
return <EthereumTypeSendForm />;
|
||||
case 'ripple':
|
||||
return <RippleTypeSendForm />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
${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.SEMIBOLD};
|
||||
`;
|
||||
|
||||
const SendButton = styled(Button)`
|
||||
min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')};
|
||||
|
||||
@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 { connect } from 'react-redux';
|
||||
|
||||
import SendFormActions from 'actions/SendFormActions';
|
||||
import SendFormActions from 'actions/ripple/SendFormActions';
|
||||
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
|
||||
import type { State, Dispatch } from 'flowtype';
|
||||
import AccountSend from './index';
|
||||
@ -12,7 +12,7 @@ type OwnProps = {}
|
||||
|
||||
export type StateProps = {
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||
sendForm: $ElementType<State, 'sendForm'>,
|
||||
sendForm: $ElementType<State, 'sendFormRipple'>,
|
||||
wallet: $ElementType<State, 'wallet'>,
|
||||
fiat: $ElementType<State, 'fiat'>,
|
||||
localStorage: $ElementType<State, 'localStorage'>,
|
||||
@ -26,7 +26,7 @@ export type Props = StateProps & DispatchProps;
|
||||
|
||||
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
|
||||
selectedAccount: state.selectedAccount,
|
||||
sendForm: state.sendForm,
|
||||
sendForm: state.sendFormRipple,
|
||||
wallet: state.wallet,
|
||||
fiat: state.fiat,
|
||||
localStorage: state.localStorage,
|
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');
|
||||
return (
|
||||
<Content>
|
||||
<Title>Sign & Verify</Title>
|
||||
<Title>Sign & Verify</Title>
|
||||
<Wrapper>
|
||||
<Sign>
|
||||
<Row>
|
||||
|
@ -7,13 +7,12 @@ import colors from 'config/colors';
|
||||
import ICONS from 'config/icons';
|
||||
import { FONT_SIZE, FONT_WEIGHT } from 'config/variables';
|
||||
|
||||
import type { Network } from 'flowtype';
|
||||
import type { Props as BaseProps } from '../../Container';
|
||||
import type { Network, State as ReducersState } from 'flowtype';
|
||||
|
||||
type Props = {
|
||||
network: Network,
|
||||
balance: string,
|
||||
fiat: $ElementType<BaseProps, 'fiat'>,
|
||||
fiat: $ElementType<ReducersState, 'fiat'>,
|
||||
}
|
||||
|
||||
type State = {
|
||||
|
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 */
|
||||
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 { connect } from 'react-redux';
|
||||
|
||||
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 { State } from 'flowtype';
|
||||
import EthereumTypeSummary from './ethereum/Container';
|
||||
import RippleTypeSummary from './ripple/Container';
|
||||
|
||||
import type { Props } from './Container';
|
||||
type WrapperProps = {
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>,
|
||||
}
|
||||
|
||||
const AccountHeading = styled.div`
|
||||
padding-bottom: 35px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
// return container for requested network type
|
||||
export default connect((state: State): WrapperProps => ({
|
||||
selectedAccount: state.selectedAccount,
|
||||
}), null)((props) => {
|
||||
const { network } = props.selectedAccount;
|
||||
if (!network) return null;
|
||||
|
||||
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.MEDIUM};
|
||||
color: ${colors.WALLET_TITLE};
|
||||
`;
|
||||
|
||||
const StyledCoinLogo = styled(CoinLogo)`
|
||||
margin-right: 10px;
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
position: relative;
|
||||
top: -7px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
switch (network.type) {
|
||||
case 'ethereum':
|
||||
return <EthereumTypeSummary />;
|
||||
case 'ripple':
|
||||
return <RippleTypeSummary />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
`;
|
||||
|
||||
const AsyncSelectWrapper = styled.div`
|
||||
padding-bottom: 32px;
|
||||
`;
|
||||
|
||||
const AddedTokensWrapper = styled.div``;
|
||||
|
||||
const AccountSummary = (props: Props) => {
|
||||
const device = props.wallet.selectedDevice;
|
||||
const {
|
||||
account,
|
||||
network,
|
||||
tokens,
|
||||
pending,
|
||||
loader,
|
||||
shouldRender,
|
||||
} = props.selectedAccount;
|
||||
|
||||
const { type, title, message } = loader;
|
||||
|
||||
if (!device || !account || !network || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
|
||||
|
||||
const explorerLink: string = `${network.explorer.address}${account.address}`;
|
||||
const pendingAmount: BigNumber = stateUtils.getPendingAmount(pending, network.symbol);
|
||||
const balance: string = new BigNumber(account.balance).minus(pendingAmount).toString(10);
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<React.Fragment>
|
||||
<AccountHeading>
|
||||
<AccountName>
|
||||
<StyledCoinLogo network={account.network} />
|
||||
<AccountTitle>Account #{parseInt(account.index, 10) + 1}</AccountTitle>
|
||||
</AccountName>
|
||||
<Link href={explorerLink} isGray>See full transaction history</Link>
|
||||
</AccountHeading>
|
||||
<AccountBalance
|
||||
network={network}
|
||||
balance={balance}
|
||||
fiat={props.fiat}
|
||||
/>
|
||||
<H2Wrapper>
|
||||
<H2>Tokens</H2>
|
||||
<StyledTooltip
|
||||
maxWidth={200}
|
||||
placement="top"
|
||||
content="Insert token name, symbol or address to be able to send it."
|
||||
>
|
||||
<StyledIcon
|
||||
icon={ICONS.HELP}
|
||||
color={colors.TEXT_SECONDARY}
|
||||
size={24}
|
||||
/>
|
||||
</StyledTooltip>
|
||||
</H2Wrapper>
|
||||
<AsyncSelectWrapper>
|
||||
<AsyncSelect
|
||||
isSearchable
|
||||
defaultOptions
|
||||
value={null}
|
||||
isMulti={false}
|
||||
placeholder="Search for the token"
|
||||
loadingMessage={() => 'Loading...'}
|
||||
noOptionsMessage={() => 'Token not found'}
|
||||
onChange={(token) => {
|
||||
if (token.name) {
|
||||
const isAdded = tokens.find(t => t.symbol === token.symbol);
|
||||
if (!isAdded) {
|
||||
props.addToken(token, account);
|
||||
}
|
||||
}
|
||||
}}
|
||||
loadOptions={input => props.loadTokens(input, account.network)}
|
||||
formatOptionLabel={(option) => {
|
||||
const isAdded = tokens.find(t => t.symbol === option.symbol);
|
||||
if (isAdded) {
|
||||
return `${option.name} (Already added)`;
|
||||
}
|
||||
return option.name;
|
||||
}}
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option.symbol}
|
||||
/>
|
||||
</AsyncSelectWrapper>
|
||||
<AddedTokensWrapper>
|
||||
{ tokens.length < 1 && (<AddTokenMessage />) }
|
||||
{tokens.map(token => (
|
||||
<AddedToken
|
||||
key={token.symbol}
|
||||
token={token}
|
||||
pending={pending}
|
||||
removeToken={props.removeToken}
|
||||
/>
|
||||
))}
|
||||
</AddedTokensWrapper>
|
||||
</React.Fragment>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountSummary;
|
||||
});
|
||||
|
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
@ -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
|
||||
import WalletContainer from 'views/Wallet';
|
||||
import AccountSummary from 'views/Wallet/views/Account/Summary/Container';
|
||||
import AccountSend from 'views/Wallet/views/Account/Send/Container';
|
||||
import AccountReceive from 'views/Wallet/views/Account/Receive/Container';
|
||||
import AccountSummary from 'views/Wallet/views/Account/Summary';
|
||||
import AccountSend from 'views/Wallet/views/Account/Send';
|
||||
import AccountReceive from 'views/Wallet/views/Account/Receive';
|
||||
import AccountSignVerify from 'views/Wallet/views/Account/SignVerify/Container';
|
||||
|
||||
import WalletDashboard from 'views/Wallet/views/Dashboard';
|
||||
|
@ -50,7 +50,7 @@ module.exports = {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js?$/,
|
||||
exclude: /node_modules/,
|
||||
exclude: [/node_modules/, /trezor-blockchain-link\/build\/workers/],
|
||||
use: ['babel-loader'],
|
||||
},
|
||||
{
|
||||
|
@ -11896,10 +11896,9 @@ tr46@^1.0.1:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
trezor-connect@6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-6.0.2.tgz#a4ca892cc4a167b34b97644e1404a56f6a110379"
|
||||
integrity sha512-oSYTPnQD9ZT3vO2hBRdgealHG7t8Wu3oTZXX/U4eRxJZ0WqdEcXG7Nvqe1BL6Rl3VTuj7ALT9DL1Uq3QFYAc3g==
|
||||
trezor-connect@6.0.3-beta.4:
|
||||
version "6.0.3-beta.4"
|
||||
resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-6.0.3-beta.4.tgz#c0b3dfe809756b455e5b5b34fb2a104846da4f72"
|
||||
dependencies:
|
||||
babel-runtime "^6.26.0"
|
||||
events "^1.1.1"
|
||||
|