1
0
mirror of https://github.com/trezor/trezor-wallet synced 2025-03-03 09:46:06 +00:00

Merge pull request #282 from trezor/fix/pending-txs

Fix/pending txs
This commit is contained in:
Vladimir Volek 2018-12-20 16:25:03 +01:00 committed by GitHub
commit cf76e8cf68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1071 additions and 1955 deletions

View File

@ -69,7 +69,7 @@
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"styled-components": "^4.1.2", "styled-components": "^4.1.2",
"styled-normalize": "^8.0.4", "styled-normalize": "^8.0.4",
"trezor-connect": "6.0.3-beta.4", "trezor-connect": "6.0.3-beta.5",
"web3": "1.0.0-beta.35", "web3": "1.0.0-beta.35",
"webpack": "^4.16.3", "webpack": "^4.16.3",
"webpack-build-notifier": "^0.1.29", "webpack-build-notifier": "^0.1.29",

View File

@ -1,17 +1,5 @@
{ {
"networks": [ "networks": [
{
"type": "ripple",
"name": "Ripple Testnet",
"testnet": true,
"symbol": "XRP",
"shortcut": "xrp",
"bip44": "m/44'/144'/a'/0/0",
"explorer": {
"tx": "https://sisyfos.trezor.io/ripple-testnet-explorer/tx/",
"address": "https://sisyfos.trezor.io/ripple-testnet-explorer/address/"
}
},
{ {
"type": "ethereum", "type": "ethereum",
"name": "Ethereum", "name": "Ethereum",
@ -22,6 +10,7 @@
"defaultGasPrice": 64, "defaultGasPrice": 64,
"defaultGasLimit": 21000, "defaultGasLimit": 21000,
"defaultGasLimitTokens": 200000, "defaultGasLimitTokens": 200000,
"decimals": 18,
"tokens": "./data/ethereumTokens.json", "tokens": "./data/ethereumTokens.json",
"web3": [ "web3": [
"wss://eth2.trezor.io/geth" "wss://eth2.trezor.io/geth"
@ -41,6 +30,7 @@
"defaultGasPrice": 64, "defaultGasPrice": 64,
"defaultGasLimit": 21000, "defaultGasLimit": 21000,
"defaultGasLimitTokens": 200000, "defaultGasLimitTokens": 200000,
"decimals": 18,
"tokens": "./data/ethereumClassicTokens.json", "tokens": "./data/ethereumClassicTokens.json",
"web3": [ "web3": [
"wss://etc2.trezor.io/geth" "wss://etc2.trezor.io/geth"
@ -61,6 +51,7 @@
"defaultGasPrice": 64, "defaultGasPrice": 64,
"defaultGasLimit": 21000, "defaultGasLimit": 21000,
"defaultGasLimitTokens": 200000, "defaultGasLimitTokens": 200000,
"decimals": 18,
"tokens": "./data/ropstenTokens.json", "tokens": "./data/ropstenTokens.json",
"web3": [ "web3": [
"wss://ropsten1.trezor.io/geth" "wss://ropsten1.trezor.io/geth"
@ -69,6 +60,31 @@
"tx": "https://ropsten.etherscan.io/tx/", "tx": "https://ropsten.etherscan.io/tx/",
"address": "https://ropsten.etherscan.io/address/" "address": "https://ropsten.etherscan.io/address/"
} }
},
{
"type": "ripple",
"name": "Ripple",
"symbol": "XRP",
"shortcut": "xrp",
"bip44": "m/44'/144'/a'/0/0",
"decimals": 6,
"explorer": {
"tx": "https://xrpcharts.ripple.com/#/transactions/",
"address": "https://xrpcharts.ripple.com/#/graph/"
}
},
{
"type": "ripple",
"name": "Ripple Testnet",
"testnet": true,
"symbol": "tXRP",
"shortcut": "txrp",
"bip44": "m/44'/144'/a'/0/0",
"decimals": 6,
"explorer": {
"tx": "https://sisyfos.trezor.io/ripple-testnet-explorer/tx/",
"address": "https://sisyfos.trezor.io/ripple-testnet-explorer/address/"
}
} }
], ],

View File

@ -90,7 +90,7 @@ export const onNotification = (payload: $ElementType<BlockchainNotification, 'pa
switch (network.type) { switch (network.type) {
case 'ethereum': case 'ethereum':
// this is not working until blockchain-link will start support blockbook backends // this is not working until blockchain-link will start support blockbook backends
await dispatch(EthereumBlockchainActions.onNotification()); await dispatch(EthereumBlockchainActions.onNotification(payload));
break; break;
case 'ripple': case 'ripple':
await dispatch(RippleBlockchainActions.onNotification(payload)); await dispatch(RippleBlockchainActions.onNotification(payload));

View File

@ -15,7 +15,6 @@ import * as storageUtils from 'utils/storage';
import { findAccountTokens } from 'reducers/TokensReducer'; import { findAccountTokens } from 'reducers/TokensReducer';
import type { Account } from 'reducers/AccountsReducer'; import type { Account } from 'reducers/AccountsReducer';
import type { Token } from 'reducers/TokensReducer'; import type { Token } from 'reducers/TokensReducer';
import type { PendingTx } from 'reducers/PendingTxReducer';
import type { Discovery } from 'reducers/DiscoveryReducer'; import type { Discovery } from 'reducers/DiscoveryReducer';
import type { import type {
@ -24,6 +23,7 @@ import type {
AsyncAction, AsyncAction,
GetState, GetState,
Dispatch, Dispatch,
Transaction,
} from 'flowtype'; } from 'flowtype';
import type { Config, Network, TokensCollection } from 'reducers/LocalStorageReducer'; import type { Config, Network, TokensCollection } from 'reducers/LocalStorageReducer';
@ -61,15 +61,15 @@ const findAccounts = (devices: Array<TrezorDevice>, accounts: Array<Account>): A
const findTokens = (accounts: Array<Account>, tokens: Array<Token>): Array<Token> => accounts.reduce((arr, account) => arr.concat(findAccountTokens(tokens, account)), []); const findTokens = (accounts: Array<Account>, tokens: Array<Token>): Array<Token> => accounts.reduce((arr, account) => arr.concat(findAccountTokens(tokens, account)), []);
const findDiscovery = (devices: Array<TrezorDevice>, discovery: Array<Discovery>): Array<Discovery> => devices.reduce((arr, dev) => arr.concat(discovery.filter(a => a.deviceState === dev.state && a.publicKey.length > 0)), []); const findDiscovery = (devices: Array<TrezorDevice>, discovery: Array<Discovery>): Array<Discovery> => devices.reduce((arr, dev) => arr.concat(discovery.filter(d => d.deviceState === dev.state && d.completed)), []);
const findPendingTxs = (accounts: Array<Account>, pending: Array<PendingTx>): Array<PendingTx> => accounts.reduce((result, account) => result.concat(pending.filter(p => p.address === account.address && p.network === account.network)), []); const findPendingTxs = (accounts: Array<Account>, pending: Array<Transaction>): Array<Transaction> => accounts.reduce((result, account) => result.concat(pending.filter(p => p.address === account.address && p.network === account.network)), []);
export const save = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const save = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const devices: Array<TrezorDevice> = getState().devices.filter(d => d.features && d.remember === true); const devices: Array<TrezorDevice> = getState().devices.filter(d => d.features && d.remember === true);
const accounts: Array<Account> = findAccounts(devices, getState().accounts); const accounts: Array<Account> = findAccounts(devices, getState().accounts);
const tokens: Array<Token> = findTokens(accounts, getState().tokens); const tokens: Array<Token> = findTokens(accounts, getState().tokens);
const pending: Array<PendingTx> = findPendingTxs(accounts, getState().pending); const pending: Array<Transaction> = findPendingTxs(accounts, getState().pending);
const discovery: Array<Discovery> = findDiscovery(devices, getState().discovery); const discovery: Array<Discovery> = findDiscovery(devices, getState().discovery);
// save devices // save devices

View File

@ -3,14 +3,15 @@
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import type { State, PendingTx } from 'reducers/PendingTxReducer'; import type { Transaction } from 'flowtype';
import type { State } from 'reducers/PendingTxReducer';
export type PendingTxAction = { export type PendingTxAction = {
type: typeof PENDING.FROM_STORAGE, type: typeof PENDING.FROM_STORAGE,
payload: State payload: State
} | { } | {
type: typeof PENDING.ADD, type: typeof PENDING.ADD,
payload: PendingTx payload: Transaction
} | { } | {
type: typeof PENDING.TX_RESOLVED, type: typeof PENDING.TX_RESOLVED,
hash: string, hash: string,

View File

@ -289,9 +289,7 @@ export const estimateGasLimit = (network: string, $options: EstimateGasOptions):
return limit.toString(); return limit.toString();
}; };
export const disconnect = (coinInfo: any): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const disconnect = (network: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
// incoming "coinInfo" from TrezorConnect is CoinInfo | EthereumNetwork type
const network: string = coinInfo.shortcut.toLowerCase();
// check if Web3 was already initialized // check if Web3 was already initialized
const instance = getState().web3.find(w3 => w3.network === network); const instance = getState().web3.find(w3 => w3.network === network);
if (instance) { if (instance) {

View File

@ -2,6 +2,7 @@
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import * as PENDING from 'actions/constants/pendingTx';
import type { import type {
TrezorDevice, TrezorDevice,
@ -9,7 +10,7 @@ import type {
GetState, GetState,
PromiseAction, PromiseAction,
} from 'flowtype'; } from 'flowtype';
import type { EthereumAccount } from 'trezor-connect'; import type { EthereumAccount, BlockchainNotification } from 'trezor-connect';
import type { Token } from 'reducers/TokensReducer'; import type { Token } from 'reducers/TokensReducer';
import type { NetworkToken } from 'reducers/LocalStorageReducer'; import type { NetworkToken } from 'reducers/LocalStorageReducer';
import * as Web3Actions from 'actions/Web3Actions'; import * as Web3Actions from 'actions/Web3Actions';
@ -85,10 +86,11 @@ export const estimateGasLimit = (network: string, data: string, value: string, g
export const subscribe = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { 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 const accounts: Array<string> = getState().accounts.filter(a => a.network === network).map(a => a.address); // eslint-disable-line no-unused-vars
await TrezorConnect.blockchainSubscribe({ const response = await TrezorConnect.blockchainSubscribe({
accounts, accounts,
coin: network, coin: network,
}); });
if (!response.success) return;
// init web3 instance if not exists // init web3 instance if not exists
await dispatch(Web3Actions.initWeb3(network)); await dispatch(Web3Actions.initWeb3(network));
}; };
@ -125,45 +127,21 @@ export const onBlockMined = (network: string): PromiseAction<void> => async (dis
} }
}; };
export const onNotification = (/*network: string*/): PromiseAction<void> => async (): Promise<void> => { export const onNotification = (payload: $ElementType<BlockchainNotification, 'payload'>): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
// todo: get transaction history here const { notification } = payload;
// console.warn("OnBlAccount", account); const account = getState().accounts.find(a => a.address === notification.address);
// this event can be triggered multiple times if (!account) return;
// // 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); if (notification.status === 'pending') {
// const exists = getState().pending.filter(p => p.address === address); dispatch({
// if (exists.length < 1) { type: PENDING.ADD,
// if (txInfo) { payload: {
// dispatch({ ...notification,
// type: PENDING.ADD, deviceState: account.deviceState,
// payload: { network: account.network,
// 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> => { export const onError = (network: string): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {

View File

@ -6,7 +6,6 @@ import BigNumber from 'bignumber.js';
import * as ACCOUNT from 'actions/constants/account'; import * as ACCOUNT from 'actions/constants/account';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import * as SEND from 'actions/constants/send'; import * as SEND from 'actions/constants/send';
import * as PENDING from 'actions/constants/pendingTx';
import * as WEB3 from 'actions/constants/web3'; import * as WEB3 from 'actions/constants/web3';
import { initialState } from 'reducers/SendFormEthereumReducer'; import { initialState } from 'reducers/SendFormEthereumReducer';
import { findToken } from 'reducers/TokensReducer'; import { findToken } from 'reducers/TokensReducer';
@ -525,21 +524,45 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
dispatch({ type: SEND.TX_COMPLETE }); dispatch({ type: SEND.TX_COMPLETE });
dispatch({ // ugly blockbook workaround:
type: PENDING.ADD, // since blockbook can't emit pending notifications
payload: { // need to trigger this event from here, where we know everything about this transaction
// blockchainNotification is 'trezor-connect' BlockchainLinkTransaction type
const fee = ValidationActions.calculateFee(currentState.gasLimit, currentState.gasPrice);
const blockchainNotification = {
type: 'send', type: 'send',
deviceState: account.deviceState, status: 'pending',
sequence: nonce, confirmations: 0,
hash: txid,
network: account.network,
address: account.address, address: account.address,
currency: currentState.currency, inputs: [
{
addresses: [account.address],
amount: currentState.amount, amount: currentState.amount,
fee,
total: currentState.total, total: currentState.total,
fee: '0', // TODO: calculate fee
}, },
}); ],
outputs: [
{
addresses: [currentState.address],
amount: currentState.amount,
},
],
hash: txid,
amount: currentState.amount,
fee,
total: currentState.total,
sequence: nonce,
currency: isToken ? currentState.currency : undefined,
};
dispatch(BlockchainActions.onNotification({
// $FlowIssue: missing coinInfo declaration
coin: {},
notification: blockchainNotification,
}));
// workaround end
// clear session storage // clear session storage
dispatch(SessionStorageActions.clear()); dispatch(SessionStorageActions.clear());

View File

@ -1,7 +1,7 @@
/* @flow */ /* @flow */
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import * as BLOCKCHAIN from 'actions/constants/blockchain'; // import * as BLOCKCHAIN from 'actions/constants/blockchain';
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import * as AccountsActions from 'actions/AccountsActions'; import * as AccountsActions from 'actions/AccountsActions';
import { toDecimalAmount } from 'utils/formatUtils'; import { toDecimalAmount } from 'utils/formatUtils';
@ -13,6 +13,7 @@ import type {
PromiseAction, PromiseAction,
} from 'flowtype'; } from 'flowtype';
const DECIMALS: number = 6;
export const subscribe = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { 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); const accounts: Array<string> = getState().accounts.filter(a => a.network === network).map(a => a.address);
@ -22,22 +23,47 @@ export const subscribe = (network: string): PromiseAction<void> => async (dispat
}); });
}; };
export const onBlockMined = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { 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); const blockchain = getState().blockchain.find(b => b.shortcut === network);
if (!blockchain) return; if (!blockchain) return;
if (fee.payload !== blockchain.fee) { // const fee = await TrezorConnect.blockchainGetFee({
dispatch({ // coin: network,
type: BLOCKCHAIN.UPDATE_FEE, // });
shortcut: network, // if (!fee.success) return;
fee: fee.payload,
}); // if (fee.payload !== blockchain.fee) {
// dispatch({
// type: BLOCKCHAIN.UPDATE_FEE,
// shortcut: network,
// fee: fee.payload,
// });
// }
const accounts: Array<any> = getState().accounts.filter(a => a.network === network);
// console.warn('ACCOUNTS', accounts);
if (accounts.length > 0) {
// const response = await TrezorConnect.rippleGetAccountInfo({
// bundle: accounts,
// level: 'transactions',
// coin: network,
// });
// console.warn('APDEJT RESP', response);
// if (!response.success) return;
// response.payload.forEach((a, i) => {
// if (a.transactions.length > 0) {
// console.warn('APDEJTED!', a, i);
// dispatch(AccountsActions.update({
// ...accounts[i],
// balance: toDecimalAmount(a.balance, DECIMALS),
// availableBalance: toDecimalAmount(a.availableBalance, DECIMALS),
// block: a.block,
// sequence: a.sequence,
// }));
// }
// });
} }
}; };
@ -50,16 +76,13 @@ export const onNotification = (payload: $ElementType<BlockchainNotification, 'pa
dispatch({ dispatch({
type: PENDING.ADD, type: PENDING.ADD,
payload: { payload: {
type: notification.type, ...notification,
deviceState: account.deviceState, deviceState: account.deviceState,
sequence: account.sequence,
hash: notification.hash,
network: account.network, network: account.network,
address: account.address,
currency: account.network, amount: toDecimalAmount(notification.amount, DECIMALS),
amount: notification.amount, total: notification.type === 'send' ? toDecimalAmount(notification.total, DECIMALS) : toDecimalAmount(notification.amount, DECIMALS),
total: notification.amount, fee: toDecimalAmount(notification.fee, DECIMALS),
fee: notification.fee,
}, },
}); });
@ -74,16 +97,17 @@ export const onNotification = (payload: $ElementType<BlockchainNotification, 'pa
const updatedAccount = await TrezorConnect.rippleGetAccountInfo({ const updatedAccount = await TrezorConnect.rippleGetAccountInfo({
account: { account: {
address: account.address, address: account.address,
block: account.block, from: account.block,
history: false, history: false,
}, },
coin: account.network,
}); });
if (!updatedAccount.success) return; if (!updatedAccount.success) return;
dispatch(AccountsActions.update({ dispatch(AccountsActions.update({
...account, ...account,
balance: toDecimalAmount(updatedAccount.payload.balance, 6), balance: toDecimalAmount(updatedAccount.payload.balance, DECIMALS),
availableDevice: toDecimalAmount(updatedAccount.payload.availableBalance, 6), availableBalance: toDecimalAmount(updatedAccount.payload.availableBalance, DECIMALS),
block: updatedAccount.payload.block, block: updatedAccount.payload.block,
sequence: updatedAccount.payload.sequence, sequence: updatedAccount.payload.sequence,
})); }));

View File

@ -48,6 +48,7 @@ export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discover
}, },
keepSession: true, // acquire and hold session keepSession: true, // acquire and hold session
useEmptyPassphrase: device.useEmptyPassphrase, useEmptyPassphrase: device.useEmptyPassphrase,
coin: network.shortcut,
}); });
// handle TREZOR response error // handle TREZOR response error

