1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-28 03:08:30 +00:00

Merge branch 'master' into unit-tests

This commit is contained in:
Vladimir Volek 2018-09-20 14:39:37 +02:00 committed by GitHub
commit 70d7c73a77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1992 additions and 990 deletions

View File

@ -7,6 +7,7 @@
.*/node_modules/redux/.* .*/node_modules/redux/.*
.*/node_modules/react-router/.* .*/node_modules/react-router/.*
.*/node_modules/react-router-redux/.* .*/node_modules/react-router-redux/.*
.*/node_modules/oboe/test/.*
.*/_old/.* .*/_old/.*
.*/public/.* .*/public/.*
@ -41,4 +42,5 @@ module.name_mapper='^views' -> '<PROJECT_ROOT>/src/views'
module.name_mapper='^data' -> '<PROJECT_ROOT>/src/data' module.name_mapper='^data' -> '<PROJECT_ROOT>/src/data'
module.name_mapper='^services' -> '<PROJECT_ROOT>/src/services' module.name_mapper='^services' -> '<PROJECT_ROOT>/src/services'
module.name_mapper='^support' -> '<PROJECT_ROOT>/src/support' module.name_mapper='^support' -> '<PROJECT_ROOT>/src/support'
module.name_mapper='^public' -> '<PROJECT_ROOT>/public'
module.system=haste module.system=haste

50
LICENSE.md Normal file
View File

@ -0,0 +1,50 @@
# TREZOR REFERENCE SOURCE LICENSE (T-RSL)
This license governs use of the accompanying software. If you use the software,
you accept this license. If you do not accept the license, do not use the
software.
## 1. Definitions
The terms "reproduce," "reproduction" and "distribution" have the same meaning
here as under U.S. copyright law.
"You" means the licensee of the software.
"Your company" means the company you worked for when you downloaded the
software.
"Reference use" means use of the software within your company as a reference,
in read only form, for the sole purposes of debugging your products,
maintaining your products, or enhancing the interoperability of your products
with the software, and specifically excludes the right to distribute the
software outside of your company.
"Licensed patents" means any Licensor patent claims which read directly on the
software as distributed by the Licensor under this license.
## 2. Grant of Rights
(A) Copyright Grant - Subject to the terms of this license, the Licensor grants
you a non-transferable, non-exclusive, worldwide, royalty-free copyright
license to reproduce the software for reference use.
(B) Patent Grant - Subject to the terms of this license, the Licensor grants
you a non-transferable, non-exclusive, worldwide, royalty-free patent license
under licensed patents for reference use.
## 3. Limitations
(A) No Trademark License - This license does not grant you any rights to use
the Licensor's name, logo, or trademarks.
(B) If you begin patent litigation against the Licensor over patents that you
think may apply to the software (including a cross-claim or counterclaim in
a lawsuit), your license to the software ends automatically.
(C) The software is licensed "as-is." You bear the risk of using it. The
Licensor gives no express warranties, guarantees or conditions. You may have
additional consumer rights under your local laws which this license cannot
change. To the extent permitted under your local laws, the Licensor excludes
the implied warranties of merchantability, fitness for a particular purpose and
non-infringement.

View File

@ -49,6 +49,7 @@
"react-router-redux": "next", "react-router-redux": "next",
"react-scale-text": "^1.2.2", "react-scale-text": "^1.2.2",
"react-select": "2.0.0", "react-select": "2.0.0",
"react-sticky-el": "^1.0.20",
"react-transition-group": "^2.2.1", "react-transition-group": "^2.2.1",
"redbox-react": "^1.6.0", "redbox-react": "^1.6.0",
"redux": "4.0.0", "redux": "4.0.0",
@ -58,8 +59,8 @@
"styled-components": "^3.3.3", "styled-components": "^3.3.3",
"styled-media-query": "^2.0.2", "styled-media-query": "^2.0.2",
"styled-normalize": "^8.0.0", "styled-normalize": "^8.0.0",
"trezor-connect": "5.0.30", "trezor-connect": "^5.0.32",
"web3": "^0.19.0", "web3": "1.0.0-beta.35",
"webpack": "^4.16.3", "webpack": "^4.16.3",
"webpack-bundle-analyzer": "^2.13.1", "webpack-bundle-analyzer": "^2.13.1",
"whatwg-fetch": "^2.0.4", "whatwg-fetch": "^2.0.4",
@ -91,8 +92,6 @@
"file-loader": "1.1.11", "file-loader": "1.1.11",
"flow-bin": "0.72.0", "flow-bin": "0.72.0",
"jest": "^23.4.2", "jest": "^23.4.2",
"less": "^3.0.1",
"less-loader": "4.1.0",
"stylelint": "^8.0.0", "stylelint": "^8.0.0",
"stylelint-config-standard": "^18.2.0", "stylelint-config-standard": "^18.2.0",
"stylelint-config-styled-components": "^0.1.1", "stylelint-config-styled-components": "^0.1.1",

View File

@ -3,21 +3,15 @@
{ {
"name": "Ethereum", "name": "Ethereum",
"symbol": "ETH", "symbol": "ETH",
"network": "ethereum", "network": "eth",
"bip44": "m/44'/60'/0'/0", "bip44": "m/44'/60'/0'/0",
"chainId": 1, "chainId": 1,
"defaultGasPrice": 64, "defaultGasPrice": 64,
"defaultGasLimit": 21000, "defaultGasLimit": 21000,
"defaultGasLimitTokens": 200000, "defaultGasLimitTokens": 200000,
"tokens": "./data/ethereumTokens.json", "tokens": "./data/ethereumTokens.json",
"backends": [ "web3": [
{ "wss://eth2.trezor.io/geth"
"name": "TREZOR Wallet - Ethereum",
"urls": [
"https://mainnet.infura.io/QGyVKozSUEh2YhL4s2G4",
"http://88.208.115.69"
]
}
], ],
"explorer": { "explorer": {
"tx": "https://etherscan.io/tx/", "tx": "https://etherscan.io/tx/",
@ -27,21 +21,15 @@
{ {
"name": "Ethereum Classic", "name": "Ethereum Classic",
"symbol": "ETC", "symbol": "ETC",
"network": "ethereum-classic", "network": "etc",
"chainId": 61, "chainId": 61,
"bip44": "m/44'/61'/0'/0", "bip44": "m/44'/61'/0'/0",
"defaultGasPrice": 64, "defaultGasPrice": 64,
"defaultGasLimit": 21000, "defaultGasLimit": 21000,
"defaultGasLimitTokens": 200000, "defaultGasLimitTokens": 200000,
"tokens": "./data/ethereumClassicTokens.json", "tokens": "./data/ethereumClassicTokens.json",
"backends": [ "web3": [
{ "wss://etc2.trezor.io/geth"
"name": "TREZOR Wallet - Ethereum",
"urls": [
"https://etc-geth.0xinfra.com/",
"https://mew.epool.io/"
]
}
], ],
"explorer": { "explorer": {
"tx": "https://gastracker.io/tx/", "tx": "https://gastracker.io/tx/",
@ -50,60 +38,31 @@
}, },
{ {
"name": "Ethereum Ropsten", "name": "Ethereum Ropsten",
"symbol": "tETH", "symbol": "tROP",
"network": "ropsten", "network": "trop",
"chainId": 3, "chainId": 3,
"bip44": "m/44'/60'/0'/0", "bip44": "m/44'/60'/0'/0",
"defaultGasPrice": 64, "defaultGasPrice": 64,
"defaultGasLimit": 21000, "defaultGasLimit": 21000,
"defaultGasLimitTokens": 200000, "defaultGasLimitTokens": 200000,
"tokens": "./data/ropstenTokens.json", "tokens": "./data/ropstenTokens.json",
"backends": [ "web3": [
{ "wss://ropsten1.trezor.io/geth"
"name": "TREZOR Wallet - Ethereum",
"urls": [
"https://ropsten.infura.io/QGyVKozSUEh2YhL4s2G4",
"http://10.34.2.5:8545"
]
}
], ],
"explorer": { "explorer": {
"tx": "https://ropsten.etherscan.io/tx/", "tx": "https://ropsten.etherscan.io/tx/",
"address": "https://ropsten.etherscan.io/address/" "address": "https://ropsten.etherscan.io/address/"
} }
},
{
"name": "Ethereum Rinkeby",
"symbol": "tETH",
"network": "rinkeby",
"chainId": 4,
"bip44": "m/44'/61'/0'/0",
"defaultGasPrice": 64,
"defaultGasLimit": 21000,
"defaultGasLimitTokens": 200000,
"tokens": "./data/rinkebyTokens.json",
"backends": [
{
"name": "TREZOR Wallet - Ethereum",
"urls": [
"https://rinkeby.infura.io/QGyVKozSUEh2YhL4s2G4"
]
}
],
"explorer": {
"tx": "https://rinkeby.etherscan.io/tx/",
"address": "https://rinkeby.etherscan.io/address/"
}
} }
], ],
"fiatValueTickers": [ "fiatValueTickers": [
{ {
"network": "ethereum", "network": "eth",
"url": "https://api.coinmarketcap.com/v1/ticker/ethereum/" "url": "https://api.coinmarketcap.com/v1/ticker/ethereum/"
}, },
{ {
"network": "ethereum-classic", "network": "etc",
"url": "https://api.coinmarketcap.com/v1/ticker/ethereum-classic/" "url": "https://api.coinmarketcap.com/v1/ticker/ethereum-classic/"
} }
], ],

View File

@ -4,5 +4,17 @@
"name": "PLASMA", "name": "PLASMA",
"symbol" :"PLASMA", "symbol" :"PLASMA",
"decimals": 6 "decimals": 6
},
{
"address": "0x58cda554935e4a1f2acbe15f8757400af275e084",
"name": "Trezor01",
"symbol": "T01",
"decimals": 0
},
{
"address": "0xa04761a776af2bed654a041430a063fd9d20fad4",
"name": "Trezor13",
"symbol": "T013",
"decimals": 13
} }
] ]

View File

@ -2,7 +2,7 @@
import * as ACCOUNT from 'actions/constants/account'; import * as ACCOUNT from 'actions/constants/account';
import type { Action, TrezorDevice } from 'flowtype'; import type { Action, TrezorDevice } from 'flowtype';
import type { State } from 'reducers/AccountsReducer'; import type { Account, State } from 'reducers/AccountsReducer';
export type AccountFromStorageAction = { export type AccountFromStorageAction = {
type: typeof ACCOUNT.FROM_STORAGE, type: typeof ACCOUNT.FROM_STORAGE,
@ -11,11 +11,12 @@ export type AccountFromStorageAction = {
export type AccountCreateAction = { export type AccountCreateAction = {
type: typeof ACCOUNT.CREATE, type: typeof ACCOUNT.CREATE,
device: TrezorDevice, payload: Account,
network: string, }
index: number,
path: Array<number>, export type AccountUpdateAction = {
address: string type: typeof ACCOUNT.UPDATE,
payload: Account,
} }
export type AccountSetBalanceAction = { export type AccountSetBalanceAction = {
@ -36,9 +37,10 @@ export type AccountSetNonceAction = {
export type AccountAction = export type AccountAction =
AccountFromStorageAction AccountFromStorageAction
| AccountCreateAction | AccountCreateAction
| AccountSetBalanceAction | AccountUpdateAction
| AccountSetNonceAction; | AccountSetBalanceAction
| AccountSetNonceAction;
export const setBalance = (address: string, network: string, deviceState: string, balance: string): Action => ({ export const setBalance = (address: string, network: string, deviceState: string, balance: string): Action => ({
type: ACCOUNT.SET_BALANCE, type: ACCOUNT.SET_BALANCE,
@ -55,3 +57,8 @@ export const setNonce = (address: string, network: string, deviceState: string,
deviceState, deviceState,
nonce, nonce,
}); });
export const update = (account: Account): Action => ({
type: ACCOUNT.UPDATE,
payload: account
});

View File

@ -0,0 +1,210 @@
/* @flow */
import Web3 from 'web3';
import HDKey from 'hdkey';
import EthereumjsUtil from 'ethereumjs-util';
import EthereumjsUnits from 'ethereumjs-units';
import EthereumjsTx from 'ethereumjs-tx';
import TrezorConnect from 'trezor-connect';
import BigNumber from 'bignumber.js';
import type { EstimateGasOptions } from 'web3';
import type { TransactionStatus, TransactionReceipt } from 'web3';
import { strip } from 'utils/ethUtils';
import * as BLOCKCHAIN from 'actions/constants/blockchain';
import * as WEB3 from 'actions/constants/web3';
import * as PENDING from 'actions/constants/pendingTx';
import * as AccountsActions from './AccountsActions';
import * as Web3Actions from './Web3Actions';
import type {
TrezorDevice,
Dispatch,
GetState,
Action,
AsyncAction,
PromiseAction,
ThunkAction,
} from 'flowtype';
import type { EthereumAccount } from 'trezor-connect';
import type { Token } from 'reducers/TokensReducer';
import type { NetworkToken } from 'reducers/LocalStorageReducer';
export type BlockchainAction = {
type: typeof BLOCKCHAIN.READY,
}
export const discoverAccount = (device: TrezorDevice, address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch, getState: GetState): 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, getState: GetState): Promise<NetworkToken> => {
return await dispatch( Web3Actions.getTokenInfo(input, network) );
}
export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
return await dispatch( Web3Actions.getTokenBalance(token) );
}
export const getGasPrice = (network: string, defaultGasPrice: number): PromiseAction<BigNumber> => async (dispatch: Dispatch, getState: GetState): Promise<BigNumber> => {
try {
const gasPrice = await dispatch( Web3Actions.getCurrentGasPrice(network) );
return new BigNumber(gasPrice);
} catch (error) {
return new BigNumber(defaultGasPrice);
}
}
export const estimateGasLimit = (network: string, data: string, value: string, gasPrice: string): PromiseAction<number> => async (dispatch: Dispatch, getState: GetState): Promise<number> => {
return await dispatch( Web3Actions.estimateGasLimit(network, { to: '', data, value, gasPrice }) );
}
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 => {
return 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);
// $FlowIssue: trezor-connect@5.0.32
return await TrezorConnect.blockchainSubscribe({
// accounts: addresses,
accounts: [],
coin: network
});
}
// Conditionally subscribe to blockchain backend
// called after TrezorConnect.init successfully emits TRANSPORT.START event
// checks if there are discovery processes loaded from LocalStorage
// if so starts subscription to proper networks
export const init = (): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
if (getState().discovery.length > 0) {
// get unique networks
const networks: Array<string> = [];
getState().discovery.forEach(discovery => {
if (networks.indexOf(discovery.network) < 0) {
networks.push(discovery.network);
}
});
// subscribe
for (let i = 0; i < networks.length; i++) {
await dispatch( subscribe(networks[i]) );
}
}
// continue wallet initialization
dispatch({
type: BLOCKCHAIN.READY
});
}
// Handle BLOCKCHAIN.ERROR event from TrezorConnect
// disconnect and remove Web3 webscocket instance if exists
export const error = (payload: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
dispatch( Web3Actions.disconnect(payload.coin) );
}

View File

@ -6,12 +6,12 @@ import * as DISCOVERY from 'actions/constants/discovery';
import * as ACCOUNT from 'actions/constants/account'; import * as ACCOUNT from 'actions/constants/account';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import type { import type {
ThunkAction, AsyncAction, Action, GetState, Dispatch, TrezorDevice, ThunkAction, AsyncAction, PromiseAction, Action, GetState, Dispatch, TrezorDevice,
} from 'flowtype'; } from 'flowtype';
import type { Discovery, State } from 'reducers/DiscoveryReducer'; import type { Discovery, State } from 'reducers/DiscoveryReducer';
import * as AccountsActions from './AccountsActions'; import * as AccountsActions from './AccountsActions';
import * as BlockchainActions from './BlockchainActions';
import { getNonceAsync, getBalanceAsync } from './Web3Actions'; import { setBalance as setTokenBalance } from './TokenActions';
export type DiscoveryStartAction = { export type DiscoveryStartAction = {
@ -24,7 +24,7 @@ export type DiscoveryStartAction = {
} }
export type DiscoveryWaitingAction = { export type DiscoveryWaitingAction = {
type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BACKEND, type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN,
device: TrezorDevice, device: TrezorDevice,
network: string network: string
} }
@ -44,147 +44,9 @@ export type DiscoveryAction = {
type: typeof DISCOVERY.FROM_STORAGE, type: typeof DISCOVERY.FROM_STORAGE,
payload: State payload: State
} | DiscoveryStartAction } | DiscoveryStartAction
| DiscoveryWaitingAction | DiscoveryWaitingAction
| DiscoveryStopAction | DiscoveryStopAction
| DiscoveryCompleteAction; | DiscoveryCompleteAction;
// Because start() is calling begin() and begin() is calling start() one of them must be declared first
// otherwise eslint will start complaining
let begin;
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { completed } = discoveryProcess;
discoveryProcess.completed = false;
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
// verify address with TREZOR
const verifyAddress = await TrezorConnect.ethereumGetAddress({
device: {
path: device.path,
instance: device.instance,
state: device.state,
},
path,
showOnTrezor: false,
keepSession: true,
useEmptyPassphrase: !device.instance,
});
if (discoveryProcess.interrupted) return;
// TODO: with block-book (Martin)
// const discoveryA = await TrezorConnect.accountDiscovery({
// device: {
// path: device.path,
// instance: device.instance,
// state: device.state
// },
// });
// if (discoveryProcess.interrupted) return;
if (verifyAddress && verifyAddress.success) {
//const trezorAddress: string = '0x' + verifyAddress.payload.address;
const trezorAddress: string = EthereumjsUtil.toChecksumAddress(verifyAddress.payload.address);
if (trezorAddress !== ethAddress) {
// throw inconsistent state error
console.warn('Inconsistent state', trezorAddress, ethAddress);
dispatch({
type: NOTIFICATION.ADD,
payload: {
type: 'error',
title: 'Address validation error',
message: `Addresses are different. TREZOR: ${trezorAddress} HDKey: ${ethAddress}`,
cancelable: true,
actions: [
{
label: 'Try again',
callback: () => {
dispatch(start(device, discoveryProcess.network));
},
},
],
},
});
return;
}
} else {
// handle TREZOR communication error
dispatch({
type: NOTIFICATION.ADD,
payload: {
type: 'error',
title: 'Address validation error',
message: verifyAddress.payload.error,
cancelable: true,
actions: [
{
label: 'Try again',
callback: () => {
dispatch(start(device, discoveryProcess.network));
},
},
],
},
});
return;
}
const web3instance = getState().web3.find(w3 => w3.network === network);
if (!web3instance) return;
const balance = await getBalanceAsync(web3instance.web3, ethAddress);
if (discoveryProcess.interrupted) return;
const nonce: number = await getNonceAsync(web3instance.web3, ethAddress);
if (discoveryProcess.interrupted) return;
const addressIsEmpty = nonce < 1 && !balance.greaterThan(0);
if (!addressIsEmpty || (addressIsEmpty && completed) || (addressIsEmpty && discoveryProcess.accountIndex === 0)) {
dispatch({
type: ACCOUNT.CREATE,
device,
network,
index: discoveryProcess.accountIndex,
path,
address: ethAddress,
});
dispatch(
AccountsActions.setBalance(ethAddress, network, device.state || 'undefined', web3instance.web3.fromWei(balance.toString(), 'ether')),
);
dispatch(AccountsActions.setNonce(ethAddress, network, device.state || 'undefined', nonce));
if (!completed) { dispatch(discoverAccount(device, discoveryProcess)); }
}
if (addressIsEmpty) {
// release acquired sesssion
await TrezorConnect.getFeatures({
device: {
path: device.path,
instance: device.instance,
state: device.state,
},
keepSession: false,
useEmptyPassphrase: !device.instance,
});
if (discoveryProcess.interrupted) return;
dispatch({
type: DISCOVERY.COMPLETE,
device,
network,
});
}
};
export const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const selected = getState().wallet.selectedDevice; const selected = getState().wallet.selectedDevice;
@ -203,26 +65,9 @@ export const start = (device: TrezorDevice, network: string, ignoreCompleted?: b
return; return;
} }
const web3 = getState().web3.find(w3 => w3.network === network); const discovery: State = getState().discovery;
if (!web3) {
console.error('Start discovery: Web3 does not exist', network);
return;
}
if (!web3.web3.currentProvider.isConnected()) {
console.error('Start discovery: Web3 is not connected', network);
dispatch({
type: DISCOVERY.WAITING_FOR_BACKEND,
device,
network,
});
return;
}
const { discovery }: { discovery: State } = getState();
const discoveryProcess: ?Discovery = discovery.find(d => d.deviceState === device.state && d.network === network); const discoveryProcess: ?Discovery = discovery.find(d => d.deviceState === device.state && d.network === network);
if (!selected.connected && (!discoveryProcess || !discoveryProcess.completed)) { if (!selected.connected && (!discoveryProcess || !discoveryProcess.completed)) {
dispatch({ dispatch({
type: DISCOVERY.WAITING_FOR_DEVICE, type: DISCOVERY.WAITING_FOR_DEVICE,
@ -232,15 +77,25 @@ export const start = (device: TrezorDevice, network: string, ignoreCompleted?: b
return; return;
} }
const blockchain = getState().blockchain.find(b => b.name === network);
if (blockchain && !blockchain.connected && (!discoveryProcess || !discoveryProcess.completed)) {
dispatch({
type: DISCOVERY.WAITING_FOR_BLOCKCHAIN,
device,
network,
});
return;
}
if (!discoveryProcess) { if (!discoveryProcess) {
dispatch(begin(device, network)); dispatch(begin(device, network))
} else if (discoveryProcess.completed && !ignoreCompleted) { } else if (discoveryProcess.completed && !ignoreCompleted) {
dispatch({ dispatch({
type: DISCOVERY.COMPLETE, type: DISCOVERY.COMPLETE,
device, device,
network, network,
}); });
} else if (discoveryProcess.interrupted || discoveryProcess.waitingForDevice) { } else if (discoveryProcess.interrupted || discoveryProcess.waitingForDevice || discoveryProcess.waitingForBlockchain) {
// discovery cycle was interrupted // discovery cycle was interrupted
// start from beginning // start from beginning
dispatch(begin(device, network)); dispatch(begin(device, network));
@ -249,7 +104,10 @@ export const start = (device: TrezorDevice, network: string, ignoreCompleted?: b
} }
}; };
begin = (device: TrezorDevice, network: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { // 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 { config } = getState().localStorage; const { config } = getState().localStorage;
const coinToDiscover = config.coins.find(c => c.network === network); const coinToDiscover = config.coins.find(c => c.network === network);
if (!coinToDiscover) return; if (!coinToDiscover) return;
@ -272,10 +130,8 @@ begin = (device: TrezorDevice, network: string): AsyncAction => async (dispatch:
useEmptyPassphrase: !device.instance, useEmptyPassphrase: !device.instance,
}); });
// handle TREZOR response error // handle TREZOR response error
if (!response.success) { if (!response.success) {
// TODO: check message
console.warn('DISCOVERY ERROR', response);
dispatch({ dispatch({
type: NOTIFICATION.ADD, type: NOTIFICATION.ADD,
payload: { payload: {
@ -312,14 +168,113 @@ begin = (device: TrezorDevice, network: string): AsyncAction => async (dispatch:
basePath, basePath,
}); });
// next iteration
dispatch(start(device, network)); dispatch(start(device, network));
}; };
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { completed } = discoveryProcess;
discoveryProcess.completed = false;
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
try {
const account = await dispatch( BlockchainActions.discoverAccount(device, ethAddress, network) );
if (discoveryProcess.interrupted) 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) ); }
}
} catch (error) {
dispatch({
type: DISCOVERY.STOP,
device
});
dispatch({
type: NOTIFICATION.ADD,
payload: {
type: 'error',
title: 'Account discovery error',
message: error.message,
cancelable: true,
actions: [
{
label: 'Try again',
callback: () => {
dispatch(start(device, discoveryProcess.network));
},
},
],
},
});
}
};
const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
await TrezorConnect.getFeatures({
device: {
path: device.path,
instance: device.instance,
state: device.state,
},
keepSession: false,
useEmptyPassphrase: !device.instance,
});
await dispatch( BlockchainActions.subscribe(discoveryProcess.network) );
if (discoveryProcess.interrupted) return;
dispatch({
type: DISCOVERY.COMPLETE,
device,
network: discoveryProcess.network,
});
}
export const reconnect = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
await dispatch(BlockchainActions.subscribe(network));
dispatch(restore());
}
export const restore = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const restore = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const selected = getState().wallet.selectedDevice; const selected = getState().wallet.selectedDevice;
if (selected && selected.connected && selected.features) { if (selected && selected.connected && selected.features) {
const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === selected.state && d.waitingForDevice); const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === selected.state && (d.interrupted || d.waitingForDevice || d.waitingForBlockchain));
if (discoveryProcess) { if (discoveryProcess) {
dispatch(start(selected, discoveryProcess.network)); dispatch(start(selected, discoveryProcess.network));
} }

View File