View File

@ -1,6 +1,4 @@
/* @flow */ /* @flow */
import React from 'react';
import Link from 'components/Link';
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import * as SEND from 'actions/constants/send'; import * as SEND from 'actions/constants/send';
@ -250,7 +248,7 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
payload: { payload: {
type: 'success', type: 'success',
title: 'Transaction success', title: 'Transaction success',
message: <Link href={`${network.explorer.tx}${txid}`} isGreen>See transaction detail</Link>, message: txid,
cancelable: true, cancelable: true,
actions: [], actions: [],
}, },

View File

@ -0,0 +1,104 @@
/* @flow */
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import colors from 'config/colors';
import Link from 'components/Link';
import type { Transaction, Network } from 'flowtype';
type Props = {
tx: Transaction,
network: Network,
};
const Wrapper = styled.div`
border-bottom: 1px solid ${colors.DIVIDER};
padding: 14px 0;
display: flex;
flex-direction: row;
&:last-child {
border-bottom: 0px;
}
`;
const Addresses = styled.div`
flex: 1;
`;
const Address = styled.div`
word-break: break-all;
padding: 2px 0px;
&:first-child {
padding-top: 0px;
}
&:last-child {
padding-bottom: 0px;
}
`;
const Date = styled(Link)`
font-size: 12px;
line-height: 18px;
padding-right: 8px;
border-bottom: 0px;
`;
const Value = styled.div`
padding-left: 8px;
white-space: nowrap;
text-align: right;
color: ${colors.GREEN_SECONDARY};
&.send {
color: ${colors.ERROR_PRIMARY};
}
`;
const Amount = styled.div`
border: 1px;
`;
const Fee = styled.div`
border: 1px;
`;
const TransactionItem = ({
tx,
network,
}: Props) => {
const url = `${network.explorer.tx}${tx.hash}`;
const date = typeof tx.timestamp === 'string' && tx.confirmations > 0 ? tx.timestamp : undefined; // TODO: format date
const addresses = (tx.type === 'send' ? tx.outputs : tx.inputs).reduce((arr, item) => arr.concat(item.addresses), []);
const currency = tx.currency || tx.network;
const isToken = currency !== tx.network;
const amount = isToken ? `${tx.amount} ${currency}` : `${tx.total} ${network.symbol}`;
const fee = isToken && tx.type === 'send' ? `${tx.fee} ${network.symbol}` : undefined;
const operation = tx.type === 'send' ? '-' : '+';
return (
<Wrapper>
{ date && (<Date href={url} isGray>{ date }</Date>)}
<Addresses>
{ addresses.map(addr => (<Address key={addr}>{addr}</Address>)) }
{ tx.confirmations <= 0 && (
<Date href={url} isGray>Transaction hash: {tx.hash}</Date>
)}
</Addresses>
<Value className={tx.type}>
<Amount>{operation}{amount}</Amount>
{ fee && (<Fee>{operation}{fee}</Fee>) }
</Value>
</Wrapper>
);
};
TransactionItem.propTypes = {
tx: PropTypes.object.isRequired,
network: PropTypes.object.isRequired,
};
export default TransactionItem;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -44,6 +44,7 @@ import type {
TransportMessageType, TransportMessageType,
UiMessageType, UiMessageType,
BlockchainEvent, BlockchainEvent,
BlockchainLinkTransaction,
} from 'trezor-connect'; } from 'trezor-connect';
import type { RouterAction, LocationState } from 'react-router-redux'; import type { RouterAction, LocationState } from 'react-router-redux';
@ -84,11 +85,17 @@ export type UnknownDevice = $Exact<{
instanceLabel: string; instanceLabel: string;
instanceName: ?string; instanceName: ?string;
ts: number; ts: number;
}> }>;
export type { Device } from 'trezor-connect'; export type { Device } from 'trezor-connect';
export type TrezorDevice = AcquiredDevice | UnknownDevice; export type TrezorDevice = AcquiredDevice | UnknownDevice;
export type Transaction = BlockchainLinkTransaction & {
deviceState: string,
network: string,
rejected?: boolean,
};
export type RouterLocationState = LocationState; export type RouterLocationState = LocationState;
// Cast event from TrezorConnect event listener to react Action // Cast event from TrezorConnect event listener to react Action
@ -152,7 +159,6 @@ export type { Account } from 'reducers/AccountsReducer';
export type { Discovery } from 'reducers/DiscoveryReducer'; export type { Discovery } from 'reducers/DiscoveryReducer';
export type { Token } from 'reducers/TokensReducer'; export type { Token } from 'reducers/TokensReducer';
export type { Web3Instance } from 'reducers/Web3Reducer'; export type { Web3Instance } from 'reducers/Web3Reducer';
export type { PendingTx } from 'reducers/PendingTxReducer';
export type Accounts = $ElementType<State, 'accounts'>; export type Accounts = $ElementType<State, 'accounts'>;
export type LocalStorage = $ElementType<State, 'localStorage'>; export type LocalStorage = $ElementType<State, 'localStorage'>;

View File

@ -9,8 +9,9 @@ import type { BlockchainConnect, BlockchainError, BlockchainBlock } from 'trezor
export type BlockchainNetwork = { export type BlockchainNetwork = {
+shortcut: string, +shortcut: string,
connected: boolean, connected: boolean,
fee: string,
block: number, block: number,
reserved: string, // xrp specific
fee: string,
}; };
export type State = Array<BlockchainNetwork>; export type State = Array<BlockchainNetwork>;
@ -34,8 +35,9 @@ const onConnect = (state: State, action: BlockchainConnect): State => {
return state.concat([{ return state.concat([{
shortcut, shortcut,
connected: true, connected: true,
fee: info.fee,
block: info.block, block: info.block,
fee: info.fee,
reserved: info.reserved || '0',
}]); }]);
}; };
@ -50,7 +52,13 @@ const onError = (state: State, action: BlockchainError): State => {
}]); }]);
} }
return state; return state.concat([{
shortcut,
connected: false,
block: 0,
fee: '0',
reserved: '0',
}]);
}; };
const onBlock = (state: State, action: BlockchainBlock): State => { const onBlock = (state: State, action: BlockchainBlock): State => {

View File

@ -195,6 +195,9 @@ export default function discovery(state: State = initialState, action: Action):
return notSupported(state, action); return notSupported(state, action);
case DISCOVERY.FROM_STORAGE: case DISCOVERY.FROM_STORAGE:
return action.payload.map((d) => { return action.payload.map((d) => {
if (d.publicKey.length < 1) return d;
// recreate ethereum discovery HDKey
// deprecated: will be removed after switching to blockbook
const hdKey: HDKey = new HDKey(); const hdKey: HDKey = new HDKey();
hdKey.publicKey = Buffer.from(d.publicKey, 'hex'); hdKey.publicKey = Buffer.from(d.publicKey, 'hex');
hdKey.chainCode = Buffer.from(d.chainCode, 'hex'); hdKey.chainCode = Buffer.from(d.chainCode, 'hex');

View File

@ -21,6 +21,7 @@ export type Network = {
address: string; address: string;
}; };
tokens: string; tokens: string;
decimals: number,
backends: Array<{ backends: Array<{
name: string; name: string;
urls: Array<string>; urls: Array<string>;

View File

@ -2,27 +2,13 @@
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import type { Action } from 'flowtype'; import type { Action, Transaction } from 'flowtype';
export type PendingTx = { export type State = Array<Transaction>;
+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 initialState: State = [];
const add = (state: State, payload: PendingTx): State => { const add = (state: State, payload: Transaction): State => {
const newState = [...state]; const newState = [...state];
newState.push(payload); newState.push(payload);
return newState; return newState;

View File

@ -6,7 +6,7 @@ import type {
Account, Account,
Network, Network,
Token, Token,
PendingTx, Transaction,
Discovery, Discovery,
} from 'flowtype'; } from 'flowtype';
@ -34,7 +34,7 @@ export type State = {
account: ?Account, account: ?Account,
network: ?Network, network: ?Network,
tokens: Array<Token>, tokens: Array<Token>,
pending: Array<PendingTx>, pending: Array<Transaction>,
discovery: ?Discovery, discovery: ?Discovery,
loader: ?Loader, loader: ?Loader,
notification: ?Notification, notification: ?Notification,

View File

@ -9,7 +9,7 @@ import type {
Network, Network,
Discovery, Discovery,
Token, Token,
PendingTx, Transaction,
Web3Instance, Web3Instance,
} from 'flowtype'; } from 'flowtype';
@ -83,18 +83,18 @@ export const getDiscoveryProcess = (state: State): ?Discovery => {
return state.discovery.find(d => d.deviceState === device.state && d.network === locationState.network); return state.discovery.find(d => d.deviceState === device.state && d.network === locationState.network);
}; };
export const getAccountPendingTx = (pending: Array<PendingTx>, account: ?Account): Array<PendingTx> => { export const getAccountPendingTx = (pending: Array<Transaction>, account: ?Account): Array<Transaction> => {
const a = account; const a = account;
if (!a) return []; if (!a) return [];
return pending.filter(p => p.network === a.network && p.address === a.address); return pending.filter(p => p.network === a.network && p.address === a.address);
}; };
export const getPendingSequence = (pending: Array<PendingTx>): number => pending.reduce((value: number, tx: PendingTx) => { export const getPendingSequence = (pending: Array<Transaction>): number => pending.reduce((value: number, tx: Transaction) => {
if (tx.rejected) return value; if (tx.rejected) return value;
return Math.max(value, tx.sequence + 1); return Math.max(value, tx.sequence + 1);
}, 0); }, 0);
export const getPendingAmount = (pending: Array<PendingTx>, currency: string, token: boolean = false): BigNumber => pending.reduce((value: BigNumber, tx: PendingTx) => { export const getPendingAmount = (pending: Array<Transaction>, currency: string, token: boolean = false): BigNumber => pending.reduce((value: BigNumber, tx: Transaction) => {
if (tx.currency === currency && !tx.rejected) { if (tx.currency === currency && !tx.rejected) {
return new BigNumber(value).plus(token ? tx.amount : tx.total); return new BigNumber(value).plus(token ? tx.amount : tx.total);
} }

View File

@ -1,19 +1,17 @@
/* @flow */ /* @flow */
import React, { PureComponent } from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
import ColorHash from 'color-hash';
import { H2 } from 'components/Heading'; import { H2 } from 'components/Heading';
import Link from 'components/Link'; import Transaction from 'components/Transaction';
import ScaleText from 'react-scale-text';
import type { Network } from 'reducers/LocalStorageReducer'; import type { Network } from 'reducers/LocalStorageReducer';
import type { Token } from 'reducers/TokensReducer';
import type { BaseProps } from '../../index'; import type { BaseProps } from '../../index';
// import testData from './test.data';
type Props = { type Props = {
pending: $PropertyType<$ElementType<BaseProps, 'selectedAccount'>, 'pending'>, pending: $PropertyType<$ElementType<BaseProps, 'selectedAccount'>, 'pending'>,
tokens: $PropertyType<$ElementType<BaseProps, 'selectedAccount'>, 'tokens'>,
network: Network network: Network
} }
@ -22,153 +20,18 @@ const Wrapper = styled.div`
border-top: 1px solid ${colors.DIVIDER}; border-top: 1px solid ${colors.DIVIDER};
`; `;
const StyledLink = styled(Link)` const PendingTransactions = (props: Props) => {
text-decoration: none; // const pending = props.pending.filter(tx => !tx.rejected).concat(testData);
`; const pending = props.pending.filter(tx => !tx.rejected);
const TransactionWrapper = styled.div`
border-bottom: 1px solid ${colors.DIVIDER};
padding: 14px 0;
display: flex;
flex-direction: row;
align-items: center;
&:last-child {
border-bottom: 0px;
}
`;
const TransactionIcon = styled.div`
padding: 6px;
margin-right: 10px;
width: 36px;
height: 36px;
border-radius: 50%;
line-height: 25px;
text-transform: uppercase;
user-select: none;
text-align: center;
color: ${props => props.textColor};
background: ${props => props.background};
border-color: ${props => props.borderColor};
&:before {
content: ${props => props.color};
}
`;
const P = styled.p``;
const TransactionName = styled.div`
flex: 1;
`;
const TransactionAmount = styled.div`
color: colors.TEXT_SECONDARY;
`;
class PendingTransactions extends PureComponent<Props> {
getPendingTransactions() {
return this.props.pending.filter(tx => !tx.rejected);
}
getTransactionIconColors(tx: any) {
let iconColors = {
textColor: '#fff',
background: '#000',
borderColor: '#000',
};
const bgColorFactory = new ColorHash({ lightness: 0.7 });
const textColorFactory = new ColorHash();
const isSmartContractTx = tx.currency !== this.props.network.symbol;
if (isSmartContractTx) {
const token: ?Token = this.findTransactionToken(tx.currency);
if (!token) {
iconColors = {
textColor: '#ffffff',
background: '#000000',
borderColor: '#000000',
};
} else {
const bgColor: string = bgColorFactory.hex(token.name);
iconColors = {
textColor: textColorFactory.hex(token.name),
background: bgColor,
borderColor: bgColor,
};
}
} else {
iconColors = {
textColor: textColorFactory.hex(tx.network),
background: bgColorFactory.hex(tx.network),
borderColor: bgColorFactory.hex(tx.network),
};
}
return iconColors;
}
getTransactionSymbol(tx: any) {
let { symbol } = this.props.network;
const isSmartContractTx = tx.currency !== this.props.network.symbol;
if (isSmartContractTx) {
const token: ?Token = this.findTransactionToken(tx.currency);
symbol = token ? token.symbol.toUpperCase() : 'Unknown';
}
return symbol;
}
getTransactionName(tx: any) {
let { name } = this.props.network;
const isSmartContractTx = tx.currency !== this.props.network.symbol;
if (isSmartContractTx) {
const token: ?Token = this.findTransactionToken(tx.currency);
name = token ? token.symbol.toUpperCase() : 'Unknown';
}
return name;
}
findTransactionToken(transactionCurrency: string) {
return this.props.tokens.find(t => t.symbol === transactionCurrency);
}
render() {
return ( return (
<Wrapper> <Wrapper>
<H2>Pending transactions</H2> <H2>Pending transactions</H2>
{this.getPendingTransactions().map(tx => ( {pending.map(tx => (
<TransactionWrapper <Transaction key={tx.hash} network={props.network} tx={tx} />
key={tx.hash}
>
<TransactionIcon
textColor={() => this.getTransactionIconColors(tx).textColor}
background={() => this.getTransactionIconColors(tx).background}
borderColor={() => this.getTransactionIconColors(tx).borderColor}
>
<ScaleText widthOnly>
<P>{this.getTransactionSymbol(tx)}</P>
</ScaleText>
</TransactionIcon>
<TransactionName>
<StyledLink
href={`${this.props.network.explorer.tx}${tx.hash}`}
isGray
>
{this.getTransactionName(tx)}
</StyledLink>
</TransactionName>
<TransactionAmount>
{tx.currency !== this.props.network.symbol ? tx.amount : tx.total} {this.getTransactionSymbol(tx)}
</TransactionAmount>
</TransactionWrapper>
))} ))}
</Wrapper> </Wrapper>
); );
} };
}
export default PendingTransactions; export default PendingTransactions;

View File

@ -0,0 +1,99 @@
export default [
{
type: 'recv',
timestamp: '16:20',
address: 'a',
deviceState: 'a',
status: 'pending',
confirmations: 0,
inputs: [
{
addresses: ['in1'],
},
{
addresses: ['in2'],
},
],
outputs: [
{
addresses: ['out1', 'out2'],
},
],
sequence: 1,
hash: '1234',
network: 'eth',
currency: 'eth',
amount: '0.001',
total: '0.001001',
fee: '0.000001',
},
{
type: 'send',
address: 'a',
deviceState: 'a',
confirmations: 0,
status: 'pending',
inputs: [
{
addresses: ['in1', 'in2'],
},
],
outputs: [
{
addresses: ['out1'],
},
],
sequence: 1,
hash: '12345',
network: 'eth',
currency: 'T01',
amount: '0.001',
total: '0.001001',
fee: '0.000001',
},
{
address: '0x73d0385F4d8E00C5e6504C6030F47BF6212736A8',
amount: '1',
confirmations: 0,
currency: 'T01',
deviceState: '4058d01c7c964787b7d06f0f32ce229088e123a042bf95aad658f1b1b99c73fc',
fee: '0.0002',
hash: '0xbf6ac83bdf29abacbca91cd4100ddd5cd8de16e72911ea7d1daec17ccbfc6099',
inputs: [{
addresses: ['0x73d0385F4d8E00C5e6504C6030F47BF6212736A8'],
}],
network: 'trop',
outputs: [{
addresses: ['0xFA01a39f8Abaeb660c3137f14A310d0b414b2A15'],
}],
sequence: 249,
status: 'pending',
timestamp: '',
total: '0.0002',
type: 'send',
},
{
address: '0x73d0385F4d8E00C5e6504C6030F47BF6212736A8',
amount: '1',
confirmations: 0,
currency: 'trop',
deviceState: '4058d01c7c964787b7d06f0f32ce229088e123a042bf95aad658f1b1b99c73fc',
fee: '0.0002',
hash: '0xbf6ac83bdf29abacbca91cd4100ddd5cd8de16e72911ea7d1daec17ccbfc6099',
inputs: [{
addresses: ['0x73d0385F4d8E00C5e6504C6030F47BF6212736A8'],
}],
network: 'trop',
outputs: [{
addresses: ['0xFA01a39f8Abaeb660c3137f14A310d0b414b2A15'],
}],
sequence: 249,
status: 'pending',
timestamp: '',
total: '0.0002',
type: 'send',
},
];

View File

@ -132,6 +132,7 @@ const ToggleAdvancedSettingsButton = styled(Button)`
const SendButton = styled(Button)` const SendButton = styled(Button)`
min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')}; min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')};
word-break: break-all;
@media screen and (max-width: ${SmallScreenWidth}) { @media screen and (max-width: ${SmallScreenWidth}) {
margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)}; margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)};

View File

@ -82,6 +82,7 @@ const ToggleAdvancedSettingsWrapper = styled.div`
const SendButton = styled(Button)` const SendButton = styled(Button)`
min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')}; min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')};
word-break: break-all;
@media screen and (max-width: ${SmallScreenWidth}) { @media screen and (max-width: ${SmallScreenWidth}) {
margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)}; margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)};

View File

@ -50,7 +50,7 @@ module.exports = {
rules: [ rules: [
{ {
test: /\.js?$/, test: /\.js?$/,
exclude: [/node_modules/, /trezor-blockchain-link\/build\/workers/], exclude: [/node_modules/, /blockchain-link\/build\/workers/],
use: ['babel-loader'], use: ['babel-loader'],
}, },
{ {

2339
yarn.lock

File diff suppressed because it is too large Load Diff