@ -24,14 +24,14 @@ export const onPinSubmit = (value: string): Action => {
}; };
}; };
export const onPassphraseSubmit = (/* passphrase: string */): AsyncAction => async (dispatch: Dispatch): Promise<void> => { export const onPassphraseSubmit = (passphrase: string): AsyncAction => async (dispatch: Dispatch): Promise<void> => {
// const resp = await TrezorConnect.uiResponse({ const resp = await TrezorConnect.uiResponse({
// type: UI.RECEIVE_PASSPHRASE, type: UI.RECEIVE_PASSPHRASE,
// payload: { payload: {
// value: passphrase, value: passphrase,
// save: true, save: true,
// }, },
// }); });
dispatch({ dispatch({
type: MODAL.CLOSE, type: MODAL.CLOSE,

View File

@ -7,6 +7,9 @@ import type { State, PendingTx } 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,
payload: PendingTx
} | { } | {
type: typeof PENDING.TX_RESOLVED, type: typeof PENDING.TX_RESOLVED,
tx: PendingTx, tx: PendingTx,

View File

@ -47,8 +47,7 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct
|| prevState.accounts !== state.accounts || prevState.accounts !== state.accounts
|| prevState.discovery !== state.discovery || prevState.discovery !== state.discovery
|| prevState.tokens !== state.tokens || prevState.tokens !== state.tokens
|| prevState.pending !== state.pending || prevState.pending !== state.pending) {
|| prevState.web3 !== state.web3) {
if (locationChange) { if (locationChange) {
// dispose current account view // dispose current account view
dispatch(dispose()); dispatch(dispose());
@ -59,7 +58,6 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct
const discovery = stateUtils.getDiscoveryProcess(state); const discovery = stateUtils.getDiscoveryProcess(state);
const tokens = stateUtils.getAccountTokens(state, account); const tokens = stateUtils.getAccountTokens(state, account);
const pending = stateUtils.getAccountPendingTx(state.pending, account); const pending = stateUtils.getAccountPendingTx(state.pending, account);
const web3 = stateUtils.getWeb3(state);
const payload: $ElementType<State, 'selectedAccount'> = { const payload: $ElementType<State, 'selectedAccount'> = {
location: location.pathname, location: location.pathname,
@ -68,7 +66,6 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct
discovery, discovery,
tokens, tokens,
pending, pending,
web3,
}; };
let needUpdate: boolean = false; let needUpdate: boolean = false;

View File

@ -12,6 +12,7 @@ import * as SEND from 'actions/constants/send';
import { initialState } from 'reducers/SendFormReducer'; import { initialState } from 'reducers/SendFormReducer';
import { findToken } from 'reducers/TokensReducer'; import { findToken } from 'reducers/TokensReducer';
import { findDevice, getPendingAmount, getPendingNonce } from 'reducers/utils'; import { findDevice, getPendingAmount, getPendingNonce } from 'reducers/utils';
import * as stateUtils from 'reducers/utils';
import type { import type {
Dispatch, Dispatch,
@ -27,7 +28,8 @@ import type { State, FeeLevel } from 'reducers/SendFormReducer';
import type { Account } from 'reducers/AccountsReducer'; import type { Account } from 'reducers/AccountsReducer';
import type { Props } from 'views/Wallet/views/AccountSend/Container'; import type { Props } from 'views/Wallet/views/AccountSend/Container';
import * as SessionStorageActions from './SessionStorageActions'; import * as SessionStorageActions from './SessionStorageActions';
import { estimateGas, pushTx } from './Web3Actions'; import { prepareEthereumTx, serializeEthereumTx } from './TxActions';
import * as BlockchainActions from './BlockchainActions';
export type SendTxAction = { export type SendTxAction = {
type: typeof SEND.TX_COMPLETE, type: typeof SEND.TX_COMPLETE,
@ -223,14 +225,13 @@ export const getFeeLevels = (symbol: string, gasPrice: BigNumber | string, gasLi
// initialize component // initialize component
export const init = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { const {
account, account,
network, network,
web3,
} = getState().selectedAccount; } = getState().selectedAccount;
if (!account || !network || !web3) return; if (!account || !network) return;
const stateFromStorage = SessionStorageActions.load(getState().router.location.pathname); const stateFromStorage = SessionStorageActions.load(getState().router.location.pathname);
if (stateFromStorage) { if (stateFromStorage) {
@ -243,7 +244,10 @@ export const init = (): ThunkAction => (dispatch: Dispatch, getState: GetState):
// TODO: check if there are some unfinished tx in localStorage // TODO: check if there are some unfinished tx in localStorage
const gasPrice: BigNumber = new BigNumber(EthereumjsUnits.convert(web3.gasPrice, 'wei', 'gwei')) || new BigNumber(network.defaultGasPrice);
// const gasPrice: BigNumber = new BigNumber(EthereumjsUnits.convert(web3.gasPrice, 'wei', 'gwei')) || new BigNumber(network.defaultGasPrice);
const gasPrice: BigNumber = await dispatch( BlockchainActions.getGasPrice(network.network, network.defaultGasPrice) );
// const gasPrice: BigNumber = new BigNumber(network.defaultGasPrice);
const gasLimit: string = network.defaultGasLimit.toString(); const gasLimit: string = network.defaultGasLimit.toString();
const feeLevels: Array<FeeLevel> = getFeeLevels(network.symbol, gasPrice, gasLimit); const feeLevels: Array<FeeLevel> = getFeeLevels(network.symbol, gasPrice, gasLimit);
@ -709,12 +713,9 @@ export const onNonceChange = (nonce: string): AsyncAction => async (dispatch: Di
const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { const {
web3,
network, network,
} = getState().selectedAccount; } = getState().selectedAccount;
if (!web3 || !network) return; if (!network) return;
const w3 = web3.web3;
const state: State = getState().sendForm; const state: State = getState().sendForm;
const requestedData = state.data; const requestedData = state.data;
@ -732,14 +733,7 @@ const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState:
return; return;
} }
// TODO: allow data starting with 0x ... const gasLimit: number = await dispatch( BlockchainActions.estimateGasLimit(network.network, state.data, state.amount, state.gasPrice) );
const data: string = `0x${state.data.length % 2 === 0 ? state.data : `0${state.data}`}`;
const gasLimit = await estimateGas(w3, {
to: '0x0000000000000000000000000000000000000000',
data,
value: w3.toHex(w3.toWei(state.amount, 'ether')),
gasPrice: w3.toHex(EthereumjsUnits.convert(state.gasPrice, 'gwei', 'wei')),
});
if (getState().sendForm.data === requestedData) { if (getState().sendForm.data === requestedData) {
dispatch(onGasLimitChange(gasLimit.toString())); dispatch(onGasLimitChange(gasLimit.toString()));
@ -777,56 +771,29 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
const { const {
account, account,
network, network,
web3,
pending, pending,
} = getState().selectedAccount; } = getState().selectedAccount;
if (!account || !web3 || !network) return;
if (!account || !network) return;
const currentState: State = getState().sendForm; const currentState: State = getState().sendForm;
const isToken: boolean = currentState.currency !== currentState.networkSymbol; const isToken: boolean = currentState.currency !== currentState.networkSymbol;
const w3 = web3.web3;
const address_n = account.addressPath; const address_n = account.addressPath;
const pendingNonce: number = stateUtils.getPendingNonce(pending);
let data: string = `0x${currentState.data}`;
let txAmount: string = w3.toHex(w3.toWei(currentState.amount, 'ether'));
let txAddress: string = currentState.address;
if (isToken) {
const token: ?Token = findToken(getState().tokens, account.address, currentState.currency, account.deviceState);
if (!token) return;
const contract = web3.erc20.at(token.address);
const amountValue: string = new BigNumber(currentState.amount).times(Math.pow(10, token.decimals)).toString(10);
data = contract.transfer.getData(currentState.address, amountValue, {
from: account.address,
gasLimit: currentState.gasLimit,
gasPrice: currentState.gasPrice,
});
txAmount = '0x00';
txAddress = token.address;
}
const pendingNonce: number = getPendingNonce(pending);
const nonce = pendingNonce > 0 && pendingNonce >= account.nonce ? pendingNonce : account.nonce; const nonce = pendingNonce > 0 && pendingNonce >= account.nonce ? pendingNonce : account.nonce;
console.warn('NONCE', nonce, account.nonce, pendingNonce); const txData = await dispatch( prepareEthereumTx({
network: network.network,
const txData = { token: isToken ? findToken(getState().tokens, account.address, currentState.currency, account.deviceState) : null,
address_n, from: account.address,
// from: currentAddress.address to: currentState.address,
to: txAddress, amount: currentState.amount,
value: txAmount, data: currentState.data,
data, gasLimit: currentState.gasLimit,
chainId: web3.chainId, gasPrice: currentState.gasPrice,
nonce: w3.toHex(nonce), nonce
gasLimit: w3.toHex(currentState.gasLimit), }) );
gasPrice: w3.toHex(EthereumjsUnits.convert(currentState.gasPrice, 'gwei', 'wei')),
r: '',
s: '',
v: '',
};
const selected: ?TrezorDevice = getState().wallet.selectedDevice; const selected: ?TrezorDevice = getState().wallet.selectedDevice;
if (!selected) return; if (!selected) return;
@ -861,9 +828,17 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
txData.v = signedTransaction.payload.v; txData.v = signedTransaction.payload.v;
try { try {
const tx = new EthereumjsTx(txData); const serializedTx: string = await dispatch( serializeEthereumTx(txData) );
const serializedTx = `0x${tx.serialize().toString('hex')}`; const push = await TrezorConnect.pushTransaction({
const txid: string = await pushTx(w3, serializedTx); tx: serializedTx,
coin: network.network
});
if (!push.success) {
throw new Error( push.payload.error );
}
const txid = push.payload.txid;
dispatch({ dispatch({
type: SEND.TX_COMPLETE, type: SEND.TX_COMPLETE,
@ -871,7 +846,7 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge
selectedCurrency: currentState.currency, selectedCurrency: currentState.currency,
amount: currentState.amount, amount: currentState.amount,
total: currentState.total, total: currentState.total,
tx, tx: txData,
nonce, nonce,
txid, txid,
txData, txData,

View File

@ -9,7 +9,7 @@ import type {
import type { State, Token } from 'reducers/TokensReducer'; import type { State, Token } from 'reducers/TokensReducer';
import type { Account } from 'reducers/AccountsReducer'; import type { Account } from 'reducers/AccountsReducer';
import type { NetworkToken } from 'reducers/LocalStorageReducer'; import type { NetworkToken } from 'reducers/LocalStorageReducer';
import { getTokenInfoAsync, getTokenBalanceAsync } from './Web3Actions'; import * as BlockchainActions from './BlockchainActions';
export type TokenAction = { export type TokenAction = {
type: typeof TOKEN.FROM_STORAGE, type: typeof TOKEN.FROM_STORAGE,
@ -42,15 +42,11 @@ export const load = (input: string, network: string): AsyncAction => async (disp
// when options is a large list (>200 items) // when options is a large list (>200 items)
return result.slice(0, 100); return result.slice(0, 100);
} }
const web3instance = getState().web3.find(w3 => w3.network === network);
if (!web3instance) return;
const info = await getTokenInfoAsync(web3instance.erc20, input); const info = await dispatch( BlockchainActions.getTokenInfo(input, network) );
if (info) { if (info) {
return [info]; return [info];
} }
//await resolveAfter(300000);
//await resolveAfter(3000);
}; };
export const setBalance = (tokenAddress: string, ethAddress: string, balance: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const setBalance = (tokenAddress: string, ethAddress: string, balance: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
@ -68,9 +64,6 @@ export const setBalance = (tokenAddress: string, ethAddress: string, balance: st
}; };
export const add = (token: NetworkToken, account: Account): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const add = (token: NetworkToken, account: Account): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const web3instance = getState().web3.find(w3 => w3.network === account.network);
if (!web3instance) return;
const tkn: Token = { const tkn: Token = {
loaded: false, loaded: false,
deviceState: account.deviceState, deviceState: account.deviceState,
@ -88,7 +81,7 @@ export const add = (token: NetworkToken, account: Account): AsyncAction => async
payload: tkn, payload: tkn,
}); });
const tokenBalance = await getTokenBalanceAsync(web3instance.erc20, tkn); const tokenBalance = await dispatch( BlockchainActions.getTokenBalance(tkn) );
dispatch(setBalance(token.address, account.address, tokenBalance)); dispatch(setBalance(token.address, account.address, tokenBalance));
}; };

View File

@ -1,6 +1,6 @@
/* @flow */ /* @flow */
import TrezorConnect, { import TrezorConnect, {
DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, UI, DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT
} from 'trezor-connect'; } from 'trezor-connect';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
@ -9,14 +9,15 @@ import { getDuplicateInstanceNumber } from 'reducers/utils';
import { push } from 'react-router-redux'; import { push } from 'react-router-redux';
import type { import type {
DeviceMessage, DeviceMessage,
UiMessage,
TransportMessage,
DeviceMessageType, DeviceMessageType,
TransportMessageType, UiMessage,
UiMessageType, UiMessageType,
TransportMessage,
TransportMessageType,
BlockchainMessage,
BlockchainMessageType,
} from 'trezor-connect'; } from 'trezor-connect';
import type { import type {
@ -115,11 +116,18 @@ 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; // assert flow type
dispatch({
type,
payload: event.payload,
});
});
// $FlowIssue LOCAL not declared // $FlowIssue LOCAL not declared
// window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://sisyfos.trezor.io/connect/'; window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://sisyfos.trezor.io/connect/';
// window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://connect.trezor.io/5/'; // window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://connect.trezor.io/5/';
//window.__TREZOR_CONNECT_SRC = 'https://sisyfos.trezor.io/connect/';
// window.__TREZOR_CONNECT_SRC = 'https://localhost:8088/';
try { try {
await TrezorConnect.init({ await TrezorConnect.init({

69
src/actions/TxActions.js Normal file
View File

@ -0,0 +1,69 @@
/* @flow */
import EthereumjsTx from 'ethereumjs-tx';
import EthereumjsUnits from 'ethereumjs-units';
import BigNumber from 'bignumber.js';
import { toHex } from 'web3-utils';
import { initWeb3 } from './Web3Actions';
import type {
Dispatch,
GetState,
PromiseAction,
} from 'flowtype';
import type {
EthereumTransaction
} from 'trezor-connect';
import type { Token } from 'reducers/TokensReducer';
type EthereumTxRequest = {
network: string;
token: ?Token;
from: string;
to: string;
amount: string;
data: string;
gasLimit: string;
gasPrice: string;
nonce: number;
}
export const prepareEthereumTx = (tx: EthereumTxRequest): PromiseAction<EthereumTransaction> => async (dispatch: Dispatch, getState: GetState): Promise<EthereumTransaction> => {
const instance = await dispatch( initWeb3(tx.network) );
const token = tx.token;
let data: string = `0x${tx.data}`; // TODO: check if already prefixed
let value: string = toHex( EthereumjsUnits.convert(tx.amount, 'ether', 'wei') );
let to: string = tx.to;
if (token) {
// smart contract transaction
const contract = instance.erc20.clone();
contract.options.address = token.address;
const tokenAmount: string = new BigNumber(tx.amount).times(Math.pow(10, token.decimals)).toString(10);
data = instance.erc20.methods.transfer(to, tokenAmount).encodeABI();
value = '0x00';
to = token.address;
}
return {
to,
value,
data,
chainId: instance.chainId,
nonce: toHex(tx.nonce),
gasLimit: toHex(tx.gasLimit),
gasPrice: toHex( EthereumjsUnits.convert(tx.gasPrice, 'gwei', 'wei') ),
r: '',
s: '',
v: '',
}
};
export const serializeEthereumTx = (tx: EthereumTransaction): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
const ethTx = new EthereumjsTx(tx);
return `0x${ ethTx.serialize().toString('hex') }`;
// return toHex( ethTx.serialize() );
}

View File

@ -5,8 +5,11 @@ import { LOCATION_CHANGE } from 'react-router-redux';
import * as WALLET from 'actions/constants/wallet'; import * as WALLET from 'actions/constants/wallet';
import * as stateUtils from 'reducers/utils'; import * as stateUtils from 'reducers/utils';
import type import type {
{ Account,
Coin,
Discovery,
Token,
Device, Device,
TrezorDevice, TrezorDevice,
RouterLocationState, RouterLocationState,

View File

@ -1,22 +1,27 @@
/* @flow */ /* @flow */
import Web3 from 'web3'; import Web3 from 'web3';
import HDKey from 'hdkey';
import BigNumber from 'bignumber.js';
import type { import EthereumjsUtil from 'ethereumjs-util';
ContractFactory, import EthereumjsUnits from 'ethereumjs-units';
EstimateGasOptions, import EthereumjsTx from 'ethereumjs-tx';
TransactionStatus, // import InputDataDecoder from 'ethereum-input-data-decoder';
TransactionReceipt, import TrezorConnect from 'trezor-connect';
} from 'web3'; import type { EstimateGasOptions, TransactionStatus, TransactionReceipt } from 'web3';
import type BigNumber from 'bignumber.js'; import { strip } from 'utils/ethUtils';
import * as WEB3 from 'actions/constants/web3'; import * as WEB3 from 'actions/constants/web3';
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import type { import type {
Dispatch, Dispatch,
GetState, GetState,
ThunkAction,
AsyncAction, AsyncAction,
PromiseAction,
} from 'flowtype'; } from 'flowtype';
import type { EthereumAccount } from 'trezor-connect';
import type { Account } from 'reducers/AccountsReducer'; import type { Account } from 'reducers/AccountsReducer';
import type { PendingTx } from 'reducers/PendingTxReducer'; import type { PendingTx } from 'reducers/PendingTxReducer';
import type { Web3Instance } from 'reducers/Web3Reducer'; import type { Web3Instance } from 'reducers/Web3Reducer';
@ -40,379 +45,266 @@ export type Web3UpdateGasPriceAction = {
export type Web3Action = { export type Web3Action = {
type: typeof WEB3.READY, type: typeof WEB3.READY,
} | { } | {
type: typeof WEB3.CREATE, type: typeof WEB3.START,
} | {
type: typeof WEB3.CREATE | typeof WEB3.DISCONNECT,
instance: Web3Instance instance: Web3Instance
} } | Web3UpdateBlockAction
| Web3UpdateBlockAction | Web3UpdateGasPriceAction;
| Web3UpdateGasPriceAction;
export function init(instance: ?Web3, coinIndex: number = 0): AsyncAction { export const initWeb3 = (network: string, urlIndex: number = 0): PromiseAction<Web3Instance> => async (dispatch: Dispatch, getState: GetState): Promise<Web3Instance> => {
return async (dispatch: Dispatch, getState: GetState): Promise<void> => { return new Promise(async (resolve, reject) => {
// check if requested web was initialized before
const instance = getState().web3.find(w3 => w3.network === network);
if (instance && instance.web3.currentProvider.connected) {
resolve(instance);
return;
}
// requested web3 wasn't initialized or is disconnected
// initialize again
const { config, ERC20Abi } = getState().localStorage; const { config, ERC20Abi } = getState().localStorage;
const coin = config.coins.find(c => c.network === network);
const coin = config.coins[coinIndex];
if (!coin) { if (!coin) {
// all instances done // coin not found
dispatch({ reject(new Error(`Network ${ network} not found in application config.`));
type: WEB3.READY,
});
return; return;
} }
const { network } = coin; // get first url
const urls = coin.backends[0].urls; const url = coin.web3[ urlIndex ];
if (!url) {
let web3host: string = urls[0]; reject(new Error('Web3 backend is not responding'));
if (instance) {
const currentHost = instance.currentProvider.host;
const currentHostIndex: number = urls.indexOf(currentHost);
if (currentHostIndex + 1 < urls.length) {
web3host = urls[currentHostIndex + 1];
} else {
console.error(`TODO: Backend ${network} not working`, instance.currentProvider);
dispatch({
type: WEB3.CREATE,
instance: {
network,
web3: instance,
chainId: coin.chainId,
erc20: instance.eth.contract(ERC20Abi),
latestBlock: '0',
gasPrice: '0',
},
});
// try next coin
dispatch(init(null, coinIndex + 1));
return;
}
}
//const instance = new Web3(window.web3.currentProvider);
const web3 = new Web3(new Web3.providers.HttpProvider(web3host));
// instance = new Web3( new Web3.providers.HttpProvider('https://pyrus2.ubiqscan.io') ); // UBQ
//instance = new Web3( new Web3.providers.HttpProvider('https://node.expanse.tech/') ); // EXP
//instance = new Web3( new Web3.providers.HttpProvider('http://10.34.0.91:8545/') );
//web3 = new Web3(new Web3.providers.HttpProvider("https://api.myetherapi.com/rop"));
//instance = new Web3(new Web3.providers.HttpProvider("https://ropsten.infura.io2/QGyVKozSUEh2YhL4s2G4"));
//web3 = new Web3( new Web3.providers.HttpProvider("ws://34.230.234.51:30303") );
// initial check if backend is running
if (!web3.currentProvider.isConnected()) {
// try different url
dispatch(init(web3, coinIndex));
return; return;
} }
const erc20 = web3.eth.contract(ERC20Abi); const web3 = new Web3( new Web3.providers.WebsocketProvider(url) );
dispatch({ const onConnect = async () => {
type: WEB3.CREATE,
instance: { const latestBlock = await web3.eth.getBlockNumber();
const gasPrice = await web3.eth.getGasPrice();
const instance = {
network, network,
web3, web3,
chainId: coin.chainId, chainId: coin.chainId,
erc20, erc20: new web3.eth.Contract(ERC20Abi),
latestBlock: '0', latestBlock,
gasPrice: '0', gasPrice,
},
});
// dispatch({
// type: WEB3.GAS_PRICE_UPDATED,
// network,
// gasPrice
// });
// console.log("GET CHAIN", instance.version.network)
// instance.version.getWhisper((err, shh) => {
// console.log("-----whisperrr", error, shh)
// })
// const sshFilter = instance.ssh.filter('latest');
// sshFilter.watch((error, blockHash) => {
// console.warn("SSH", error, blockHash);
// });
//const shh = instance.shh.newIdentity();
// const latestBlockFilter = web3.eth.filter('latest');
const onBlockMined = async (error: ?Error, blockHash: ?string) => {
if (error) {
window.setTimeout(() => {
// try again
onBlockMined(new Error('manually_triggered_error'), undefined);
}, 30000);
} }
if (blockHash) { dispatch({
dispatch({ type: WEB3.CREATE,
type: WEB3.BLOCK_UPDATED, instance,
network, });
blockHash,
});
}
// TODO: filter only current device resolve(instance);
const accounts = getState().accounts.filter(a => a.network === network); }
for (const account of accounts) {
const nonce = await getNonceAsync(web3, account.address);
if (nonce !== account.nonce) {
dispatch(AccountsActions.setNonce(account.address, account.network, account.deviceState, nonce));
// dispatch( getBalance(account) ); const onEnd = async () => {
// TODO: check if nonce was updated,
// update tokens balance, web3.currentProvider.reset();
// update account balance, const instance = getState().web3.find(w3 => w3.network === network);
// update pending transactions
if (instance && instance.web3.currentProvider.connected) {
// backend disconnects
// dispatch({
// type: 'WEB3.DISCONNECT',
// network
// });
} else {
// backend initialization error for given url, try next one
try {
const web3 = await dispatch( initWeb3(network, urlIndex + 1) );
resolve(web3);
} catch (error) {
reject(error);
} }
dispatch(getBalance(account));
// dispatch( getNonce(account) );
} }
}
const tokens = getState().tokens.filter(t => t.network === network); web3.currentProvider.on('connect', onConnect);
tokens.forEach(token => dispatch(getTokenBalance(token))); web3.currentProvider.on('end', onEnd);
web3.currentProvider.on('error', onEnd);
});
}
dispatch(getGasPrice(network)); export const discoverAccount = (address: string, network: string): PromiseAction<EthereumAccount> => async (dispatch: Dispatch, getState: GetState): Promise<EthereumAccount> => {
const instance: Web3Instance = await dispatch( initWeb3(network) );
const pending = getState().pending.filter(p => p.network === network); const balance = await instance.web3.eth.getBalance(address);
pending.forEach(pendingTx => dispatch(getTransactionReceipt(pendingTx))); const nonce = await instance.web3.eth.getTransactionCount(address);
}; return {
address,
// latestBlockFilter.watch(onBlockMined); transactions: 0,
onBlockMined(new Error('manually_triggered_error'), undefined); block: 0,
balance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
nonce
// init next coin
dispatch(init(web3, coinIndex + 1));
// let instance2 = new Web3( new Web3.providers.HttpProvider('https://pyrus2.ubiqscan.io') );
// console.log("INIT WEB3", instance, instance2);
// instance2.eth.getGasPrice((error, gasPrice) => {
// console.log("---gasss price from UBQ", gasPrice)
// });
}; };
} }
export const resolvePendingTransactions = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
export function getGasPrice(network: string): AsyncAction { const instance: Web3Instance = await dispatch( initWeb3(network) );
return async (dispatch: Dispatch, getState: GetState): Promise<void> => { const pending = getState().pending.filter(p => p.network === network);
const index: number = getState().web3.findIndex(w3 => w3.network === network); for (const tx of pending) {
const status = await instance.web3.eth.getTransaction(tx.id);
const web3instance = getState().web3[index]; if (!status) {
const { web3 } = web3instance;
web3.eth.getGasPrice((error, gasPrice) => {
if (!error) {
if (web3instance.gasPrice && web3instance.gasPrice.toString() !== gasPrice.toString()) {
dispatch({
type: WEB3.GAS_PRICE_UPDATED,
network,
gasPrice,
});
}
}
});
};
}
export function getBalance(account: Account): AsyncAction {
return async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const web3instance = getState().web3.filter(w3 => w3.network === account.network)[0];
const { web3 } = web3instance;
web3.eth.getBalance(account.address, (error: Error, balance: BigNumber) => {
if (!error) {
const newBalance: string = web3.fromWei(balance.toString(), 'ether');
if (account.balance !== newBalance) {
dispatch(AccountsActions.setBalance(
account.address,
account.network,
account.deviceState,
newBalance,
));
// dispatch( loadHistory(addr) );
}
}
});
};
}
export function getTokenBalance(token: Token): AsyncAction {
return async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const web3instance = getState().web3.filter(w3 => w3.network === token.network)[0];
const contract = web3instance.erc20.at(token.address);
contract.balanceOf(token.ethAddress, (error: Error, balance: BigNumber) => {
if (balance) {
const newBalance: string = balance.dividedBy(Math.pow(10, token.decimals)).toString(10);
if (newBalance !== token.balance) {
dispatch(TokenActions.setBalance(
token.address,
token.ethAddress,
newBalance,
));
}
}
});
};
}
export function getNonce(account: Account): AsyncAction {
return async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const web3instance = getState().web3.filter(w3 => w3.network === account.network)[0];
const { web3 } = web3instance;
web3.eth.getTransactionCount(account.address, (error: Error, result: number) => {
if (!error) {
if (account.nonce !== result) {
dispatch(AccountsActions.setNonce(account.address, account.network, account.deviceState, result));
}
}
});
};
}
export const getTransactionReceipt = (tx: PendingTx): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const web3instance = getState().web3.filter(w3 => w3.network === tx.network)[0];
const { web3 } = web3instance;
web3.eth.getTransaction(tx.id, (error: Error, status: TransactionStatus) => {
if (!error && !status) {
dispatch({ dispatch({
type: PENDING.TX_NOT_FOUND, type: PENDING.TX_NOT_FOUND,
tx, tx,
}); });
} else if (status && status.blockNumber) { } else {
web3.eth.getTransactionReceipt(tx.id, (error: Error, receipt: TransactionReceipt) => { const receipt = await instance.web3.eth.getTransactionReceipt(tx.id);
if (receipt) { if (receipt) {
if (status.gas !== receipt.gasUsed) { if (status.gas !== receipt.gasUsed) {
dispatch({
type: PENDING.TX_TOKEN_ERROR,
tx,
});
}
dispatch({ dispatch({
type: PENDING.TX_RESOLVED, type: PENDING.TX_TOKEN_ERROR,
tx, tx,
receipt,
}); });
} }
}); dispatch({
type: PENDING.TX_RESOLVED,
tx,
receipt,
});
}
} }
}); }
}
export const getPendingInfo = (network: string, txid: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const instance: Web3Instance = await dispatch( initWeb3(network) );
const tx = await instance.web3.eth.getTransaction(txid);
/*
if (tx.input !== "0x") {
// find token:
// tx.to <= smart contract address
// smart contract data
const decoder = new InputDataDecoder(instance.erc20.options.jsonInterface);
const data = decoder.decodeData(tx.input);
if (data.name === 'transfer') {
console.warn("DATA!", data.inputs[0], data.inputs[1].toString(10));
}
}
*/
// return tx;
}
export const getTxInput = (): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const instance: Web3Instance = await dispatch( initWeb3("ropsten") );
// const inputData = instance.web3.utils.hexToAscii("0xa9059cbb00000000000000000000000073d0385f4d8e00c5e6504c6030f47bf6212736a80000000000000000000000000000000000000000000000000000000000000001");
// console.warn("input data!", inputData);
}
export const updateAccount = (account: Account, newAccount: EthereumAccount, network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const instance: Web3Instance = await dispatch( initWeb3(network) );
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 }) );
// update tokens for this account
dispatch( updateAccountTokens(account) );
}
export const updateAccountTokens = (account: Account): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const tokens = getState().tokens.filter(t => t.network === account.network && t.ethAddress === account.address);
for (const token of tokens) {
const balance = await dispatch( getTokenBalance(token) );
// const newBalance: string = balance.dividedBy(Math.pow(10, token.decimals)).toString(10);
if (balance !== token.balance) {
dispatch(TokenActions.setBalance(
token.address,
token.ethAddress,
balance,
));
}
}
}
export const getTokenInfo = (address: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch, getState: GetState): Promise<NetworkToken> => {
const instance: Web3Instance = await dispatch( initWeb3(network) );
const contract = instance.erc20.clone();
contract.options.address = address;
const name = await contract.methods.name().call();
const symbol = await contract.methods.symbol().call();
const decimals = await contract.methods.decimals().call();
return {
address,
name,
symbol,
decimals,
};
}; };
export const getTransaction = (web3: Web3, txid: string): Promise<any> => new Promise((resolve, reject) => { export const getTokenBalance = (token: Token): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
web3.eth.getTransaction(txid, (error, result) => { const instance = await dispatch( initWeb3(token.network) );
if (error) { const contract = instance.erc20.clone();
reject(error); contract.options.address = token.address;
} else {
resolve(result);
}
});
});
export const getBalanceAsync = (web3: Web3, address: string): Promise<BigNumber> => new Promise((resolve, reject) => { const balance = await contract.methods.balanceOf(token.ethAddress).call();
web3.eth.getBalance(address, (error: Error, result: BigNumber) => { return new BigNumber(balance).dividedBy(Math.pow(10, token.decimals)).toString(10);
if (error) { };
reject(error);
} else {
resolve(result);
}
});
});
export const getTokenBalanceAsync = (erc20: ContractFactory, token: Token): Promise<string> => new Promise((resolve, reject) => { export const getCurrentGasPrice = (network: string): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
const contract = erc20.at(token.address); const instance = getState().web3.find(w3 => w3.network === network);
contract.balanceOf(token.ethAddress, (error: Error, balance: BigNumber) => { if (instance) {
if (error) { return EthereumjsUnits.convert(instance.gasPrice, 'wei', 'gwei');
reject(error); } else {
} else { throw "0";
const newBalance: string = balance.dividedBy(Math.pow(10, token.decimals)).toString(10); }
resolve(newBalance); }
}
});
});
export const getNonceAsync = (web3: Web3, address: string): Promise<number> => new Promise((resolve, reject) => { export const updateGasPrice = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
web3.eth.getTransactionCount(address, (error: Error, result: number) => { try {
if (error) { const instance = await dispatch( initWeb3(network) );
reject(error); const gasPrice = await instance.web3.eth.getGasPrice();
} else { if (instance.gasPrice !== gasPrice) {
resolve(result); dispatch({
} type: WEB3.GAS_PRICE_UPDATED,
}); network,
}); gasPrice
export const getTokenInfoAsync = (erc20: ContractFactory, address: string): Promise<?NetworkToken> => new Promise((resolve) => {
const contract = erc20.at(address, (error/* , res */) => {
// console.warn("callback", error, res)
});
const info: NetworkToken = {
address,
name: '',
symbol: '',
decimals: 0,
};
contract.name.call((error: Error, name: string) => {
if (error) {
resolve(null);
return;
}
info.name = name;
contract.symbol.call((error: Error, symbol: string) => {
if (error) {
resolve(null);
return;
}
info.symbol = symbol;
contract.decimals.call((error: Error, decimals: BigNumber) => {
if (decimals) {
info.decimals = decimals.toNumber();
resolve(info);
} else {
resolve(null);
}
}); });
}
} catch (e) {
// silent action
// nothing happens if this fails
}
}
export const estimateGasLimit = (network: string, options: EstimateGasOptions): PromiseAction<number> => async (dispatch: Dispatch, getState: GetState): Promise<number> => {
const instance = await dispatch( initWeb3(network) );
// TODO: allow data starting with 0x ...
options.to = '0x0000000000000000000000000000000000000000';
options.data = `0x${options.data.length % 2 === 0 ? options.data : `0${options.data}`}`;
options.value = instance.web3.utils.toHex( EthereumjsUnits.convert(options.value || '0', 'ether', 'wei') );
options.gasPrice = instance.web3.utils.toHex( EthereumjsUnits.convert(options.gasPrice, 'gwei', 'wei') );
const limit = await instance.web3.eth.estimateGas(options);
return limit;
};
export const disconnect = (coinInfo: any): 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
const instance = getState().web3.find(w3 => w3.network === network);
if (instance) {
// reset current connection
instance.web3.currentProvider.reset();
instance.web3.currentProvider.connection.close();
// remove instance from reducer
dispatch({
type: WEB3.DISCONNECT,
instance
}); });
}); }
}); };
export const estimateGas = (web3: Web3, options: EstimateGasOptions): Promise<number> => new Promise((resolve, reject) => {
web3.eth.estimateGas(options, (error: ?Error, gas: ?number) => {
if (error) {
reject(error);
} else if (typeof gas === 'number') {
resolve(gas);
}
});
});
export const pushTx = (web3: Web3, tx: any): Promise<string> => new Promise((resolve, reject) => {
web3.eth.sendRawTransaction(tx, (error: Error, result: string) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});

View File

@ -6,6 +6,7 @@ export const DISPOSE: 'account__dispose' = 'account__dispose';
export const CREATE: 'account__create' = 'account__create'; export const CREATE: 'account__create' = 'account__create';
export const REMOVE: 'account__remove' = 'account__remove'; export const REMOVE: 'account__remove' = 'account__remove';
export const UPDATE: 'account__update' = 'account__update';
export const SET_BALANCE: 'account__set_balance' = 'account__set_balance'; export const SET_BALANCE: 'account__set_balance' = 'account__set_balance';
export const SET_NONCE: 'account__set_nonce' = 'account__set_nonce'; export const SET_NONCE: 'account__set_nonce' = 'account__set_nonce';
export const FROM_STORAGE: 'account__from_storage' = 'account__from_storage'; export const FROM_STORAGE: 'account__from_storage' = 'account__from_storage';

View File

@ -0,0 +1,6 @@
/* @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';

View File

@ -5,5 +5,5 @@ export const START: 'discovery__start' = 'discovery__start';
export const STOP: 'discovery__stop' = 'discovery__stop'; export const STOP: 'discovery__stop' = 'discovery__stop';
export const COMPLETE: 'discovery__complete' = 'discovery__complete'; export const COMPLETE: 'discovery__complete' = 'discovery__complete';
export const WAITING_FOR_DEVICE: 'discovery__waiting_for_device' = 'discovery__waiting_for_device'; export const WAITING_FOR_DEVICE: 'discovery__waiting_for_device' = 'discovery__waiting_for_device';
export const WAITING_FOR_BACKEND: 'discovery__waiting_for_backend' = 'discovery__waiting_for_backend'; export const WAITING_FOR_BLOCKCHAIN: 'discovery__waiting_for_blockchain' = 'discovery__waiting_for_blockchain';
export const FROM_STORAGE: 'discovery__from_storage' = 'discovery__from_storage'; export const FROM_STORAGE: 'discovery__from_storage' = 'discovery__from_storage';

View File

@ -2,6 +2,7 @@
export const FROM_STORAGE: 'pending__from_storage' = 'pending__from_storage'; export const FROM_STORAGE: 'pending__from_storage' = 'pending__from_storage';
export const ADD: 'pending__add' = 'pending__add';
export const TX_RESOLVED: 'pending__tx_resolved' = 'pending__tx_resolved'; export const TX_RESOLVED: 'pending__tx_resolved' = 'pending__tx_resolved';
export const TX_NOT_FOUND: 'pending__tx_not_found' = 'pending__tx_not_found'; export const TX_NOT_FOUND: 'pending__tx_not_found' = 'pending__tx_not_found';
export const TX_TOKEN_ERROR: 'pending__tx_token_error' = 'pending__tx_token_error'; export const TX_TOKEN_ERROR: 'pending__tx_token_error' = 'pending__tx_token_error';

View File

@ -8,3 +8,4 @@ export const READY: 'web3__ready' = 'web3__ready';
export const BLOCK_UPDATED: 'web3__block_updated' = 'web3__block_updated'; export const BLOCK_UPDATED: 'web3__block_updated' = 'web3__block_updated';
export const GAS_PRICE_UPDATED: 'web3__gas_price_updated' = 'web3__gas_price_updated'; export const GAS_PRICE_UPDATED: 'web3__gas_price_updated' = 'web3__gas_price_updated';
export const PENDING_TX_RESOLVED: 'web3__pending_tx_resolved' = 'web3__pending_tx_resolved'; export const PENDING_TX_RESOLVED: 'web3__pending_tx_resolved' = 'web3__pending_tx_resolved';
export const DISCONNECT: 'web3__disconnect' = 'web3__disconnect';

View File

@ -12,7 +12,7 @@ import icons from 'config/icons';
import colors from 'config/colors'; import colors from 'config/colors';
import Link from 'components/Link'; import Link from 'components/Link';
import { Props } from './index'; import type { Props } from 'components/modals/index';
type State = { type State = {
defaultName: string; defaultName: string;

View File

@ -7,6 +7,8 @@ import type {
Middleware as ReduxMiddleware, Middleware as ReduxMiddleware,
ThunkAction as ReduxThunkAction, ThunkAction as ReduxThunkAction,
AsyncAction as ReduxAsyncAction, AsyncAction as ReduxAsyncAction,
PromiseAction as ReduxPromiseAction,
ThunkDispatch as ReduxThunkDispatch,
PlainDispatch as ReduxPlainDispatch, PlainDispatch as ReduxPlainDispatch,
} from 'redux'; } from 'redux';
@ -15,6 +17,7 @@ import type { ReducersState } from 'reducers';
// Actions // Actions
import type { SelectedAccountAction } from 'actions/SelectedAccountActions'; import type { SelectedAccountAction } from 'actions/SelectedAccountActions';
import type { AccountAction } from 'actions/AccountsActions'; import type { AccountAction } from 'actions/AccountsActions';
import type { BlockchainAction } from 'actions/BlockchainActions';
import type { DiscoveryAction } from 'actions/DiscoveryActions'; import type { DiscoveryAction } from 'actions/DiscoveryActions';
import type { StorageAction } from 'actions/LocalStorageActions'; import type { StorageAction } from 'actions/LocalStorageActions';
import type { LogAction } from 'actions/LogActions'; import type { LogAction } from 'actions/LogActions';
@ -37,6 +40,7 @@ import type {
DeviceFirmwareStatus, DeviceFirmwareStatus,
DeviceMessageType, DeviceMessageType,
TransportMessageType, TransportMessageType,
BlockchainMessageType,
UiMessageType, UiMessageType,
} from 'trezor-connect'; } from 'trezor-connect';
@ -102,6 +106,11 @@ type UiEventAction = {
// }, // },
} }
type BlockchainEventAction = {
type: BlockchainMessageType,
payload: any,
}
// TODO: join this message with uiMessage // TODO: join this message with uiMessage
type IFrameHandshake = { type IFrameHandshake = {
type: 'iframe_handshake', type: 'iframe_handshake',
@ -114,9 +123,11 @@ export type Action =
| TransportEventAction | TransportEventAction
| DeviceEventAction | DeviceEventAction
| UiEventAction | UiEventAction
| BlockchainEventAction
| SelectedAccountAction | SelectedAccountAction
| AccountAction | AccountAction
| BlockchainAction
| DiscoveryAction | DiscoveryAction
| StorageAction | StorageAction
| LogAction | LogAction
@ -154,6 +165,7 @@ export type Middleware = ReduxMiddleware<State, Action>;
export type ThunkAction = ReduxThunkAction<State, Action>; export type ThunkAction = ReduxThunkAction<State, Action>;
export type AsyncAction = ReduxAsyncAction<State, Action>; export type AsyncAction = ReduxAsyncAction<State, Action>;
export type PromiseAction<R> = ReduxPromiseAction<State, Action, R>;
export type Store = ReduxStore<State, Action>; export type Store = ReduxStore<State, Action>;
export type GetState = () => State; export type GetState = () => State;

View File

@ -1,3 +1,5 @@
/* @flow */
declare module 'bignumber.js' { declare module 'bignumber.js' {
declare type $npm$big$number$object = number | string | T_BigNumber declare type $npm$big$number$object = number | string | T_BigNumber
declare type $npm$cmp$result = -1 | 0 | 1 declare type $npm$cmp$result = -1 | 0 | 1
@ -24,7 +26,7 @@ declare module 'bignumber.js' {
constructor(value: $npm$big$number$object): T_BigNumber; constructor(value: $npm$big$number$object): T_BigNumber;
// Methods // Methods
abs(): BigNumber; abs(): T_BigNumber;
cmp(n: $npm$big$number$object): $npm$cmp$result; cmp(n: $npm$big$number$object): $npm$cmp$result;
div(n: $npm$big$number$object): T_BigNumber; div(n: $npm$big$number$object): T_BigNumber;
dividedBy(n: $npm$big$number$object): T_BigNumber; dividedBy(n: $npm$big$number$object): T_BigNumber;

View File

@ -1,10 +1,12 @@
/* @flow */
declare module 'redux' { declare module 'redux' {
/* /*
S = State S = State
A = Action A = Action
D = Dispatch D = Dispatch
R = Promise response
*/ */
declare export type DispatchAPI<A> = (action: A) => A; declare export type DispatchAPI<A> = (action: A) => A;
@ -13,12 +15,14 @@ declare module 'redux' {
declare export type ThunkAction<S, A> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => void; declare export type ThunkAction<S, A> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => void;
declare export type AsyncAction<S, A> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => Promise<void>; declare export type AsyncAction<S, A> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => Promise<void>;
declare export type PromiseAction<S, A, R> = (dispatch: ReduxDispatch<S, A>, getState: () => S) => Promise<R>;
declare export type ThunkDispatch<S, A> = (action: ThunkAction<S, A>) => void; declare export type ThunkDispatch<S, A> = (action: ThunkAction<S, A>) => void;
declare export type AsyncDispatch<S, A> = (action: AsyncAction<S, A>) => Promise<void>; declare export type AsyncDispatch<S, A> = (action: AsyncAction<S, A>) => Promise<void>;
declare export type PromiseDispatch<S, A> = <R>(action: PromiseAction<S, A, R>) => Promise<R>;
declare export type PlainDispatch<A: {type: $Subtype<string>}> = DispatchAPI<A>; declare export type PlainDispatch<A: {type: $Subtype<string>}> = DispatchAPI<A>;
/* NEW: Dispatch is now a combination of these different dispatch types */ /* NEW: Dispatch is now a combination of these different dispatch types */
declare export type ReduxDispatch<S, A> = PlainDispatch<A> & ThunkDispatch<S, A> & AsyncDispatch<S, A>; declare export type ReduxDispatch<S, A> = PlainDispatch<A> & ThunkDispatch<S, A> & AsyncDispatch<S, A> & PromiseDispatch<S, A>;
declare export type MiddlewareAPI<S, A> = { declare export type MiddlewareAPI<S, A> = {
// dispatch: Dispatch<S, A>; // dispatch: Dispatch<S, A>;

View File

@ -1,8 +1,10 @@
/* @flow */
import type BigNumber from 'bignumber.js'; import type BigNumber from 'bignumber.js';
import type { EthereumUnitT, EthereumAddressT } from 'ethereum-types'; import type { EthereumUnitT, EthereumAddressT } from 'ethereum-types';
declare module 'web3' { declare module 'web3' {
declare type ProviderT = { declare type HttpProviderT = {
host: string; host: string;
timeout: number; timeout: number;
isConnected: () => boolean; isConnected: () => boolean;
@ -10,14 +12,28 @@ declare module 'web3' {
sendAsync: (payload: any, callback: (error: Error, result: any) => void) => any; sendAsync: (payload: any, callback: (error: Error, result: any) => void) => any;
}; };
declare type WebsocketProviderT = {
connection: {
close: () => void;
}; // WebSocket type
on: (type: string, callback: () => any) => void;
removeAllListeners: (type: string) => void;
reset: () => void;
connected: boolean;
}
declare class Web3T { declare class Web3T {
static providers: { static providers: {
HttpProvider: (host: string, timeout?: number) => ProviderT; HttpProvider: (host: string, timeout?: number) => HttpProviderT;
WebsocketProvider: (host: string, options?: any) => WebsocketProviderT;
}; };
constructor(ProviderT): Web3T; // constructor(HttpProviderT): Web3T;
currentProvider: ProviderT; constructor(WebsocketProviderT): Web3T;
// currentProvider: HttpProviderT;
currentProvider: WebsocketProviderT;
eth: Eth; eth: Eth;
utils: Utils;
toHex: (str: string | number) => string; toHex: (str: string | number) => string;
isAddress: (address: string) => boolean; isAddress: (address: string) => boolean;
@ -78,20 +94,33 @@ declare module 'web3' {
transactionIndex: number transactionIndex: number
} }
//declare function F_CardanoGetAddress(params: (P.$Common & CARDANO.$CardanoGetAddress)): Promise<CARDANO.CardanoGetAddress$>;
//declare function F_CardanoGetAddress(params: (P.$Common & { bundle: Array<CARDANO.$CardanoGetAddress> })): Promise<CARDANO.CardanoGetAddress$$>;
declare type PromiseEvent<T> = {
once: typeof F_PromiseEventOn;
on: typeof F_PromiseEventOn;
off: (type: string, callback: Function) => PromiseEvent<T>;
then: () => (result: T) => PromiseEvent<T>;
catch: () => (error: Error) => PromiseEvent<T>;
}
declare function F_PromiseEventOn<T>(type: 'transactionHash', callback: (hash: string) => void): PromiseEvent<T>;
declare function F_PromiseEventOn<T>(type: 'receipt', callback: (receipt: TransactionReceipt) => void): PromiseEvent<T>;
declare function F_PromiseEventOn<T>(type: 'confirmation', callback: (confirmations: number, receipt: TransactionReceipt) => void): PromiseEvent<T>;
declare function F_PromiseEventOn<T>(type: 'error', callback: (error: Error) => void): PromiseEvent<T>;
declare class Eth { declare class Eth {
getGasPrice: (callback: (error: Error, gasPrice: string) => void) => void, getBalance: (address: string) => Promise<string>;
getBalance: (address: string, callback: (error: Error, balance: BigNumber) => void) => void, getTransactionCount: (address: string) => Promise<number>;
getTransactionCount: (address: string, callback: (error: Error, result: number) => void) => void, estimateGas: (options: EstimateGasOptions) => Promise<number>;
getTransaction: (txid: string, callback: (error: Error, result: TransactionStatus) => void) => void, getGasPrice: () => Promise<string>;
getTransactionReceipt: (txid: string, callback: (error: Error, result: TransactionReceipt) => void) => void, getBlockNumber: () => Promise<number>;
getBlockNumber: (callback: (error: Error, blockNumber: number) => void) => void, Contract: (abi: Array<Object>, options?: any) => Contract;
getBlock: (hash: string, callback: (error: Error, result: any) => void) => void, sendSignedTransaction: (tx: string) => PromiseEvent<TransactionReceipt>;
// getAccounts: (callback: (error: Error, accounts: Array<EthereumAddressT>) => void) => void, getTransaction: (txid: string) => Promise<TransactionStatus>;
// sign: (payload: string, signer: EthereumAddressT) => Promise<string>, getTransactionReceipt: (txid: string) => Promise<TransactionReceipt>;
contract: (abi: Array<Object>) => ContractFactory, subscribe: (type: string, callback: Function) => any;
estimateGas: (options: EstimateGasOptions, callback: (error: ?Error, gas: ?number) => void) => void,
sendRawTransaction: (tx: any, callback: (error: Error, result: string) => void) => void,
filter: (type: string) => Filter; // return intance with "watch"
} }
declare export class Filter { declare export class Filter {
@ -99,108 +128,40 @@ declare module 'web3' {
stopWatching: (callback: any) => void, stopWatching: (callback: any) => void,
} }
declare export class ContractFactory { declare type ContractMethod<T> = {
// constructor(abi: Array<Object>); call: () => Promise<T>;
eth: Eth;
abi: Array<Object>;
at: (address: string, callback: ?(error: Error, contract: Contract) => void) => Contract; // TODO
} }
declare export class Contract { declare export class Contract {
name: { clone: () => Contract;
call: (callback: (error: Error, name: string) => void) => void;
}, options: {
symbol: { address: string;
call: (callback: (error: Error, symbol: string) => void) => void; jsonInterface: JSON;
}, };
decimals: {
call: (callback: (error: Error, decimals: BigNumber) => void) => void; methods: {
}, name: () => ContractMethod<string>;
balanceOf: (address: string, callback: (error: Error, balance: BigNumber) => void) => void, symbol: () => ContractMethod<string>;
transfer: any, decimals: () => ContractMethod<number>;
balanceOf: (address: string) => ContractMethod<string>;
transfer: (to: string, amount: any) => {
encodeABI: () => string;
}
};
}
declare class Utils {
toHex: (str: string | number) => string;
hexToNumberString: (str: string) => string;
isAddress: (address: string) => boolean;
toWei: (number: BigNumber, unit?: EthereumUnitT) => BigNumber;
toWei: (number: string, unit?: EthereumUnitT) => string;
toDecimal: (number: BigNumber) => number;
toDecimal: (number: string) => number;
soliditySha3: (payload: string | number | BigNumber | Object) => String;
} }
declare export default typeof Web3T; declare export default typeof Web3T;
} }
//
//
/*declare module 'web3' {
module.exports = {
eth: {
_requestManager: any;
iban: {
(iban: string): void;
fromAddress: (address: string) => any;
fromBban: (bban: string) => any;
createIndirect: (options: any) => any;
isValid: (iban: string) => boolean;
};
sendIBANTransaction: any;
contract: (abi: any) => {
eth: any;
abi: any[];
new: (...args: any[]) => {
_eth: any;
transactionHash: any;
address: any;
abi: any[];
};
at: (address: any, callback: Function) => any;
getData: (...args: any[]) => any;
};
filter: (fil: any, callback: any, filterCreationErrorCallback: any) => {
requestManager: any;
options: any;
implementation: {
[x: string]: any;
};
filterId: any;
callbacks: any[];
getLogsCallbacks: any[];
pollFilters: any[];
formatter: any;
watch: (callback: any) => any;
stopWatching: (callback: any) => any;
get: (callback: any) => any;
};
namereg: () => {
eth: any;
abi: any[];
new: (...args: any[]) => {
_eth: any;
transactionHash: any;
address: any;
abi: any[];
};
at: (address: any, callback: Function) => any;
getData: (...args: any[]) => any;
};
icapNamereg: () => {
eth: any;
abi: any[];
new: (...args: any[]) => {
_eth: any;
transactionHash: any;
address: any;
abi: any[];
};
at: (address: any, callback: Function) => any;
getData: (...args: any[]) => any;
};
isSyncing: (callback: any) => {
requestManager: any;
pollId: string;
callbacks: any[];
lastSyncState: boolean;
addCallback: (callback: any) => any;
stopWatching: () => void;
};
}
}
}
*/

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -4,7 +4,7 @@ import { render } from 'react-dom';
import baseStyles from 'support/BaseStyles'; import baseStyles from 'support/BaseStyles';
import { onBeforeUnload } from 'actions/WalletActions'; import { onBeforeUnload } from 'actions/WalletActions';
import App from 'views/index'; import App from 'views/index';
import store from 'store'; import store from './store';
const root: ?HTMLElement = document.getElementById('root'); const root: ?HTMLElement = document.getElementById('root');
if (root) { if (root) {

View File

@ -7,7 +7,6 @@ import * as ACCOUNT from 'actions/constants/account';
import type { Action, TrezorDevice } from 'flowtype'; import type { Action, TrezorDevice } from 'flowtype';
import type { import type {
AccountCreateAction,
AccountSetBalanceAction, AccountSetBalanceAction,
AccountSetNonceAction, AccountSetNonceAction,
} from 'actions/AccountsActions'; } from 'actions/AccountsActions';
@ -22,6 +21,8 @@ export type Account = {
+address: string; +address: string;
balance: string; balance: string;
nonce: number; nonce: number;
block: number;
transactions: number;
} }
export type State = Array<Account>; export type State = Array<Account>;
@ -37,28 +38,14 @@ export const findDeviceAccounts = (state: State, device: TrezorDevice, network:
return state.filter(addr => addr.deviceState === device.state); return state.filter(addr => addr.deviceState === device.state);
}; };
const createAccount = (state: State, action: AccountCreateAction): State => { const createAccount = (state: State, account: Account): State => {
// TODO check with device_id // TODO check with device_id
// check if account was created before // check if account was created before
// const exist: ?Account = state.find(account => account.address === action.address && account.network === action.network && action.device.features && account.deviceID === action.device.features.device_id); const exist: ?Account = state.find(a => a.address === account.address && a.network === account.network && a.deviceState === account.deviceState);
const exist: ?Account = state.find(account => account.address === action.address && account.network === action.network && account.deviceState === action.device.state);
if (exist) { if (exist) {
return state; return state;
} }
const newState: State = [ ...state ];
const account: Account = {
loaded: false,
network: action.network,
deviceID: action.device.features ? action.device.features.device_id : '0',
deviceState: action.device.state || 'undefined',
index: action.index,
addressPath: action.path,
address: action.address,
balance: '0',
nonce: 0,
};
const newState: State = [...state];
newState.push(account); newState.push(account);
return newState; return newState;
}; };
@ -74,8 +61,16 @@ const clear = (state: State, devices: Array<TrezorDevice>): State => {
return newState; return newState;
}; };
const updateAccount = (state: State, account: Account): State => {
const index: number = state.findIndex(a => a.address === account.address && a.network === account.network && a.deviceState === account.deviceState);
const newState: State = [...state];
newState[index] = account;
return newState;
}
const setBalance = (state: State, action: AccountSetBalanceAction): State => { const setBalance = (state: State, action: AccountSetBalanceAction): State => {
const index: number = state.findIndex(account => account.address === action.address && account.network === action.network && account.deviceState === action.deviceState); // const index: number = state.findIndex(account => account.address === action.address && account.network === action.network && account.deviceState === action.deviceState);
const index: number = state.findIndex(account => account.address === action.address && account.network === action.network);
const newState: State = [...state]; const newState: State = [...state];
newState[index].loaded = true; newState[index].loaded = true;
newState[index].balance = action.balance; newState[index].balance = action.balance;
@ -93,7 +88,7 @@ const setNonce = (state: State, action: AccountSetNonceAction): State => {
export default (state: State = initialState, action: Action): State => { export default (state: State = initialState, action: Action): State => {
switch (action.type) { switch (action.type) {
case ACCOUNT.CREATE: case ACCOUNT.CREATE:
return createAccount(state, action); return createAccount(state, action.payload);
case CONNECT.FORGET: case CONNECT.FORGET:
case CONNECT.FORGET_SINGLE: case CONNECT.FORGET_SINGLE:
@ -105,6 +100,9 @@ export default (state: State = initialState, action: Action): State => {
//case CONNECT.FORGET_SINGLE : //case CONNECT.FORGET_SINGLE :
// return forgetAccounts(state, action); // return forgetAccounts(state, action);
case ACCOUNT.UPDATE :
return updateAccount(state, action.payload);
case ACCOUNT.SET_BALANCE: case ACCOUNT.SET_BALANCE:
return setBalance(state, action); return setBalance(state, action);
case ACCOUNT.SET_NONCE: case ACCOUNT.SET_NONCE:

View File

@ -0,0 +1,64 @@
/* @flow */
import { BLOCKCHAIN } from 'trezor-connect';
import type { Action } from 'flowtype';
export type BlockchainNetwork = {
+name: string;
connected: boolean;
}
export type State = Array<BlockchainNetwork>;
export const initialState: State = [];
const find = (state: State, name: string): number => {
return state.findIndex(b => b.name === name);
}
const connect = (state: State, action: any): State => {
const name = action.payload.coin.shortcut.toLowerCase();
const network: BlockchainNetwork = {
name,
connected: true,
}
const newState: State = [...state];
const index: number = find(newState, name);
if (index >= 0) {
newState[index] = network;
} else {
newState.push(network);
}
return newState;
};
const disconnect = (state: State, action: any): State => {
const name = action.payload.coin.shortcut.toLowerCase();
const network: BlockchainNetwork = {
name,
connected: false,
}
const newState: State = [...state];
const index: number = find(newState, name);
if (index >= 0) {
newState[index] = network;
} else {
newState.push(network);
}
return newState;
};
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);
default:
return state;
}
};

View File

@ -16,9 +16,7 @@ import type {
DiscoveryCompleteAction, DiscoveryCompleteAction,
} from 'actions/DiscoveryActions'; } from 'actions/DiscoveryActions';
import type { import type { Account } from './AccountsReducer';
AccountCreateAction,
} from 'actions/AccountsActions';
export type Discovery = { export type Discovery = {
network: string; network: string;
@ -31,7 +29,7 @@ export type Discovery = {
interrupted: boolean; interrupted: boolean;
completed: boolean; completed: boolean;
waitingForDevice: boolean; waitingForDevice: boolean;
waitingForBackend: boolean; waitingForBlockchain: boolean;
} }
export type State = Array<Discovery>; export type State = Array<Discovery>;
@ -55,7 +53,7 @@ const start = (state: State, action: DiscoveryStartAction): State => {
interrupted: false, interrupted: false,
completed: false, completed: false,
waitingForDevice: false, waitingForDevice: false,
waitingForBackend: false, waitingForBlockchain: false,
}; };
const newState: State = [...state]; const newState: State = [...state];
@ -75,8 +73,8 @@ const complete = (state: State, action: DiscoveryCompleteAction): State => {
return newState; return newState;
}; };
const accountCreate = (state: State, action: AccountCreateAction): State => { const accountCreate = (state: State, account: Account): State => {
const index: number = findIndex(state, action.network, action.device.state || '0'); const index: number = findIndex(state, account.network, account.deviceState);
const newState: State = [...state]; const newState: State = [...state];
newState[index].accountIndex++; newState[index].accountIndex++;
return newState; return newState;
@ -98,6 +96,7 @@ const stop = (state: State, action: DiscoveryStopAction): State => {
if (d.deviceState === action.device.state && !d.completed) { if (d.deviceState === action.device.state && !d.completed) {
d.interrupted = true; d.interrupted = true;
d.waitingForDevice = false; d.waitingForDevice = false;
d.waitingForBlockchain = false;
} }
return d; return d;
}); });
@ -116,7 +115,7 @@ const waitingForDevice = (state: State, action: DiscoveryWaitingAction): State =
interrupted: false, interrupted: false,
completed: false, completed: false,
waitingForDevice: true, waitingForDevice: true,
waitingForBackend: false, waitingForBlockchain: false,
}; };
const index: number = findIndex(state, action.network, deviceState); const index: number = findIndex(state, action.network, deviceState);
@ -130,7 +129,7 @@ const waitingForDevice = (state: State, action: DiscoveryWaitingAction): State =
return newState; return newState;
}; };
const waitingForBackend = (state: State, action: DiscoveryWaitingAction): State => { const waitingForBlockchain = (state: State, action: DiscoveryWaitingAction): State => {
const deviceState: string = action.device.state || '0'; const deviceState: string = action.device.state || '0';
const instance: Discovery = { const instance: Discovery = {
network: action.network, network: action.network,
@ -143,7 +142,7 @@ const waitingForBackend = (state: State, action: DiscoveryWaitingAction): State
interrupted: false, interrupted: false,
completed: false, completed: false,
waitingForDevice: false, waitingForDevice: false,
waitingForBackend: true, waitingForBlockchain: true,
}; };
const index: number = findIndex(state, action.network, deviceState); const index: number = findIndex(state, action.network, deviceState);
@ -162,15 +161,15 @@ export default function discovery(state: State = initialState, action: Action):
case DISCOVERY.START: case DISCOVERY.START:
return start(state, action); return start(state, action);
case ACCOUNT.CREATE: case ACCOUNT.CREATE:
return accountCreate(state, action); return accountCreate(state, action.payload);
case DISCOVERY.STOP: case DISCOVERY.STOP:
return stop(state, action); return stop(state, action);
case DISCOVERY.COMPLETE: case DISCOVERY.COMPLETE:
return complete(state, action); return complete(state, action);
case DISCOVERY.WAITING_FOR_DEVICE: case DISCOVERY.WAITING_FOR_DEVICE:
return waitingForDevice(state, action); return waitingForDevice(state, action);
case DISCOVERY.WAITING_FOR_BACKEND: case DISCOVERY.WAITING_FOR_BLOCKCHAIN:
return waitingForBackend(state, action); return waitingForBlockchain(state, action);
case DISCOVERY.FROM_STORAGE: case DISCOVERY.FROM_STORAGE:
return action.payload.map((d) => { return action.payload.map((d) => {
const hdKey: HDKey = new HDKey(); const hdKey: HDKey = new HDKey();
@ -181,7 +180,7 @@ export default function discovery(state: State = initialState, action: Action):
hdKey, hdKey,
interrupted: false, interrupted: false,
waitingForDevice: false, waitingForDevice: false,
waitingForBackend: false, waitingForBlockchain: false,
}; };
}); });
case CONNECT.FORGET: case CONNECT.FORGET:

View File

@ -22,7 +22,8 @@ export type Coin = {
backends: Array<{ backends: Array<{
name: string; name: string;
urls: Array<string>; urls: Array<string>;
}> }>;
web3: Array<string>;
} }
export type NetworkToken = { export type NetworkToken = {

View File

@ -6,6 +6,7 @@ import type { Action } from 'flowtype';
import type { SendTxAction } from 'actions/SendFormActions'; import type { SendTxAction } from 'actions/SendFormActions';
export type PendingTx = { export type PendingTx = {
+type: 'send' | 'recv';
+id: string; +id: string;
+network: string; +network: string;
+currency: string; +currency: string;
@ -24,6 +25,7 @@ const initialState: State = [];
const add = (state: State, action: SendTxAction): State => { const add = (state: State, action: SendTxAction): State => {
const newState = [...state]; const newState = [...state];
newState.push({ newState.push({
type: 'send',
id: action.txid, id: action.txid,
network: action.account.network, network: action.account.network,
currency: action.selectedCurrency, currency: action.selectedCurrency,
@ -37,6 +39,12 @@ const add = (state: State, action: SendTxAction): State => {
return newState; return newState;
}; };
const add_NEW = (state: State, payload: any): State => {
const newState = [...state];
newState.push(payload);
return newState;
};
const remove = (state: State, id: string): State => state.filter(tx => tx.id !== id); const remove = (state: State, id: string): State => state.filter(tx => tx.id !== id);
const reject = (state: State, id: string): State => state.map((tx) => { const reject = (state: State, id: string): State => state.map((tx) => {
@ -51,6 +59,8 @@ export default function pending(state: State = initialState, action: Action): St
case SEND.TX_COMPLETE: case SEND.TX_COMPLETE:
return add(state, action); return add(state, action);
// case PENDING.ADD:
// return add(state, action.payload);
case PENDING.TX_RESOLVED: case PENDING.TX_RESOLVED:
return remove(state, action.tx.id); return remove(state, action.tx.id);
case PENDING.TX_NOT_FOUND: case PENDING.TX_NOT_FOUND:

View File

@ -10,7 +10,6 @@ import type {
Token, Token,
PendingTx, PendingTx,
Discovery, Discovery,
Web3Instance,
} from 'flowtype'; } from 'flowtype';
export type State = { export type State = {
@ -20,7 +19,6 @@ export type State = {
network: ?Coin; network: ?Coin;
tokens: Array<Token>, tokens: Array<Token>,
pending: Array<PendingTx>, pending: Array<PendingTx>,
web3: ?Web3Instance,
discovery: ?Discovery discovery: ?Discovery
}; };
@ -30,7 +28,6 @@ export const initialState: State = {
network: null, network: null,
tokens: [], tokens: [],
pending: [], pending: [],
web3: null,
discovery: null, discovery: null,
}; };

View File

@ -2,9 +2,8 @@
import { LOCATION_CHANGE } from 'react-router-redux'; import { LOCATION_CHANGE } from 'react-router-redux';
import { DEVICE } from 'trezor-connect'; import { DEVICE, TRANSPORT } from 'trezor-connect';
import * as MODAL from 'actions/constants/modal'; import * as MODAL from 'actions/constants/modal';
import * as WEB3 from 'actions/constants/web3';
import * as WALLET from 'actions/constants/wallet'; import * as WALLET from 'actions/constants/wallet';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
@ -41,7 +40,7 @@ export default function wallet(state: State = initialState, action: Action): Sta
initialPathname: action.pathname, initialPathname: action.pathname,
}; };
case WEB3.READY: case TRANSPORT.START:
return { return {
...state, ...state,
ready: true, ready: true,

View File

@ -3,7 +3,8 @@
import Web3 from 'web3'; import Web3 from 'web3';
import type { ContractFactory } from 'web3'; import type { Contract } from 'web3';
import * as STORAGE from 'actions/constants/localStorage';
import * as WEB3 from 'actions/constants/web3'; import * as WEB3 from 'actions/constants/web3';
import type { Action } from 'flowtype'; import type { Action } from 'flowtype';
@ -18,7 +19,7 @@ export type Web3Instance = {
chainId: number; chainId: number;
latestBlock: any; latestBlock: any;
gasPrice: string; gasPrice: string;
erc20: ContractFactory; erc20: Contract;
} }
export type State = Array<Web3Instance>; export type State = Array<Web3Instance>;
@ -26,8 +27,13 @@ export type State = Array<Web3Instance>;
const initialState: State = []; const initialState: State = [];
const createWeb3 = (state: State, instance: Web3Instance): State => { const createWeb3 = (state: State, instance: Web3Instance): State => {
const index: number = state.findIndex(w3 => w3.network === instance.network);
const newState: Array<Web3Instance> = [...state]; const newState: Array<Web3Instance> = [...state];
newState.push(instance); if (index >= 0) {
newState[index] = instance;
} else {
newState.push(instance);
}
return newState; return newState;
}; };
@ -45,6 +51,13 @@ const updateGasPrice = (state: State, action: Web3UpdateGasPriceAction): State =
return newState; return newState;
}; };
const disconnect = (state: State, instance: Web3Instance): State => {
const index: number = state.indexOf(instance);
const newState: Array<Web3Instance> = [...state];
newState.splice(index, 1);
return newState;
};
export default function web3(state: State = initialState, action: Action): State { export default function web3(state: State = initialState, action: Action): State {
switch (action.type) { switch (action.type) {
case WEB3.CREATE: case WEB3.CREATE:
@ -53,6 +66,8 @@ export default function web3(state: State = initialState, action: Action): State
return updateLatestBlock(state, action); return updateLatestBlock(state, action);
case WEB3.GAS_PRICE_UPDATED: case WEB3.GAS_PRICE_UPDATED:
return updateGasPrice(state, action); return updateGasPrice(state, action);
case WEB3.DISCONNECT:
return disconnect(state, action.instance);
default: default:
return state; return state;
} }

View File

@ -19,6 +19,7 @@ import pending from 'reducers/PendingTxReducer';
import fiat from 'reducers/FiatRateReducer'; import fiat from 'reducers/FiatRateReducer';
import wallet from 'reducers/WalletReducer'; import wallet from 'reducers/WalletReducer';
import devices from 'reducers/DevicesReducer'; import devices from 'reducers/DevicesReducer';
import blockchain from 'reducers/BlockchainReducer';
const reducers = { const reducers = {
router: routerReducer, router: routerReducer,
@ -39,6 +40,7 @@ const reducers = {
fiat, fiat,
wallet, wallet,
devices, devices,
blockchain
}; };
export type Reducers = typeof reducers; export type Reducers = typeof reducers;

View File

@ -72,7 +72,6 @@ const RouterService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
__unloading = true; __unloading = true;
} else if (action.type === LOCATION_CHANGE && !__unloading) { } else if (action.type === LOCATION_CHANGE && !__unloading) {
const { location } = api.getState().router; const { location } = api.getState().router;
const { web3 } = api.getState();
const { devices } = api.getState(); const { devices } = api.getState();
const { error } = api.getState().connect; const { error } = api.getState().connect;
@ -92,8 +91,8 @@ const RouterService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
redirectPath = '/'; redirectPath = '/';
} else { } else {
const isModalOpened: boolean = api.getState().modal.opened; const isModalOpened: boolean = api.getState().modal.opened;
// if web3 wasn't initialized yet or there are no devices attached or initialization error occurs // there are no devices attached or initialization error occurs
const landingPage: boolean = web3.length < 1 || devices.length < 1 || error !== null; const landingPage: boolean = devices.length < 1 || error !== null;
// modal is still opened and currentPath is still valid // modal is still opened and currentPath is still valid
// example 1 (valid blocking): url changes while passphrase modal opened but device is still connected (we want user to finish this action) // example 1 (valid blocking): url changes while passphrase modal opened but device is still connected (we want user to finish this action)

View File

@ -1,16 +1,16 @@
/* @flow */ /* @flow */
import { push } from 'react-router-redux'; import { push } from 'react-router-redux';
import { import TrezorConnect, {
TRANSPORT, DEVICE, TRANSPORT, DEVICE_EVENT, UI_EVENT, UI, DEVICE, BLOCKCHAIN
} from 'trezor-connect'; } from 'trezor-connect';
import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as TrezorConnectActions from 'actions/TrezorConnectActions';
import * as DiscoveryActions from 'actions/DiscoveryActions'; import * as DiscoveryActions from 'actions/DiscoveryActions';
import * as BlockchainActions from 'actions/BlockchainActions';
import * as ModalActions from 'actions/ModalActions'; import * as ModalActions from 'actions/ModalActions';
import { init as initWeb3 } from 'actions/Web3Actions';
import * as WEB3 from 'actions/constants/web3';
import * as STORAGE from 'actions/constants/localStorage'; import * as STORAGE from 'actions/constants/localStorage';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
import { READY as BLOCKCHAIN_READY } from 'actions/constants/blockchain';
import type { import type {
Middleware, Middleware,
@ -32,9 +32,9 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
} else if (action.type === TRANSPORT.ERROR) { } else if (action.type === TRANSPORT.ERROR) {
// TODO: check if modal is open // TODO: check if modal is open
// api.dispatch( push('/') ); // api.dispatch( push('/') );
} else if (action.type === TRANSPORT.START && api.getState().web3.length < 1) { } else if (action.type === TRANSPORT.START) {
api.dispatch(initWeb3()); api.dispatch(BlockchainActions.init());
} else if (action.type === WEB3.READY) { } else if (action.type === BLOCKCHAIN_READY) {
api.dispatch(TrezorConnectActions.postInit()); api.dispatch(TrezorConnectActions.postInit());
} else if (action.type === DEVICE.DISCONNECT) { } else if (action.type === DEVICE.DISCONNECT) {
api.dispatch(TrezorConnectActions.deviceDisconnect(action.device)); api.dispatch(TrezorConnectActions.deviceDisconnect(action.device));
@ -63,6 +63,12 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
api.dispatch(TrezorConnectActions.onSelectDevice(action.device)); api.dispatch(TrezorConnectActions.onSelectDevice(action.device));
} else if (action.type === CONNECT.COIN_CHANGED) { } else if (action.type === CONNECT.COIN_CHANGED) {
api.dispatch(TrezorConnectActions.coinChanged(action.payload.network)); api.dispatch(TrezorConnectActions.coinChanged(action.payload.network));
} else if (action.type === BLOCKCHAIN.BLOCK) {
api.dispatch(BlockchainActions.onBlockMined(action.payload.coin));
} else if (action.type === BLOCKCHAIN.NOTIFICATION) {
// api.dispatch(BlockchainActions.onNotification(action.payload));
} else if (action.type === BLOCKCHAIN.ERROR) {
api.dispatch( BlockchainActions.error(action.payload) );
} }
return action; return action;

View File

@ -6,13 +6,14 @@ import thunk from 'redux-thunk';
// import createHistory from 'history/createBrowserHistory'; // import createHistory from 'history/createBrowserHistory';
// import { useRouterHistory } from 'react-router'; // import { useRouterHistory } from 'react-router';
import createHistory from 'history/createHashHistory'; import createHistory from 'history/createHashHistory';
import { createLogger } from 'redux-logger';
import reducers from 'reducers'; import reducers from 'reducers';
import services from 'services'; import services from 'services';
import Raven from 'raven-js'; import Raven from 'raven-js';
import RavenMiddleware from 'redux-raven-middleware'; import RavenMiddleware from 'redux-raven-middleware';
// import type { Action, GetState, Store } from 'flowtype'; import type { Action, GetState, Store } from 'flowtype';
export const history: History = createHistory({ queryKey: false }); export const history: History = createHistory({ queryKey: false });
@ -30,17 +31,29 @@ const middleware = [
let composedEnhancers: any; let composedEnhancers: any;
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
// const excludeLogger = (getState: GetState, action: Action): boolean => { const excludeLogger = (getState: GetState, action: Action): boolean => {
// //'@@router/LOCATION_CHANGE' //'@@router/LOCATION_CHANGE'
// const excluded: Array<string> = ['LOG_TO_EXCLUDE', 'log__add']; const excluded: Array<?string> = ['LOG_TO_EXCLUDE', 'log__add', undefined];
// const pass: Array<string> = excluded.filter(act => action.type === act); const pass: Array<?string> = excluded.filter(act => action.type === act);
// return pass.length === 0; return pass.length === 0;
// }; };
const logger = createLogger({
level: 'info',
predicate: excludeLogger,
collapsed: true,
});
const devToolsExtension: ?Function = window.devToolsExtension;
if (typeof devToolsExtension === 'function') {
enhancers.push(devToolsExtension());
}
composedEnhancers = compose( composedEnhancers = compose(
applyMiddleware(...middleware, ...services), applyMiddleware(logger, ...middleware, ...services),
...enhancers, ...enhancers,
); );
} else { } else {
composedEnhancers = compose( composedEnhancers = compose(
applyMiddleware(...middleware, ...services), applyMiddleware(...middleware, ...services),

View File

@ -4,7 +4,7 @@
// import root from 'window-or-global'; // import root from 'window-or-global';
// import Promise from 'es6-promise'; // import Promise from 'es6-promise';
export async function resolveAfter<T>(msec: number, value: any = null): Promise<T> { export async function resolveAfter(msec: number, value?: any): Promise<any> {
await new Promise((resolve) => { await new Promise((resolve) => {
//root.setTimeout(resolve, msec, value); //root.setTimeout(resolve, msec, value);
window.setTimeout(resolve, msec, value); window.setTimeout(resolve, msec, value);

View File

@ -8,7 +8,6 @@ import LandingPage from './index';
export type StateProps = { export type StateProps = {
localStorage: $ElementType<State, 'localStorage'>, localStorage: $ElementType<State, 'localStorage'>,
modal: $ElementType<State, 'modal'>, modal: $ElementType<State, 'modal'>,
web3: $ElementType<State, 'web3'>,
wallet: $ElementType<State, 'wallet'>, wallet: $ElementType<State, 'wallet'>,
connect: $ElementType<State, 'connect'>, connect: $ElementType<State, 'connect'>,
router: $ElementType<State, 'router'>, router: $ElementType<State, 'router'>,
@ -29,7 +28,6 @@ export type Props = StateProps & DispatchProps;
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
localStorage: state.localStorage, localStorage: state.localStorage,
modal: state.modal, modal: state.modal,
web3: state.web3,
wallet: state.wallet, wallet: state.wallet,
connect: state.connect, connect: state.connect,
router: state.router, router: state.router,

View File

@ -103,7 +103,6 @@ export default class InstallBridge extends Component<Props, State> {
} }
onChange(value: InstallTarget) { onChange(value: InstallTarget) {
console.warn(value);
this.setState({ this.setState({
target: value, target: value,
}); });

View File

@ -113,6 +113,9 @@ const AccountMenu = (props: Props): ?React$Element<string> => {
const { config } = props.localStorage; const { config } = props.localStorage;
const selectedCoin = config.coins.find(c => c.network === location.state.network); const selectedCoin = config.coins.find(c => c.network === location.state.network);
if (!selected || !selectedCoin) return;
const fiatRate = props.fiat.find(f => f.network === selectedCoin.network); const fiatRate = props.fiat.find(f => f.network === selectedCoin.network);
const deviceAccounts: Accounts = findDeviceAccounts(accounts, selected, location.state.network); const deviceAccounts: Accounts = findDeviceAccounts(accounts, selected, location.state.network);

View File

@ -1,6 +1,7 @@
/* @flow */ /* @flow */
import * as React from 'react'; import * as React from 'react';
import { Notification } from 'components/Notification'; import { Notification } from 'components/Notification';
import { reconnect } from 'actions/DiscoveryActions';
import type { State } from 'flowtype'; import type { State } from 'flowtype';
@ -8,11 +9,12 @@ export type StateProps = {
className: string; className: string;
selectedAccount: $ElementType<State, 'selectedAccount'>, selectedAccount: $ElementType<State, 'selectedAccount'>,
wallet: $ElementType<State, 'wallet'>, wallet: $ElementType<State, 'wallet'>,
blockchain: $ElementType<State, 'blockchain'>,
children?: React.Node children?: React.Node
} }
export type DispatchProps = { export type DispatchProps = {
blockchainReconnect: typeof reconnect;
} }
export type Props = StateProps & DispatchProps; export type Props = StateProps & DispatchProps;
@ -28,8 +30,28 @@ const SelectedAccount = (props: Props) => {
const { const {
account, account,
discovery, discovery,
network
} = accountState; } = accountState;
if (!network) return; // TODO: this shouldn't happen. change accountState reducer?
const blockchain = props.blockchain.find(b => b.name === network.network);
if (blockchain && !blockchain.connected) {
return (
<Notification
type="error"
title="Backend not connected"
actions={
[{
label: "Try again",
callback: async () => {
await props.blockchainReconnect(network.network);
}
}]
} />
);
}
// account not found (yet). checking why... // account not found (yet). checking why...
if (!account) { if (!account) {
if (!discovery || discovery.waitingForDevice) { if (!discovery || discovery.waitingForDevice) {
@ -57,11 +79,6 @@ const SelectedAccount = (props: Props) => {
message="Connect device to load accounts" message="Connect device to load accounts"
/> />
); );
} if (discovery.waitingForBackend) {
// case 4: backend is not working
return (
<Notification type="warning" title="Backend not working" />
);
} if (discovery.completed) { } if (discovery.completed) {
// case 5: account not found and discovery is completed // case 5: account not found and discovery is completed
return ( return (

View File

@ -42,7 +42,7 @@ const TopNavigationAccount = (props) => {
const basePath = `/device/${urlParams.device}/network/${urlParams.network}/account/${urlParams.account}`; const basePath = `/device/${urlParams.device}/network/${urlParams.network}/account/${urlParams.account}`;
return ( return (
<Wrapper> <Wrapper className="account-tabs">
<StyledNavLink exact to={`${basePath}`}>Summary</StyledNavLink> <StyledNavLink exact to={`${basePath}`}>Summary</StyledNavLink>
<StyledNavLink to={`${basePath}/send`}>Send</StyledNavLink> <StyledNavLink to={`${basePath}/send`}>Send</StyledNavLink>
<StyledNavLink to={`${basePath}/receive`}>Receive</StyledNavLink> <StyledNavLink to={`${basePath}/receive`}>Receive</StyledNavLink>

View File

@ -20,7 +20,7 @@ import TopNavigationAccount from './components/TopNavigationAccount';
import TopNavigationDeviceSettings from './components/TopNavigationDeviceSettings'; import TopNavigationDeviceSettings from './components/TopNavigationDeviceSettings';
type WalletContainerProps = { type WalletContainerProps = {
// wallet: $ElementType<State, 'wallet'>, wallet: $ElementType<State, 'wallet'>,
children?: React.Node children?: React.Node
} }

View File

@ -2,6 +2,7 @@
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { reconnect } from 'actions/DiscoveryActions';
import { showAddress } from 'actions/ReceiveActions'; import { showAddress } from 'actions/ReceiveActions';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype'; import type { State, Dispatch } from 'flowtype';
@ -28,12 +29,14 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
className: 'receive', className: 'receive',
selectedAccount: state.selectedAccount, selectedAccount: state.selectedAccount,
wallet: state.wallet, wallet: state.wallet,
blockchain: state.blockchain,
receive: state.receive, receive: state.receive,
modal: state.modal, modal: state.modal,
}); });
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({ const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
blockchainReconnect: bindActionCreators(reconnect, dispatch),
showAddress: bindActionCreators(showAddress, dispatch), showAddress: bindActionCreators(showAddress, dispatch),
}); });

View File

@ -5,6 +5,7 @@ import * as React from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { reconnect } from 'actions/DiscoveryActions';
import SendFormActions from 'actions/SendFormActions'; import SendFormActions from 'actions/SendFormActions';
import * as SessionStorageActions from 'actions/SessionStorageActions'; import * as SessionStorageActions from 'actions/SessionStorageActions';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
@ -12,7 +13,6 @@ import type { State, Dispatch } from 'flowtype';
import type { StateProps as BaseStateProps, DispatchProps as BaseDispatchProps } from 'views/Wallet/components/SelectedAccount'; import type { StateProps as BaseStateProps, DispatchProps as BaseDispatchProps } from 'views/Wallet/components/SelectedAccount';
import AccountSend from './index'; import AccountSend from './index';
type OwnProps = { } type OwnProps = { }
export type StateProps = BaseStateProps & { export type StateProps = BaseStateProps & {
@ -33,6 +33,7 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
className: 'send-from', className: 'send-from',
selectedAccount: state.selectedAccount, selectedAccount: state.selectedAccount,
wallet: state.wallet, wallet: state.wallet,
blockchain: state.blockchain,
sendForm: state.sendForm, sendForm: state.sendForm,
fiat: state.fiat, fiat: state.fiat,
@ -40,6 +41,7 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
}); });
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({ const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
blockchainReconnect: bindActionCreators(reconnect, dispatch),
sendFormActions: bindActionCreators(SendFormActions, dispatch), sendFormActions: bindActionCreators(SendFormActions, dispatch),
saveSessionStorage: bindActionCreators(SessionStorageActions.save, dispatch), saveSessionStorage: bindActionCreators(SessionStorageActions.save, dispatch),
}); });

View File

@ -237,7 +237,7 @@ class AccountSend extends Component<Props, State> {
} }
getTokensSelectData(tokens: Array<Token>, accountNetwork: any) { getTokensSelectData(tokens: Array<Token>, accountNetwork: any) {
const tokensSelectData = tokens.map(t => ({ value: t.symbol, label: t.symbol })); const tokensSelectData: Array<{ value: string, label: string }> = tokens.map(t => ({ value: t.symbol, label: t.symbol }));
tokensSelectData.unshift({ value: accountNetwork.symbol, label: accountNetwork.symbol }); tokensSelectData.unshift({ value: accountNetwork.symbol, label: accountNetwork.symbol });
return tokensSelectData; return tokensSelectData;

View File

@ -3,6 +3,7 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import { reconnect } from 'actions/DiscoveryActions';
import * as TokenActions from 'actions/TokenActions'; import * as TokenActions from 'actions/TokenActions';
import type { State, Dispatch } from 'flowtype'; import type { State, Dispatch } from 'flowtype';
@ -30,6 +31,7 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
className: 'summary', className: 'summary',
selectedAccount: state.selectedAccount, selectedAccount: state.selectedAccount,
wallet: state.wallet, wallet: state.wallet,
blockchain: state.blockchain,
tokens: state.tokens, tokens: state.tokens,
summary: state.summary, summary: state.summary,
@ -38,6 +40,7 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
}); });
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({ const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
blockchainReconnect: bindActionCreators(reconnect, dispatch),
addToken: bindActionCreators(TokenActions.add, dispatch), addToken: bindActionCreators(TokenActions.add, dispatch),
loadTokens: bindActionCreators(TokenActions.load, dispatch), loadTokens: bindActionCreators(TokenActions.load, dispatch),
removeToken: bindActionCreators(TokenActions.remove, dispatch), removeToken: bindActionCreators(TokenActions.remove, dispatch),

View File

@ -97,7 +97,7 @@ class AccountBalance extends Component<Props, State> {
let fiat = ''; let fiat = '';
if (fiatRate) { if (fiatRate) {
accountBalance = new BigNumber(this.props.balance); accountBalance = new BigNumber(this.props.balance);
fiatRateValue = new BigNumber(fiatRate.value); fiatRateValue = new BigNumber(fiatRate.value).toFixed(2);
fiat = accountBalance.times(fiatRateValue).toFixed(2); fiat = accountBalance.times(fiatRateValue).toFixed(2);
} }
@ -127,7 +127,7 @@ class AccountBalance extends Component<Props, State> {
{fiatRate && ( {fiatRate && (
<BalanceWrapper> <BalanceWrapper>
<Label>Rate</Label> <Label>Rate</Label>
<FiatValue>${fiatRateValue.toFixed(2)}</FiatValue> <FiatValue>${fiatRateValue}</FiatValue>
<CoinBalace>1.00 {selectedCoin.symbol}</CoinBalace> <CoinBalace>1.00 {selectedCoin.symbol}</CoinBalace>
</BalanceWrapper> </BalanceWrapper>
)} )}

View File

@ -69,7 +69,7 @@ const AccountSummary = (props: Props) => {
} = props.selectedAccount; } = props.selectedAccount;
// flow // flow
if (!device || !account || !network) return null; if (!device || !account || !network) return <SelectedAccount {...props} />;
const explorerLink: string = `${network.explorer.address}${account.address}`; const explorerLink: string = `${network.explorer.address}${account.address}`;
const pendingAmount: BigNumber = stateUtils.getPendingAmount(pending, network.symbol); const pendingAmount: BigNumber = stateUtils.getPendingAmount(pending, network.symbol);

View File

@ -2,7 +2,7 @@ import webpack from 'webpack';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import HtmlWebpackPlugin from 'html-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin';
import CopyWebpackPlugin from 'copy-webpack-plugin'; import CopyWebpackPlugin from 'copy-webpack-plugin';
import FlowWebpackPlugin from 'flow-webpack-plugin'; import MiniCssExtractPlugin from '../../trezor-connect/node_modules/mini-css-extract-plugin';
import { import {
TREZOR_CONNECT_ROOT, TREZOR_CONNECT_ROOT,
@ -48,21 +48,17 @@ module.exports = {
{ {
test: /\.js?$/, test: /\.js?$/,
exclude: /node_modules/, exclude: /node_modules/,
use: ['babel-loader'],
},
{
test: /\.less$/,
use: [ use: [
'babel-loader',
{ {
loader: 'eslint-loader', loader: MiniCssExtractPlugin.loader,
options: { options: { publicPath: '../' },
emitWarning: true,
},
},
{
loader: 'stylelint-custom-processor-loader',
options: {
emitWarning: true,
configPath: '.stylelintrc',
},
}, },
`${TREZOR_CONNECT_ROOT}/node_modules/css-loader`,
`${TREZOR_CONNECT_ROOT}/node_modules/less-loader`,
], ],
}, },
{ {
@ -115,7 +111,10 @@ module.exports = {
hints: false, hints: false,
}, },
plugins: [ plugins: [
new FlowWebpackPlugin(), new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
}),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
chunks: ['index'], chunks: ['index'],
template: `${SRC}index.html`, template: `${SRC}index.html`,

955
yarn.lock

File diff suppressed because it is too large Load Diff