Merge pull request #625 from trezor/feature/passphrase-redesign

[WIP] Feature/passphrase redesign
release/1.4.0-beta
Vladimir Volek 4 years ago committed by GitHub
commit bb6a13812b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,5 @@
[include] [include]
.*/node_modules/hd-wallet/lib
[ignore] [ignore]
.*/node_modules/fbjs/.* .*/node_modules/fbjs/.*
@ -7,6 +8,9 @@
.*/node_modules/redux/.* .*/node_modules/redux/.*
.*/node_modules/react-router/.* .*/node_modules/react-router/.*
.*/node_modules/oboe/test/.* .*/node_modules/oboe/test/.*
.*/node_modules/trezor-link/lib/lowlevel/webusb.js.flow
.*/node_modules/hd-wallet/lib/utils/stream.js.flow
.*/node_modules/protobufjs-old-fixed-webpack/src/bower.json
.*/_old/.* .*/_old/.*
.*/scripts/solidity/.* .*/scripts/solidity/.*
.*/build/.* .*/build/.*
@ -19,7 +23,6 @@
./src/flowtype/npm/react-router-dom_v4.x.x.js ./src/flowtype/npm/react-router-dom_v4.x.x.js
; ./src/flowtype/npm/react-intl_v2.x.x.js // TODO: uncomment to get proper flow support for intl (needs some flow fixing) ; ./src/flowtype/npm/react-intl_v2.x.x.js // TODO: uncomment to get proper flow support for intl (needs some flow fixing)
./src/flowtype/npm/connected-react-router.js ./src/flowtype/npm/connected-react-router.js
./src/flowtype/npm/bignumber.js
./src/flowtype/npm/ethereum-types.js ./src/flowtype/npm/ethereum-types.js
./src/flowtype/npm/web3.js ./src/flowtype/npm/web3.js
./src/flowtype/css.js ./src/flowtype/css.js

@ -34,7 +34,7 @@
"dependencies": { "dependencies": {
"@babel/polyfill": "^7.2.5", "@babel/polyfill": "^7.2.5",
"@hot-loader/react-dom": "16.8.6", "@hot-loader/react-dom": "16.8.6",
"bignumber.js": "8.0.2", "bignumber.js": "9.0.0",
"color-hash": "^1.0.3", "color-hash": "^1.0.3",
"commander": "^2.19.0", "commander": "^2.19.0",
"connected-react-router": "6.4.0", "connected-react-router": "6.4.0",
@ -47,7 +47,6 @@
"ethereumjs-util": "^6.0.0", "ethereumjs-util": "^6.0.0",
"express": "^4.16.4", "express": "^4.16.4",
"git-revision-webpack-plugin": "^3.0.3", "git-revision-webpack-plugin": "^3.0.3",
"hdkey": "^1.1.0",
"history": "^4.7.2", "history": "^4.7.2",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"jest-fetch-mock": "^2.1.0", "jest-fetch-mock": "^2.1.0",
@ -81,7 +80,7 @@
"styled-components": "^4.1.3", "styled-components": "^4.1.3",
"styled-normalize": "^8.0.6", "styled-normalize": "^8.0.6",
"trezor-bridge-communicator": "1.0.2", "trezor-bridge-communicator": "1.0.2",
"trezor-connect": "7.0.2", "trezor-connect": "8.1.0-beta.8",
"trezor-ui-components": "^1.0.0-beta.20", "trezor-ui-components": "^1.0.0-beta.20",
"wallet-address-validator": "^0.2.4", "wallet-address-validator": "^0.2.4",
"web3": "1.0.0-beta.36", "web3": "1.0.0-beta.36",
@ -124,7 +123,7 @@
"eslint-plugin-prettier": "^3.0.1", "eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.4", "eslint-plugin-react": "^7.12.4",
"file-loader": "3.0.1", "file-loader": "3.0.1",
"flow-bin": "0.90", "flow-bin": "0.121.0",
"jest": "^24.9.0", "jest": "^24.9.0",
"prettier": "^1.16.4", "prettier": "^1.16.4",
"prettier-eslint": "^9.0.0", "prettier-eslint": "^9.0.0",

@ -15,7 +15,7 @@
"name": "Ethereum", "name": "Ethereum",
"symbol": "ETH", "symbol": "ETH",
"shortcut": "eth", "shortcut": "eth",
"bip44": "m/44'/60'/0'/0", "bip44": "m/44'/60'/0'/0/a",
"chainId": 1, "chainId": 1,
"defaultGasPrice": 64, "defaultGasPrice": 64,
"defaultGasLimit": 21000, "defaultGasLimit": 21000,
@ -26,8 +26,8 @@
"wss://eth2.trezor.io/geth" "wss://eth2.trezor.io/geth"
], ],
"explorer": { "explorer": {
"tx": "https://etherscan.io/tx/", "tx": "https://eth1.trezor.io/tx/",
"address": "https://etherscan.io/address/" "address": "https://eth1.trezor.io/address/"
}, },
"hasSignVerify": true "hasSignVerify": true
}, },
@ -38,7 +38,7 @@
"symbol": "ETC", "symbol": "ETC",
"shortcut": "etc", "shortcut": "etc",
"chainId": 61, "chainId": 61,
"bip44": "m/44'/61'/0'/0", "bip44": "m/44'/61'/0'/0/a",
"defaultGasPrice": 64, "defaultGasPrice": 64,
"defaultGasLimit": 21000, "defaultGasLimit": 21000,
"defaultGasLimitTokens": 200000, "defaultGasLimitTokens": 200000,
@ -61,7 +61,7 @@
"symbol": "tROP", "symbol": "tROP",
"shortcut": "trop", "shortcut": "trop",
"chainId": 3, "chainId": 3,
"bip44": "m/44'/60'/0'/0", "bip44": "m/44'/60'/0'/0/a",
"defaultGasPrice": 64, "defaultGasPrice": 64,
"defaultGasLimit": 21000, "defaultGasLimit": 21000,
"defaultGasLimitTokens": 200000, "defaultGasLimitTokens": 200000,
@ -83,8 +83,8 @@
"wss://ropsten1.trezor.io/geth" "wss://ropsten1.trezor.io/geth"
], ],
"explorer": { "explorer": {
"tx": "https://ropsten.etherscan.io/tx/", "tx": "https://ropsten1.trezor.io/tx/",
"address": "https://ropsten.etherscan.io/address/" "address": "https://ropsten1.trezor.io/address/"
}, },
"hasSignVerify": true "hasSignVerify": true
}, },

@ -76,32 +76,31 @@ export const subscribe = (networkName: string): PromiseAction<void> => async (
} }
}; };
export const onBlockMined = ( export const onBlockMined = (payload: BlockchainBlock): PromiseAction<void> => async (
payload: $ElementType<BlockchainBlock, 'payload'> dispatch: Dispatch,
): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { getState: GetState
): Promise<void> => {
const shortcut = payload.coin.shortcut.toLowerCase(); const shortcut = payload.coin.shortcut.toLowerCase();
const { block } = payload;
if (getState().router.location.state.network !== shortcut) return;
const { config } = getState().localStorage; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === shortcut); const network = config.networks.find(c => c.shortcut === shortcut);
if (!network) return; if (!network) return;
switch (network.type) { switch (network.type) {
case 'ethereum': case 'ethereum':
await dispatch(EthereumBlockchainActions.onBlockMined(shortcut)); await dispatch(EthereumBlockchainActions.onBlockMined(network));
break; break;
case 'ripple': case 'ripple':
await dispatch(RippleBlockchainActions.onBlockMined(shortcut, block)); await dispatch(RippleBlockchainActions.onBlockMined(network));
break; break;
default: default:
break; break;
} }
}; };
export const onNotification = ( export const onNotification = (payload: BlockchainNotification): PromiseAction<void> => async (
payload: $ElementType<BlockchainNotification, 'payload'> dispatch: Dispatch,
): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { getState: GetState
): Promise<void> => {
const shortcut = payload.coin.shortcut.toLowerCase(); const shortcut = payload.coin.shortcut.toLowerCase();
const { config } = getState().localStorage; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === shortcut); const network = config.networks.find(c => c.shortcut === shortcut);
@ -109,11 +108,10 @@ export const onNotification = (
switch (network.type) { switch (network.type) {
case 'ethereum': case 'ethereum':
// this is not working until blockchain-link will start support blockbook backends await dispatch(EthereumBlockchainActions.onNotification(payload, network));
await dispatch(EthereumBlockchainActions.onNotification(payload));
break; break;
case 'ripple': case 'ripple':
await dispatch(RippleBlockchainActions.onNotification(payload)); await dispatch(RippleBlockchainActions.onNotification(payload, network));
break; break;
default: default:
break; break;
@ -122,9 +120,10 @@ export const onNotification = (
// Handle BLOCKCHAIN.ERROR event from TrezorConnect // Handle BLOCKCHAIN.ERROR event from TrezorConnect
// disconnect and remove Web3 websocket instance if exists // disconnect and remove Web3 websocket instance if exists
export const onError = ( export const onError = (payload: BlockchainError): PromiseAction<void> => async (
payload: $ElementType<BlockchainError, 'payload'> dispatch: Dispatch,
): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { getState: GetState
): Promise<void> => {
const shortcut = payload.coin.shortcut.toLowerCase(); const shortcut = payload.coin.shortcut.toLowerCase();
const { config } = getState().localStorage; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === shortcut); const network = config.networks.find(c => c.shortcut === shortcut);

@ -4,10 +4,9 @@ import * as ACCOUNT from 'actions/constants/account';
import * as IMPORT from 'actions/constants/importAccount'; import * as IMPORT from 'actions/constants/importAccount';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import type { AsyncAction, Account, TrezorDevice, Network, Dispatch, GetState } from 'flowtype'; import type { AsyncAction, Account, TrezorDevice, Network, Dispatch, GetState } from 'flowtype';
import * as BlockchainActions from 'actions/ethereum/BlockchainActions';
import * as LocalStorageActions from 'actions/LocalStorageActions'; import * as LocalStorageActions from 'actions/LocalStorageActions';
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import { toDecimalAmount } from 'utils/formatUtils'; import { enhanceAccount } from 'utils/accountUtils';
export type ImportAccountAction = export type ImportAccountAction =
| { | {
@ -23,10 +22,7 @@ export type ImportAccountAction =
const findIndex = (accounts: Array<Account>, network: Network, device: TrezorDevice): number => { const findIndex = (accounts: Array<Account>, network: Network, device: TrezorDevice): number => {
return accounts.filter( return accounts.filter(
a => a => a.imported === true && a.network === network.shortcut && a.deviceID === device.id
a.imported === true &&
a.network === network.shortcut &&
a.deviceID === (device.features || {}).device_id
).length; ).length;
}; };
@ -35,113 +31,22 @@ export const importAddress = (
network: Network, network: Network,
device: ?TrezorDevice device: ?TrezorDevice
): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { ): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
if (!device) return; if (!device || !device.features) return;
dispatch({ dispatch({
type: IMPORT.START, type: IMPORT.START,
}); });
let payload; const response = await TrezorConnect.getAccountInfo({
try { descriptor: address,
if (network.type === 'ethereum') { coin: network.shortcut,
const account = await dispatch( });
BlockchainActions.discoverAccount(device, address, network.shortcut)
);
const index = findIndex(getState().accounts, network, device);
const empty = account.nonce <= 0 && account.balance === '0';
payload = {
imported: true,
index,
network: network.shortcut,
deviceID: device.features ? device.features.device_id : '0',
deviceState: device.state || '0',
accountPath: account.path || [],
descriptor: account.descriptor,
balance: account.balance,
availableBalance: account.balance,
block: account.block,
transactions: account.transactions,
empty,
networkType: 'ethereum',
nonce: account.nonce,
};
dispatch({
type: ACCOUNT.CREATE,
payload,
});
dispatch({
type: IMPORT.SUCCESS,
});
dispatch(LocalStorageActions.setImportedAccount(payload));
dispatch({
type: NOTIFICATION.ADD,
payload: {
variant: 'success',
title: 'The account has been successfully imported',
cancelable: true,
},
});
} else if (network.type === 'ripple') {
const response = await TrezorConnect.rippleGetAccountInfo({
account: {
descriptor: address,
},
coin: network.shortcut,
});
// handle TREZOR response error
if (!response.success) {
throw new Error(response.payload.error);
}
const account = response.payload;
const empty = account.sequence <= 0 && account.balance === '0';
const index = findIndex(getState().accounts, network, device);
payload = {
imported: true,
index,
network: network.shortcut,
deviceID: device.features ? device.features.device_id : '0',
deviceState: device.state || '0',
accountPath: account.path || [],
descriptor: account.descriptor,
balance: toDecimalAmount(account.balance, network.decimals),
availableBalance: toDecimalAmount(account.availableBalance, network.decimals),
block: account.block,
transactions: account.transactions,
empty,
networkType: 'ripple', // handle TREZOR response error
sequence: account.sequence, if (!response.success) {
reserve: toDecimalAmount(account.reserve, network.decimals),
};
dispatch({
type: ACCOUNT.CREATE,
payload,
});
dispatch({
type: IMPORT.SUCCESS,
});
dispatch(LocalStorageActions.setImportedAccount(payload));
dispatch({
type: NOTIFICATION.ADD,
payload: {
variant: 'success',
title: 'The account has been successfully imported',
cancelable: true,
},
});
}
} catch (error) {
dispatch({ dispatch({
type: IMPORT.FAIL, type: IMPORT.FAIL,
error: error.message, error: response.payload.error,
}); });
dispatch({ dispatch({
@ -149,9 +54,36 @@ export const importAddress = (
payload: { payload: {
variant: 'error', variant: 'error',
title: 'Import account error', title: 'Import account error',
message: error.message, message: response.payload.error,
cancelable: true, cancelable: true,
}, },
}); });
return;
} }
const index = findIndex(getState().accounts, network, device);
const account = enhanceAccount(response.payload, {
imported: true,
index,
network,
device,
});
dispatch({
type: ACCOUNT.CREATE,
payload: account,
});
dispatch({
type: IMPORT.SUCCESS,
});
dispatch(LocalStorageActions.setImportedAccount(account));
dispatch({
type: NOTIFICATION.ADD,
payload: {
variant: 'success',
title: 'The account has been successfully imported',
cancelable: true,
},
});
}; };

@ -213,7 +213,8 @@ const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise<void> =>
} }
}; };
const VERSION: string = '2'; // version 3 since trezor-connect@8 implementation
const VERSION: string = '3';
const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => {
// validate version // validate version
@ -454,9 +455,8 @@ export const removeImportedAccounts = (device: TrezorDevice): ThunkAction => (
const importedAccounts: ?Array<Account> = getImportedAccounts(); const importedAccounts: ?Array<Account> = getImportedAccounts();
if (!importedAccounts) return; if (!importedAccounts) return;
const deviceId = device.features ? device.features.device_id : null;
const filteredImportedAccounts = importedAccounts.filter( const filteredImportedAccounts = importedAccounts.filter(
account => account.deviceID !== deviceId account => account.deviceID !== device.id
); );
storageUtils.remove(TYPE, KEY_IMPORTED_ACCOUNTS); storageUtils.remove(TYPE, KEY_IMPORTED_ACCOUNTS);
filteredImportedAccounts.forEach(account => { filteredImportedAccounts.forEach(account => {

@ -33,14 +33,14 @@ export const onPinSubmit = (value: string): Action => {
}; };
}; };
export const onPassphraseSubmit = (passphrase: string): AsyncAction => async ( export const onPassphraseSubmit = (
dispatch: Dispatch, passphrase: string,
getState: GetState passphraseOnDevice: boolean = false
): Promise<void> => { ): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { modal } = getState(); const { modal } = getState();
if (modal.context !== MODAL.CONTEXT_DEVICE) return; if (modal.context !== MODAL.CONTEXT_DEVICE) return;
if (passphrase === '') { if (passphrase === '' && !passphraseOnDevice) {
// set standard wallet type if passphrase is blank // set standard wallet type if passphrase is blank
dispatch({ dispatch({
type: CONNECT.UPDATE_WALLET_TYPE, type: CONNECT.UPDATE_WALLET_TYPE,
@ -54,6 +54,7 @@ export const onPassphraseSubmit = (passphrase: string): AsyncAction => async (
payload: { payload: {
value: passphrase, value: passphrase,
save: true, save: true,
passphraseOnDevice,
}, },
}); });
@ -144,7 +145,7 @@ export const onDeviceConnect = (device: Device): ThunkAction => (
device.features && device.features &&
modal.device && modal.device &&
modal.device.features && modal.device.features &&
modal.device.features.device_id === device.features.device_id modal.device.id === device.id
) { ) {
dispatch({ dispatch({
type: MODAL.CLOSE, type: MODAL.CLOSE,

@ -54,7 +54,7 @@ export const showUnverifiedAddress = (): Action => ({
}); });
//export const showAddress = (address_n: string): AsyncAction => { //export const showAddress = (address_n: string): AsyncAction => {
export const showAddress = (path: Array<number>): AsyncAction => async ( export const showAddress = (path: string): AsyncAction => async (
dispatch: Dispatch, dispatch: Dispatch,
getState: GetState getState: GetState
): Promise<void> => { ): Promise<void> => {

@ -65,14 +65,14 @@ export const paramsValidation = (params: RouterLocationState): PayloadAction<boo
device = devices.find( device = devices.find(
d => d =>
d.features && d.features &&
d.features.device_id === params.device && d.id === params.device &&
d.instance === parseInt(params.deviceInstance, 10) d.instance === parseInt(params.deviceInstance, 10)
); );
} else { } else {
device = devices.find( device = devices.find(
d => d =>
((!d.features || d.mode === 'bootloader') && d.path === params.device) || ((!d.features || d.mode === 'bootloader') && d.path === params.device) ||
(d.features && d.features.device_id === params.device) (d.features && d.id === params.device)
); );
} }
@ -218,21 +218,22 @@ const getDeviceUrl = (device: TrezorDevice | Device): PayloadAction<?string> =>
getState: GetState getState: GetState
): ?string => { ): ?string => {
let url: ?string; let url: ?string;
const prefix = `/device/${device.id || device.path}`;
if (!device.features) { if (!device.features) {
url = `/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`; url = `${prefix}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`;
} else if (device.mode === 'bootloader') { } else if (device.mode === 'bootloader') {
// device in bootloader doesn't have device_id // device in bootloader doesn't have device_id
url = `/device/${device.path}/bootloader`; url = `${prefix}/bootloader`;
} else if (device.mode === 'initialize') { } else if (device.mode === 'initialize') {
url = `/device/${device.features.device_id}/initialize`; url = `${prefix}/initialize`;
} else if (device.mode === 'seedless') { } else if (device.mode === 'seedless') {
url = `/device/${device.features.device_id}/seedless`; url = `${prefix}/seedless`;
} else if (device.firmware === 'required') { } else if (device.firmware === 'required') {
url = `/device/${device.features.device_id}/firmware-update`; url = `${prefix}/firmware-update`;
} else if (typeof device.instance === 'number') { } else if (typeof device.instance === 'number') {
url = `/device/${device.features.device_id}:${device.instance}`; url = `${prefix}:${device.instance}`;
} else { } else {
url = `/device/${device.features.device_id}`; url = `${prefix}`;
// make sure that device is not TrezorDevice type // make sure that device is not TrezorDevice type
if (!device.hasOwnProperty('ts')) { if (!device.hasOwnProperty('ts')) {
// it is device from trezor-connect triggered by DEVICE.CONNECT event // it is device from trezor-connect triggered by DEVICE.CONNECT event
@ -379,10 +380,8 @@ export const gotoLandingPage = (): ThunkAction => (dispatch: Dispatch): void =>
export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => ( export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => (
dispatch: Dispatch dispatch: Dispatch
): void => { ): void => {
if (device.features) { if (device.id) {
const devUrl: string = `${device.features.device_id}${ const devUrl: string = `${device.id}${device.instance ? `:${device.instance}` : ''}`;
device.instance ? `:${device.instance}` : ''
}`;
dispatch(goto(`/device/${devUrl}/settings`)); dispatch(goto(`/device/${devUrl}/settings`));
} }
}; };
@ -403,8 +402,8 @@ export const gotoFirmwareUpdate = (): ThunkAction => (
getState: GetState getState: GetState
): void => { ): void => {
const { selectedDevice } = getState().wallet; const { selectedDevice } = getState().wallet;
if (!selectedDevice || !selectedDevice.features) return; if (!selectedDevice || !selectedDevice.id) return;
const devUrl: string = `${selectedDevice.features.device_id}${ const devUrl: string = `${selectedDevice.id}${
selectedDevice.instance ? `:${selectedDevice.instance}` : '' selectedDevice.instance ? `:${selectedDevice.instance}` : ''
}`; }`;
dispatch(goto(`/device/${devUrl}/firmware-update`)); dispatch(goto(`/device/${devUrl}/firmware-update`));
@ -415,8 +414,8 @@ export const gotoFirmwareUpdate = (): ThunkAction => (
*/ */
export const gotoBackup = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { export const gotoBackup = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const { selectedDevice } = getState().wallet; const { selectedDevice } = getState().wallet;
if (!selectedDevice || !selectedDevice.features) return; if (!selectedDevice || !selectedDevice.id) return;
const devUrl: string = `${selectedDevice.features.device_id}${ const devUrl: string = `${selectedDevice.id}${
selectedDevice.instance ? `:${selectedDevice.instance}` : '' selectedDevice.instance ? `:${selectedDevice.instance}` : ''
}`; }`;
dispatch(goto(`/device/${devUrl}/backup`)); dispatch(goto(`/device/${devUrl}/backup`));

@ -39,7 +39,7 @@ export type SignVerifyAction =
message: ?string, message: ?string,
}; };
const sign = (path: Array<number>, message: string, hex: boolean = false): AsyncAction => async ( const sign = (path: string, message: string, hex: boolean = false): AsyncAction => async (
dispatch: Dispatch, dispatch: Dispatch,
getState: GetState getState: GetState
): Promise<void> => { ): Promise<void> => {

@ -1,7 +1,9 @@
/* @flow */ /* @flow */
import * as TOKEN from 'actions/constants/token'; import * as TOKEN from 'actions/constants/token';
import { toDecimalAmount } from 'utils/formatUtils';
import type { TokenInfo } from 'trezor-connect';
import type { GetState, AsyncAction, Action, Dispatch } from 'flowtype'; import type { GetState, AsyncAction, Action, Dispatch } from 'flowtype';
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';
@ -60,21 +62,19 @@ export const setBalance = (
ethAddress: string, ethAddress: string,
balance: string balance: string
): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { ): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const newState: Array<Token> = [...getState().tokens]; const { tokens } = getState();
const token: ?Token = newState.find( const index = tokens.findIndex(t => t.address === tokenAddress && t.ethAddress === ethAddress);
t => t.address === tokenAddress && t.ethAddress === ethAddress if (index >= 0) {
); const token = tokens[index];
if (token) { const payload = tokens.slice();
const others = newState.filter(t => t !== token); payload[index] = {
...token,
loaded: true,
balance,
};
dispatch({ dispatch({
type: TOKEN.SET_BALANCE, type: TOKEN.SET_BALANCE,
payload: others.concat([ payload,
{
...token,
loaded: true,
balance,
},
]),
}); });
} }
}; };
@ -103,6 +103,27 @@ export const add = (token: NetworkToken, account: Account): AsyncAction => async
dispatch(setBalance(token.address, account.descriptor, tokenBalance)); dispatch(setBalance(token.address, account.descriptor, tokenBalance));
}; };
export const createAccountTokens = (account: Account, tokens: TokenInfo[]): AsyncAction => async (
dispatch: Dispatch
): Promise<void> => {
tokens.forEach(t => {
dispatch({
type: TOKEN.ADD,
payload: {
address: t.address,
balance: toDecimalAmount(t.balance || '0', t.decimals),
decimals: t.decimals,
deviceState: account.deviceState,
ethAddress: account.descriptor,
loaded: true,
name: t.name || '',
network: account.network,
symbol: t.symbol || '',
},
});
});
};
export const remove = (token: Token): Action => ({ export const remove = (token: Token): Action => ({
type: TOKEN.REMOVE, type: TOKEN.REMOVE,
token, token,

@ -17,16 +17,6 @@ import * as RouterActions from 'actions/RouterActions';
import * as deviceUtils from 'utils/device'; import * as deviceUtils from 'utils/device';
import * as buildUtils from 'utils/build'; import * as buildUtils from 'utils/build';
import type {
DeviceMessage,
DeviceMessageType,
UiMessage,
UiMessageType,
TransportMessage,
TransportMessageType,
BlockchainEvent,
} from 'trezor-connect';
import type { import type {
Dispatch, Dispatch,
GetState, GetState,
@ -113,62 +103,32 @@ export const init = (): AsyncAction => async (
getState: GetState getState: GetState
): Promise<void> => { ): Promise<void> => {
// set listeners // set listeners
TrezorConnect.on( TrezorConnect.on(DEVICE_EVENT, event => {
DEVICE_EVENT, dispatch(event);
(event: DeviceMessage): void => { });
// post event to reducers
const type: DeviceMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({
type,
device: event.payload,
});
}
);
TrezorConnect.on( TrezorConnect.on(UI_EVENT, event => {
UI_EVENT, dispatch(event);
(event: UiMessage): void => { });
// post event to reducers
const type: UiMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({
type,
payload: event.payload,
});
}
);
TrezorConnect.on( TrezorConnect.on(TRANSPORT_EVENT, event => {
TRANSPORT_EVENT, dispatch(event);
(event: TransportMessage): void => { });
// post event to reducers
const type: TransportMessageType = event.type; // eslint-disable-line prefer-destructuring
dispatch({
type,
payload: event.payload,
});
}
);
// post event to reducers // post event to reducers
TrezorConnect.on( TrezorConnect.on(BLOCKCHAIN_EVENT, event => {
BLOCKCHAIN_EVENT, dispatch(event);
(event: BlockchainEvent): void => { });
dispatch(event);
}
);
let connectSrc;
if (buildUtils.isDev()) { if (buildUtils.isDev()) {
// eslint-disable-next-line connectSrc = typeof LOCAL === 'string' ? LOCAL : undefined;
window.__TREZOR_CONNECT_SRC =
typeof LOCAL === 'string'
? LOCAL
: 'https://connect.corp.sldev.cz/fix/v7-ripple-lib-error/'; // eslint-disable-line no-underscore-dangle
// window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://localhost:8088/'; // eslint-disable-line no-underscore-dangle
window.TrezorConnect = TrezorConnect; window.TrezorConnect = TrezorConnect;
} }
try { try {
await TrezorConnect.init({ await TrezorConnect.init({
connectSrc,
transportReconnect: true, transportReconnect: true,
debug: false, debug: false,
popup: false, popup: false,
@ -223,10 +183,18 @@ export const requestWalletType = (): AsyncAction => async (
if (!isDeviceReady) return; if (!isDeviceReady) return;
if (selected.features && selected.features.passphrase_protection) { if (selected.features && selected.features.passphrase_protection) {
dispatch({ if (selected.features.passphrase_always_on_device) {
type: CONNECT.REQUEST_WALLET_TYPE, dispatch({
device: selected, type: CONNECT.RECEIVE_WALLET_TYPE,
}); device: selected,
hidden: true,
});
} else {
dispatch({
type: CONNECT.REQUEST_WALLET_TYPE,
device: selected,
});
}
} else { } else {
dispatch({ dispatch({
type: CONNECT.RECEIVE_WALLET_TYPE, type: CONNECT.RECEIVE_WALLET_TYPE,
@ -298,12 +266,7 @@ export const deviceDisconnect = (device: Device): AsyncAction => async (
): Promise<void> => { ): Promise<void> => {
if (device.features) { if (device.features) {
const instances = getState().devices.filter( const instances = getState().devices.filter(
d => d => d.features && device.features && d.state && !d.remember && d.id === device.id
d.features &&
device.features &&
d.state &&
!d.remember &&
d.features.device_id === device.features.device_id
); );
if (instances.length > 0) { if (instances.length > 0) {
const isSelected = deviceUtils.isSelectedDevice( const isSelected = deviceUtils.isSelectedDevice(
@ -401,8 +364,16 @@ export const duplicateDeviceOld = (device: TrezorDevice): AsyncAction => async (
export const duplicateDevice = (device: TrezorDevice): AsyncAction => async ( export const duplicateDevice = (device: TrezorDevice): AsyncAction => async (
dispatch: Dispatch dispatch: Dispatch
): Promise<void> => { ): Promise<void> => {
dispatch({ if (device.features && device.features.passphrase_always_on_device) {
type: CONNECT.REQUEST_WALLET_TYPE, dispatch({
device, type: CONNECT.RECEIVE_WALLET_TYPE,
}); device,
hidden: true,
});
} else {
dispatch({
type: CONNECT.REQUEST_WALLET_TYPE,
device,
});
}
}; };

@ -21,7 +21,7 @@ type EthereumTxRequest = {
data: string, data: string,
gasLimit: string, gasLimit: string,
gasPrice: string, gasPrice: string,
nonce: number, nonce: string,
}; };
export const prepareEthereumTx = ( export const prepareEthereumTx = (
@ -54,9 +54,6 @@ export const prepareEthereumTx = (
nonce: toHex(tx.nonce), nonce: toHex(tx.nonce),
gasLimit: toHex(tx.gasLimit), gasLimit: toHex(tx.gasLimit),
gasPrice: toHex(EthereumjsUnits.convert(tx.gasPrice, 'gwei', 'wei')), gasPrice: toHex(EthereumjsUnits.convert(tx.gasPrice, 'gwei', 'wei')),
r: '',
s: '',
v: '',
}; };
}; };

@ -144,7 +144,7 @@ export const clearUnavailableDevicesData = (prevState: State, device: Device): T
d => d =>
d.features && d.features &&
device.features && device.features &&
d.features.device_id === device.features.device_id && d.id === device.id &&
d.features.passphrase_protection !== device.features.passphrase_protection d.features.passphrase_protection !== device.features.passphrase_protection
); );

@ -11,13 +11,11 @@ import * as ethUtils from 'utils/ethUtils';
import type { Dispatch, GetState, ThunkAction, PromiseAction } from 'flowtype'; import type { Dispatch, GetState, ThunkAction, PromiseAction } from 'flowtype';
import type { EthereumAccount } from 'trezor-connect';
import type { Account } from 'reducers/AccountsReducer'; import type { Account } from 'reducers/AccountsReducer';
import type { Web3Instance } from 'reducers/Web3Reducer'; import type { Web3Instance } from 'reducers/Web3Reducer';
import type { Token } from 'reducers/TokensReducer'; import type { Token } from 'reducers/TokensReducer';
import type { NetworkToken } from 'reducers/LocalStorageReducer'; import type { NetworkToken } from 'reducers/LocalStorageReducer';
import * as TokenActions from './TokenActions'; import * as TokenActions from './TokenActions';
import * as AccountsActions from './AccountsActions';
export type Web3UpdateBlockAction = { export type Web3UpdateBlockAction = {
type: typeof WEB3.BLOCK_UPDATED, type: typeof WEB3.BLOCK_UPDATED,
@ -127,22 +125,22 @@ export const initWeb3 = (
web3.currentProvider.on('error', onEnd); web3.currentProvider.on('error', onEnd);
}); });
export const discoverAccount = ( // not used since connect@8
descriptor: string, // export const discoverAccount = (descriptor: string, network: string): PromiseAction<any> => async (
network: string // dispatch: Dispatch
): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => { // ): Promise<any> => {
const instance: Web3Instance = await dispatch(initWeb3(network)); // const instance: Web3Instance = await dispatch(initWeb3(network));
const balance = await instance.web3.eth.getBalance(descriptor); // const balance = await instance.web3.eth.getBalance(descriptor);
const nonce = await instance.web3.eth.getTransactionCount(descriptor); // const nonce = await instance.web3.eth.getTransactionCount(descriptor);
return { // return {
descriptor, // descriptor,
transactions: 0, // transactions: 0,
block: 0, // block: 0,
balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), // balance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'), // availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
nonce, // nonce,
}; // };
}; // };
export const resolvePendingTransactions = (network: string): PromiseAction<void> => async ( export const resolvePendingTransactions = (network: string): PromiseAction<void> => async (
dispatch: Dispatch, dispatch: Dispatch,
@ -151,24 +149,24 @@ export const resolvePendingTransactions = (network: string): PromiseAction<void>
const instance: Web3Instance = await dispatch(initWeb3(network)); const instance: Web3Instance = await dispatch(initWeb3(network));
const pending = getState().pending.filter(p => p.network === network); const pending = getState().pending.filter(p => p.network === network);
pending.forEach(async tx => { pending.forEach(async tx => {
const status = await instance.web3.eth.getTransaction(tx.hash); const status = await instance.web3.eth.getTransaction(tx.txid);
if (!status) { if (!status) {
dispatch({ dispatch({
type: PENDING.TX_REJECTED, type: PENDING.TX_REJECTED,
hash: tx.hash, hash: tx.txid,
}); });
} else { } else {
const receipt = await instance.web3.eth.getTransactionReceipt(tx.hash); const receipt = await instance.web3.eth.getTransactionReceipt(tx.txid);
if (receipt) { if (receipt) {
if (status.gas !== receipt.gasUsed) { if (status.gas !== receipt.gasUsed) {
dispatch({ dispatch({
type: PENDING.TX_TOKEN_ERROR, type: PENDING.TX_TOKEN_ERROR,
hash: tx.hash, hash: tx.txid,
}); });
} }
dispatch({ dispatch({
type: PENDING.TX_RESOLVED, type: PENDING.TX_RESOLVED,
hash: tx.hash, hash: tx.txid,
}); });
} }
} }
@ -205,30 +203,31 @@ export const getTxInput = (): PromiseAction<void> => async (dispatch: Dispatch):
}; };
*/ */
export const updateAccount = ( // not used since connect@8
account: Account, // export const updateAccount = (
newAccount: EthereumAccount, // account: Account,
network: string // newAccount: any,
): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => { // network: string
const instance: Web3Instance = await dispatch(initWeb3(network)); // ): PromiseAction<void> => async (dispatch: Dispatch): Promise<void> => {
const balance = await instance.web3.eth.getBalance(account.descriptor); // const instance: Web3Instance = await dispatch(initWeb3(network));
const nonce = await instance.web3.eth.getTransactionCount(account.descriptor); // const balance = await instance.web3.eth.getBalance(account.descriptor);
const empty = nonce <= 0 && balance === '0'; // const nonce = await instance.web3.eth.getTransactionCount(account.descriptor);
dispatch( // const empty = nonce <= 0 && balance === '0';
AccountsActions.update({ // dispatch(
networkType: 'ethereum', // AccountsActions.update({
...account, // networkType: 'ethereum',
...newAccount, // ...account,
empty, // ...newAccount,
nonce, // empty,
balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), // nonce,
availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'), // balance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
}) // availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'),
); // })
// );
// update tokens for this account
dispatch(updateAccountTokens(account)); // // update tokens for this account
}; // dispatch(updateAccountTokens(account));
// };
export const updateAccountTokens = (account: Account): PromiseAction<void> => async ( export const updateAccountTokens = (account: Account): PromiseAction<void> => async (
dispatch: Dispatch, dispatch: Dispatch,

@ -3,47 +3,14 @@
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import * as AccountsActions from 'actions/AccountsActions';
import * as Web3Actions from 'actions/Web3Actions';
import { mergeAccount, enhanceTransaction } from 'utils/accountUtils';
import type { TrezorDevice, Dispatch, GetState, PromiseAction } from 'flowtype'; import type { Dispatch, GetState, PromiseAction, Network } from 'flowtype';
import type { EthereumAccount, BlockchainNotification } from 'trezor-connect'; import type { BlockchainNotification } from 'trezor-connect';
import type { Token } from 'reducers/TokensReducer'; import type { Token } from 'reducers/TokensReducer';
import type { NetworkToken } from 'reducers/LocalStorageReducer'; import type { NetworkToken } from 'reducers/LocalStorageReducer';
import * as Web3Actions from 'actions/Web3Actions';
import * as AccountsActions from 'actions/AccountsActions';
export const discoverAccount = (
device: TrezorDevice,
descriptor: string,
network: string
): PromiseAction<EthereumAccount> => async (dispatch: Dispatch): Promise<EthereumAccount> => {
// get data from connect
const txs = await TrezorConnect.ethereumGetAccountInfo({
account: {
descriptor,
block: 0,
transactions: 0,
balance: '0',
availableBalance: '0',
nonce: 0,
},
coin: network,
});
if (!txs.success) {
throw new Error(txs.payload.error);
}
// blockbook web3 fallback
const web3account = await dispatch(Web3Actions.discoverAccount(descriptor, network));
return {
descriptor,
transactions: txs.payload.transactions,
block: txs.payload.block,
balance: web3account.balance,
availableBalance: web3account.balance,
nonce: web3account.nonce,
};
};
export const getTokenInfo = (input: string, network: string): PromiseAction<NetworkToken> => async ( export const getTokenInfo = (input: string, network: string): PromiseAction<NetworkToken> => async (
dispatch: Dispatch dispatch: Dispatch
@ -103,9 +70,9 @@ export const subscribe = (network: string): PromiseAction<void> => async (
dispatch: Dispatch, dispatch: Dispatch,
getState: GetState getState: GetState
): Promise<void> => { ): Promise<void> => {
const accounts: Array<string> = getState() const accounts = getState()
.accounts.filter(a => a.network === network) .accounts.filter(a => a.network === network)
.map(a => a.descriptor); // eslint-disable-line no-unused-vars .map(a => ({ descriptor: a.descriptor }));
const response = await TrezorConnect.blockchainSubscribe({ const response = await TrezorConnect.blockchainSubscribe({
accounts, accounts,
coin: network, coin: network,
@ -115,7 +82,7 @@ export const subscribe = (network: string): PromiseAction<void> => async (
await dispatch(Web3Actions.initWeb3(network)); await dispatch(Web3Actions.initWeb3(network));
}; };
export const onBlockMined = (network: string): PromiseAction<void> => async ( export const onBlockMined = (network: Network): PromiseAction<void> => async (
dispatch: Dispatch, dispatch: Dispatch,
getState: GetState getState: GetState
): Promise<void> => { ): Promise<void> => {
@ -123,56 +90,52 @@ export const onBlockMined = (network: string): PromiseAction<void> => async (
// check latest saved transaction blockhash against blockhheight // check latest saved transaction blockhash against blockhheight
// try to resolve pending transactions // try to resolve pending transactions
await dispatch(Web3Actions.resolvePendingTransactions(network)); await dispatch(Web3Actions.resolvePendingTransactions(network.shortcut));
await dispatch(Web3Actions.updateGasPrice(network)); await dispatch(Web3Actions.updateGasPrice(network.shortcut));
const accounts: Array<any> = getState().accounts.filter(a => a.network === network); const accounts = getState().accounts.filter(a => a.network === network.shortcut);
if (accounts.length > 0) { if (accounts.length === 0) return;
// find out which account changed const blockchain = getState().blockchain.find(b => b.shortcut === network.shortcut);
const response = await TrezorConnect.ethereumGetAccountInfo({ if (!blockchain) return; // flowtype fallback
accounts,
coin: network, // find out which account changed
}); const bundle = accounts.map(a => ({ descriptor: a.descriptor, coin: network.shortcut }));
const response = await TrezorConnect.getAccountInfo({ bundle });
if (response.success) {
response.payload.forEach((a, i) => { if (!response.success) return;
if (a.transactions > 0) {
// load additional data from Web3 (balance, nonce, tokens) response.payload.forEach((info, i) => {
dispatch(Web3Actions.updateAccount(accounts[i], a, network)); dispatch(
} else { AccountsActions.update(mergeAccount(info, accounts[i], network, blockchain.block))
// there are no new txs, just update block );
// TODO: There still could be internal transactions as a result of contract dispatch(Web3Actions.updateAccountTokens(accounts[i]));
// If that's the case, account balance won't be updated });
// Currently waiting for deprecating web3 and utilising new blockbook
dispatch(AccountsActions.update({ ...accounts[i], block: a.block }));
// HACK: since blockbook can't work with smart contracts for now
// try to update tokens balances added to this account using Web3
dispatch(Web3Actions.updateAccountTokens(accounts[i]));
}
});
}
}
}; };
export const onNotification = ( export const onNotification = (
payload: $ElementType<BlockchainNotification, 'payload'> payload: BlockchainNotification,
network: Network
): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { ): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { notification } = payload; const { descriptor, tx } = payload.notification;
const account = getState().accounts.find(a => a.descriptor === notification.descriptor); const account = getState().accounts.find(a => a.descriptor === descriptor);
if (!account) return; const blockchain = getState().blockchain.find(b => b.shortcut === network.shortcut);
if (!account || !blockchain) return;
if (!notification.blockHeight) { dispatch({
dispatch({ type: PENDING.ADD,
type: PENDING.ADD, payload: enhanceTransaction(account, tx, network),
payload: { });
...notification,
deviceState: account.deviceState, const response = await TrezorConnect.getAccountInfo({
network: account.network, descriptor: account.descriptor,
}, coin: account.network,
}); });
}
if (!response.success) return;
dispatch(
AccountsActions.update(mergeAccount(response.payload, account, network, blockchain.block))
);
}; };
export const onError = (network: string): PromiseAction<void> => async ( export const onError = (network: string): PromiseAction<void> => async (

@ -1,10 +1,9 @@
/* @flow */ /* @flow */
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import EthereumjsUtil from 'ethereumjs-util';
import * as DISCOVERY from 'actions/constants/discovery'; import * as DISCOVERY from 'actions/constants/discovery';
import * as BlockchainActions from 'actions/ethereum/BlockchainActions'; // import { createAccountTokens } from 'actions/TokenActions';
import { enhanceAccount } from 'utils/accountUtils';
import type { PromiseAction, Dispatch, GetState, TrezorDevice, Network, Account } from 'flowtype'; import type { PromiseAction, Dispatch, GetState, TrezorDevice, Network, Account } from 'flowtype';
import type { Discovery } from 'reducers/DiscoveryReducer'; import type { Discovery } from 'reducers/DiscoveryReducer';
@ -13,30 +12,41 @@ export type DiscoveryStartAction = {
networkType: 'ethereum', networkType: 'ethereum',
network: Network, network: Network,
device: TrezorDevice, device: TrezorDevice,
publicKey: string,
chainCode: string,
basePath: Array<number>,
}; };
// first iteration
// generate public key for this account
// start discovery process
export const begin = ( export const begin = (
device: TrezorDevice, device: TrezorDevice,
network: Network network: Network
): PromiseAction<DiscoveryStartAction> => async (): Promise<DiscoveryStartAction> => { ): PromiseAction<DiscoveryStartAction> => async (): Promise<DiscoveryStartAction> => ({
// get xpub from TREZOR type: DISCOVERY.START,
const response = await TrezorConnect.getPublicKey({ networkType: 'ethereum',
network,
device,
});
export const discoverAccount = (
device: TrezorDevice,
discoveryProcess: Discovery
): PromiseAction<Account> => async (dispatch: Dispatch, getState: GetState): Promise<Account> => {
const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === discoveryProcess.network);
if (!network) throw new Error('Discovery network not found');
const { accountIndex } = discoveryProcess;
const path = network.bip44.slice(0).replace('a', accountIndex.toString());
const response = await TrezorConnect.getAccountInfo({
device: { device: {
path: device.path, path: device.path,
instance: device.instance, instance: device.instance,
state: device.state, state: device.state,
}, },
path: network.bip44, path,
details: 'tokenBalances',
pageSize: 1,
keepSession: true, // acquire and hold session keepSession: true, // acquire and hold session
//useEmptyPassphrase: !device.instance,
useEmptyPassphrase: device.useEmptyPassphrase, useEmptyPassphrase: device.useEmptyPassphrase,
network: network.name, coin: network.shortcut,
}); });
// handle TREZOR response error // handle TREZOR response error
@ -44,59 +54,15 @@ export const begin = (
throw new Error(response.payload.error); throw new Error(response.payload.error);
} }
const basePath: Array<number> = response.payload.path; const account = enhanceAccount(response.payload, {
index: discoveryProcess.accountIndex,
return {
type: DISCOVERY.START,
networkType: 'ethereum',
network, network,
device, device,
publicKey: response.payload.publicKey, });
chainCode: response.payload.chainCode,
basePath,
};
};
export const discoverAccount = (
device: TrezorDevice,
discoveryProcess: Discovery
): PromiseAction<Account> => async (dispatch: Dispatch, getState: GetState): Promise<Account> => {
const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === discoveryProcess.network);
if (!network) throw new Error('Discovery network not found');
const 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);
// TODO: check if address was created before
const account = await dispatch(
BlockchainActions.discoverAccount(device, ethAddress, network.shortcut)
);
// const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0';
const empty = account.nonce <= 0 && account.balance === '0';
return {
imported: false,
index: discoveryProcess.accountIndex,
network: network.shortcut,
deviceID: device.features ? device.features.device_id : '0',
deviceState: device.state || '0',
accountPath: path,
descriptor: ethAddress,
balance: account.balance, // if (response.payload.tokens) {
availableBalance: account.balance, // dispatch(createAccountTokens(account, response.payload.tokens));
block: account.block, // }
transactions: account.transactions,
empty,
networkType: 'ethereum', return account;
nonce: account.nonce,
};
}; };

@ -486,7 +486,7 @@ export const onGasPriceChange = (gasPrice: string): ThunkAction => (
): void => { ): void => {
const state: State = getState().sendFormEthereum; const state: State = getState().sendFormEthereum;
// switch to custom fee level // switch to custom fee level
let newSelectedFeeLevel = state.selectedFeeLevel; let newSelectedFeeLevel;
if (state.selectedFeeLevel.value !== 'Custom') if (state.selectedFeeLevel.value !== 'Custom')
newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom'); newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom');
@ -498,7 +498,7 @@ export const onGasPriceChange = (gasPrice: string): ThunkAction => (
untouched: false, untouched: false,
touched: { ...state.touched, gasPrice: true }, touched: { ...state.touched, gasPrice: true },
gasPrice, gasPrice,
selectedFeeLevel: newSelectedFeeLevel, selectedFeeLevel: newSelectedFeeLevel || state.selectedFeeLevel,
}, },
}); });
}; };
@ -673,9 +673,12 @@ export const onSend = (): AsyncAction => async (
const currentState: State = getState().sendFormEthereum; const currentState: State = getState().sendFormEthereum;
const isToken: boolean = currentState.currency !== currentState.networkSymbol; const isToken = currentState.currency !== currentState.networkSymbol;
const pendingNonce: number = reducerUtils.getPendingSequence(pending); const pendingNonce = new BigNumber(reducerUtils.getPendingSequence(pending));
const nonce = pendingNonce > 0 && pendingNonce >= account.nonce ? pendingNonce : account.nonce; const nonce =
pendingNonce.gt(0) && pendingNonce.gt(account.nonce)
? pendingNonce.toString()
: account.nonce;
const txData = await dispatch( const txData = await dispatch(
prepareEthereumTx({ prepareEthereumTx({
@ -727,12 +730,15 @@ export const onSend = (): AsyncAction => async (
return; return;
} }
txData.r = signedTransaction.payload.r;
txData.s = signedTransaction.payload.s;
txData.v = signedTransaction.payload.v;
try { try {
const serializedTx: string = await dispatch(serializeEthereumTx(txData)); const serializedTx: string = await dispatch(
serializeEthereumTx({
...txData,
r: signedTransaction.payload.r,
s: signedTransaction.payload.s,
v: signedTransaction.payload.v,
})
);
const push = await TrezorConnect.pushTransaction({ const push = await TrezorConnect.pushTransaction({
tx: serializedTx, tx: serializedTx,
coin: network.shortcut, coin: network.shortcut,
@ -746,58 +752,6 @@ export const onSend = (): AsyncAction => async (
dispatch({ type: SEND.TX_COMPLETE }); dispatch({ type: SEND.TX_COMPLETE });
// ugly blockbook workaround:
// since blockbook can't emit pending notifications
// need to trigger this event from here, where we know everything about this transaction
// blockchainNotification is 'trezor-connect' BlockchainLinkTransaction type
const fee = ValidationActions.calculateFee(currentState.gasLimit, currentState.gasPrice);
const blockchainNotification = {
type: 'send',
descriptor: account.descriptor,
inputs: [
{
addresses: [account.descriptor],
amount: currentState.amount,
fee,
total: currentState.total,
},
],
outputs: [
{
addresses: [currentState.address],
amount: currentState.amount,
},
],
hash: txid,
amount: currentState.amount,
fee,
total: currentState.total,
sequence: nonce,
tokens: isToken
? [
{
name: currentState.currency,
shortcut: currentState.currency,
value: currentState.amount,
},
]
: undefined,
blockHeight: 0,
blockHash: undefined,
timestamp: undefined,
};
dispatch(
BlockchainActions.onNotification({
// $FlowIssue: missing coinInfo declaration
coin: {},
notification: blockchainNotification,
})
);
// workaround end
// clear session storage // clear session storage
dispatch(SessionStorageActions.clear()); dispatch(SessionStorageActions.clear());

@ -4,7 +4,7 @@ import TrezorConnect from 'trezor-connect';
import * as BLOCKCHAIN from 'actions/constants/blockchain'; import * as BLOCKCHAIN from 'actions/constants/blockchain';
import * as PENDING from 'actions/constants/pendingTx'; import * as PENDING from 'actions/constants/pendingTx';
import * as AccountsActions from 'actions/AccountsActions'; import * as AccountsActions from 'actions/AccountsActions';
import { toDecimalAmount } from 'utils/formatUtils'; import { mergeAccount, enhanceTransaction } from 'utils/accountUtils';
import { observeChanges } from 'reducers/utils'; import { observeChanges } from 'reducers/utils';
import type { BlockchainNotification } from 'trezor-connect'; import type { BlockchainNotification } from 'trezor-connect';
@ -20,10 +20,10 @@ import type {
export const subscribe = (network: string): PromiseAction<void> => async ( export const subscribe = (network: string): PromiseAction<void> => async (
dispatch: Dispatch, dispatch: Dispatch,
getState: GetState getState: GetState
): Promise<void> => { ) => {
const accounts: Array<string> = getState() const accounts = getState()
.accounts.filter(a => a.network === network) .accounts.filter(a => a.network === network)
.map(a => a.descriptor); .map(a => ({ descriptor: a.descriptor }));
await TrezorConnect.blockchainSubscribe({ await TrezorConnect.blockchainSubscribe({
accounts, accounts,
coin: network, coin: network,
@ -47,161 +47,81 @@ export const getFeeLevels = (network: Network): PayloadAction<Array<BlockchainFe
return blockchain.feeLevels; return blockchain.feeLevels;
}; };
export const onBlockMined = (networkShortcut: string, block: number): PromiseAction<void> => async ( export const onBlockMined = (network: Network): PromiseAction<void> => async (
dispatch: Dispatch, dispatch: Dispatch,
getState: GetState getState: GetState
): Promise<void> => { ): Promise<void> => {
const blockchain = getState().blockchain.find(b => b.shortcut === networkShortcut); const blockchain = getState().blockchain.find(b => b.shortcut === network.shortcut);
if (!blockchain) return; // flowtype fallback if (!blockchain) return; // flowtype fallback
// if last update was more than 5 minutes ago // if last update was more than 5 minutes ago
const now = new Date().getTime(); const now = new Date().getTime();
if (blockchain.feeTimestamp < now - 300000) { if (blockchain.feeTimestamp < now - 300000) {
const feeRequest = await TrezorConnect.blockchainEstimateFee({ const feeRequest = await TrezorConnect.blockchainEstimateFee({
coin: networkShortcut, request: {
feeLevels: 'smart',
},
coin: network.shortcut,
}); });
if (feeRequest.success && observeChanges(blockchain.feeLevels, feeRequest.payload)) { if (feeRequest.success && observeChanges(blockchain.feeLevels, feeRequest.payload)) {
// check if downloaded fee levels are different // check if downloaded fee levels are different
dispatch({ dispatch({
type: BLOCKCHAIN.UPDATE_FEE, type: BLOCKCHAIN.UPDATE_FEE,
shortcut: networkShortcut, shortcut: network.shortcut,
feeLevels: feeRequest.payload, feeLevels: feeRequest.payload.levels.map(l => ({
name: 'Normal',
value: l.feePerUnit,
})),
}); });
} }
} }
// TODO: check for blockchain rollbacks here! // TODO: check for blockchain rollbacks here!
const accounts: Array<any> = getState().accounts.filter(a => a.network === networkShortcut); const accounts = getState().accounts.filter(a => a.network === network.shortcut);
if (accounts.length === 0) return; if (accounts.length === 0) return;
const { networks } = getState().localStorage.config;
const network = networks.find(c => c.shortcut === networkShortcut);
if (!network) return;
// HACK: Since Connect always returns account.transactions as 0
// we don't have info about new transactions for the account since last update.
// Untill there is a better solution compare accounts block.
// If we missed some blocks (wallet was offline) we'll update the account
// If we are update to date with the last block that means wallet was online
// and we would get Blockchain notification about new transaction if needed
accounts.forEach(async account => {
const missingBlocks = account.block !== block - 1;
if (!missingBlocks) {
// account was last updated on account.block, current block is +1, we didn't miss single block
// if there was new tx, blockchain notification would let us know
// so just update the block for the account
dispatch(
AccountsActions.update({
...account,
block,
})
);
} else {
// we missed some blocks (wallet was offline). get updated account info from connect
const response = await TrezorConnect.rippleGetAccountInfo({
account: {
descriptor: account.descriptor,
},
level: 'transactions',
coin: networkShortcut,
});
if (!response.success) return; const bundle = accounts.map(a => ({ descriptor: a.descriptor, coin: network.shortcut }));
const response = await TrezorConnect.getAccountInfo({ bundle });
const updatedAccount = response.payload;
if (!response.success) return;
// new txs
dispatch( response.payload.forEach((info, i) => {
AccountsActions.update({ dispatch(
...account, AccountsActions.update(mergeAccount(info, accounts[i], network, blockchain.block))
balance: toDecimalAmount(updatedAccount.balance, network.decimals), );
availableBalance: toDecimalAmount(
updatedAccount.availableBalance,
network.decimals
),
block: updatedAccount.block,
sequence: updatedAccount.sequence,
})
);
}
}); });
}; };
export const onNotification = ( export const onNotification = (
payload: $ElementType<BlockchainNotification, 'payload'> payload: BlockchainNotification,
network: Network
): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => { ): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { notification } = payload; const { descriptor, tx } = payload.notification;
const account = getState().accounts.find(a => a.descriptor === notification.descriptor); const account = getState().accounts.find(a => a.descriptor === descriptor);
if (!account) return; const blockchain = getState().blockchain.find(b => b.shortcut === network.shortcut);
const { network } = getState().selectedAccount; if (!account || !blockchain) return;
if (!network) return; // flowtype fallback
if (!notification.blockHeight) { if (!tx.blockHeight) {
dispatch({ dispatch({
type: PENDING.ADD, type: PENDING.ADD,
payload: { payload: enhanceTransaction(account, tx, network),
...notification,
deviceState: account.deviceState,
network: account.network,
amount: toDecimalAmount(notification.amount, network.decimals),
total:
notification.type === 'send'
? toDecimalAmount(notification.total, network.decimals)
: toDecimalAmount(notification.amount, network.decimals),
fee: toDecimalAmount(notification.fee, network.decimals),
},
}); });
// todo: replace "send success" notification with link to explorer
} else { } else {
dispatch({ dispatch({
type: PENDING.TX_RESOLVED, type: PENDING.TX_RESOLVED,
hash: notification.hash, hash: tx.txid,
}); });
} }
// In case of tx sent between two Trezor accounts there is a possibility that only 1 notification will be received const response = await TrezorConnect.getAccountInfo({
// therefore we need to find target account and update data for it as well descriptor: account.descriptor,
const accountsToUpdate = [account]; coin: account.network,
const targetAddress = });
notification.type === 'send'
? notification.outputs[0].addresses[0]
: notification.inputs[0].addresses[0];
const targetAccount = getState().accounts.find(a => a.descriptor === targetAddress);
if (targetAccount) {
accountsToUpdate.push(targetAccount);
}
accountsToUpdate.forEach(async a => { if (!response.success) return;
const response = await TrezorConnect.rippleGetAccountInfo({
account: {
descriptor: a.descriptor,
from: a.block,
history: false,
},
coin: a.network,
});
if (response.success) { dispatch(
const updatedAccount = response.payload; AccountsActions.update(mergeAccount(response.payload, account, network, blockchain.block))
const empty = updatedAccount.sequence <= 0 && updatedAccount.balance === '0'; );
dispatch(
AccountsActions.update({
networkType: 'ripple',
...a,
balance: toDecimalAmount(updatedAccount.balance, network.decimals),
availableBalance: toDecimalAmount(
updatedAccount.availableBalance,
network.decimals
),
block: updatedAccount.block,
sequence: updatedAccount.sequence,
reserve: toDecimalAmount(updatedAccount.reserve, network.decimals),
empty,
})
);
}
});
}; };

@ -2,7 +2,7 @@
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import * as DISCOVERY from 'actions/constants/discovery'; import * as DISCOVERY from 'actions/constants/discovery';
import { toDecimalAmount } from 'utils/formatUtils'; import { enhanceAccount } from 'utils/accountUtils';
import type { PromiseAction, GetState, Dispatch, TrezorDevice, Network, Account } from 'flowtype'; import type { PromiseAction, GetState, Dispatch, TrezorDevice, Network, Account } from 'flowtype';
import type { Discovery } from 'reducers/DiscoveryReducer'; import type { Discovery } from 'reducers/DiscoveryReducer';
@ -35,16 +35,13 @@ export const discoverAccount = (
const { accountIndex } = discoveryProcess; const { accountIndex } = discoveryProcess;
const path = network.bip44.slice(0).replace('a', accountIndex.toString()); const path = network.bip44.slice(0).replace('a', accountIndex.toString());
const response = await TrezorConnect.rippleGetAccountInfo({ const response = await TrezorConnect.getAccountInfo({
device: { device: {
path: device.path, path: device.path,
instance: device.instance, instance: device.instance,
state: device.state, state: device.state,
}, },
account: { path,
path,
block: 0,
},
keepSession: true, // acquire and hold session keepSession: true, // acquire and hold session
useEmptyPassphrase: device.useEmptyPassphrase, useEmptyPassphrase: device.useEmptyPassphrase,
coin: network.shortcut, coin: network.shortcut,
@ -55,26 +52,9 @@ export const discoverAccount = (
throw new Error(response.payload.error); throw new Error(response.payload.error);
} }
const account = response.payload; return enhanceAccount(response.payload, {
const empty = account.sequence <= 0 && account.balance === '0';
return {
imported: false,
index: discoveryProcess.accountIndex, index: discoveryProcess.accountIndex,
network: network.shortcut, network,
deviceID: device.features ? device.features.device_id : '0', device,
deviceState: device.state || '0', });
accountPath: account.path || [],
descriptor: account.descriptor,
balance: toDecimalAmount(account.balance, network.decimals),
availableBalance: toDecimalAmount(account.availableBalance, network.decimals),
block: account.block,
transactions: account.transactions,
empty,
networkType: 'ripple',
sequence: account.sequence,
reserve: toDecimalAmount(account.reserve, network.decimals),
};
}; };

@ -1,6 +1,7 @@
/* @flow */ /* @flow */
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'trezor-ui-components';
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import * as SEND from 'actions/constants/send'; import * as SEND from 'actions/constants/send';
@ -374,7 +375,7 @@ export const onFeeChange = (fee: string): ThunkAction => (
const state: State = getState().sendFormRipple; const state: State = getState().sendFormRipple;
// switch to custom fee level // switch to custom fee level
let newSelectedFeeLevel = state.selectedFeeLevel; let newSelectedFeeLevel;
if (state.selectedFeeLevel.value !== 'Custom') if (state.selectedFeeLevel.value !== 'Custom')
newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom'); newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom');
@ -385,7 +386,7 @@ export const onFeeChange = (fee: string): ThunkAction => (
...state, ...state,
untouched: false, untouched: false,
touched: { ...state.touched, fee: true }, touched: { ...state.touched, fee: true },
selectedFeeLevel: newSelectedFeeLevel, selectedFeeLevel: newSelectedFeeLevel || state.selectedFeeLevel,
fee, fee,
}, },
}); });
@ -501,7 +502,11 @@ export const onSend = (): AsyncAction => async (
payload: { payload: {
variant: 'success', variant: 'success',
title: <FormattedMessage {...l10nMessages.TR_TRANSACTION_SUCCESS} />, title: <FormattedMessage {...l10nMessages.TR_TRANSACTION_SUCCESS} />,
message: txid, message: (
<Link href={`${network.explorer.tx}${txid}`} isGreen>
<FormattedMessage {...l10nMessages.TR_SEE_TRANSACTION_DETAILS} />
</Link>
),
cancelable: true, cancelable: true,
actions: [], actions: [],
}, },

@ -173,16 +173,14 @@ const addressBalanceValidation = ($state: State): PromiseAction<void> => async (
if (!network) return; if (!network) return;
let minAmount: string = '0'; let minAmount: string = '0';
const response = await TrezorConnect.rippleGetAccountInfo({ const response = await TrezorConnect.getAccountInfo({
account: { descriptor: $state.address,
descriptor: $state.address,
},
coin: network.shortcut, coin: network.shortcut,
}); });
if (response.success) { if (response.success) {
const empty = response.payload.sequence <= 0 && response.payload.balance === '0'; if (response.payload.empty) {
if (empty) { const reserve = response.payload.misc ? response.payload.misc.reserve : '0';
minAmount = toDecimalAmount(response.payload.reserve, network.decimals); minAmount = toDecimalAmount(reserve || '0', network.decimals);
} }
} }

@ -5,7 +5,7 @@ import { Header } from 'trezor-ui-components';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import type { toggleSidebar as toggleSidebarType } from 'actions/WalletActions'; import { toggleSidebar as toggleSidebarAction } from 'actions/WalletActions';
import l10nMessages from './index.messages'; import l10nMessages from './index.messages';
import LanguagePicker from './components/LanguagePicker/Container'; import LanguagePicker from './components/LanguagePicker/Container';
@ -13,7 +13,7 @@ import LanguagePicker from './components/LanguagePicker/Container';
type MyProps = { type MyProps = {
sidebarEnabled?: boolean, sidebarEnabled?: boolean,
sidebarOpened?: ?boolean, sidebarOpened?: ?boolean,
toggleSidebar?: toggleSidebarType, toggleSidebar?: typeof toggleSidebarAction,
}; };
const MyHeader = ({ sidebarEnabled, sidebarOpened, toggleSidebar }: MyProps) => ( const MyHeader = ({ sidebarEnabled, sidebarOpened, toggleSidebar }: MyProps) => (

@ -63,7 +63,7 @@ const Value = styled.div`
text-align: right; text-align: right;
color: ${colors.GREEN_SECONDARY}; color: ${colors.GREEN_SECONDARY};
&.send { &.sent {
color: ${colors.ERROR_PRIMARY}; color: ${colors.ERROR_PRIMARY};
} }
`; `;
@ -77,28 +77,27 @@ const Fee = styled.div`
`; `;
const TransactionItem = ({ tx, network }: Props) => { const TransactionItem = ({ tx, network }: Props) => {
const url = `${network.explorer.tx}${tx.hash}`; const url = `${network.explorer.tx}${tx.txid}`;
const date = typeof tx.timestamp === 'string' ? tx.timestamp : undefined; // TODO: format date const date = typeof tx.blockTime === 'number' ? tx.blockTime : undefined; // TODO: format date
const addresses = (tx.type === 'send' ? tx.outputs : tx.inputs).reduce( const addresses = tx.targets.reduce((arr, item) => arr.concat(item.addresses), []);
(arr, item) => arr.concat(item.addresses),
[] const operation = tx.type === 'sent' ? '-' : '+';
); const amount =
tx.tokens.length > 0 ? (
const operation = tx.type === 'send' ? '-' : '+'; tx.tokens.map(t => (
const amount = tx.tokens ? ( <Amount key={t.symbol}>
tx.tokens.map(t => ( {operation}
<Amount key={t.value}> {t.amount} {t.symbol}
</Amount>
))
) : (
<Amount>
{operation} {operation}
{t.value} {t.shortcut} {tx.amount} {network.symbol}
</Amount> </Amount>
)) );
) : ( const fee =
<Amount> tx.tokens.length > 0 && tx.type === 'sent' ? `${tx.fee} ${network.symbol}` : undefined;
{operation}
{tx.total} {network.symbol}
</Amount>
);
const fee = tx.tokens && tx.type === 'send' ? `${tx.fee} ${network.symbol}` : undefined;
return ( return (
<Wrapper> <Wrapper>
@ -113,7 +112,7 @@ const TransactionItem = ({ tx, network }: Props) => {
))} ))}
{!tx.blockHeight && ( {!tx.blockHeight && (
<TransactionHash href={url} isGray> <TransactionHash href={url} isGray>
Transaction hash: {tx.hash} Transaction hash: {tx.txid}
</TransactionHash> </TransactionHash>
)} )}
</Addresses> </Addresses>

@ -30,20 +30,10 @@ const DeviceIcon = ({
color = COLORS.TEXT_SECONDARY, color = COLORS.TEXT_SECONDARY,
hoverColor, hoverColor,
onClick, onClick,
...rest
}: Props) => { }: Props) => {
const majorVersion = device.features ? device.features.major_version : 2; const majorVersion = device.features ? device.features.major_version : 2;
const icon = getDeviceIcon(majorVersion); const icon = getDeviceIcon(majorVersion);
return ( return <Icon icon={icon} hoverColor={hoverColor} onClick={onClick} color={color} size={size} />;
<Icon
icon={icon}
hoverColor={hoverColor}
onClick={onClick}
color={color}
size={size}
{...rest}
/>
);
}; };
DeviceIcon.propTypes = { DeviceIcon.propTypes = {

@ -16,6 +16,7 @@ import Pin from 'components/modals/pin/Pin';
import InvalidPin from 'components/modals/pin/Invalid'; import InvalidPin from 'components/modals/pin/Invalid';
import Passphrase from 'components/modals/passphrase/Passphrase'; import Passphrase from 'components/modals/passphrase/Passphrase';
import PassphraseType from 'components/modals/passphrase/Type'; import PassphraseType from 'components/modals/passphrase/Type';
import PassphraseOnDevice from 'components/modals/passphrase/OnDevice';
import ConfirmSignTx from 'components/modals/confirm/SignTx'; import ConfirmSignTx from 'components/modals/confirm/SignTx';
import ConfirmAction from 'components/modals/confirm/Action'; import ConfirmAction from 'components/modals/confirm/Action';
import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress'; import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress';
@ -97,6 +98,10 @@ const getDeviceContextModal = (props: Props) => {
} }
} }
case UI.REQUEST_PASSPHRASE_ON_DEVICE:
case 'ButtonRequest_PassphraseEntry':
return <PassphraseOnDevice device={modal.device} />;
case 'ButtonRequest_ProtectCall': case 'ButtonRequest_ProtectCall':
return <ConfirmAction device={modal.device} />; return <ConfirmAction device={modal.device} />;

@ -0,0 +1,43 @@
/* @flow */
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { H5, P, colors } from 'trezor-ui-components';
import DeviceIcon from 'components/images/DeviceIcon';
import type { TrezorDevice } from 'flowtype';
type Props = {
device: TrezorDevice,
};
const Wrapper = styled.div`
max-width: 360px;
padding: 30px 48px;
`;
const StyledDeviceIcon = styled(DeviceIcon)`
margin-bottom: 10px;
`;
const Header = styled.div``;
const PassphraseType = (props: Props) => (
<Wrapper>
<Header>
<StyledDeviceIcon device={props.device} size={32} color={colors.TEXT_SECONDARY} />
<H5>Enter passphrase on {props.device.label} device</H5>
<P size="small">
If you enter a wrong passphrase, you will not unlock the desired hidden wallet.
</P>
</Header>
</Wrapper>
);
PassphraseType.propTypes = {
device: PropTypes.object.isRequired,
};
export default PassphraseType;

@ -73,6 +73,10 @@ const LinkButton = styled(Button)`
} }
`; `;
const OnDeviceButton = styled(Button)`
margin: 10px 0 10px 0;
`;
class Passphrase extends PureComponent<Props, State> { class Passphrase extends PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -169,7 +173,10 @@ class Passphrase extends PureComponent<Props, State> {
})); }));
} }
submitPassphrase(shouldLeavePassphraseBlank: boolean = false) { submitPassphrase(
shouldLeavePassphraseBlank: boolean = false,
passphraseOnDevice: boolean = false
) {
const { onPassphraseSubmit } = this.props; const { onPassphraseSubmit } = this.props;
const passphrase = this.state.passphraseInputValue; const passphrase = this.state.passphraseInputValue;
@ -181,7 +188,7 @@ class Passphrase extends PureComponent<Props, State> {
isPassphraseHidden: true, isPassphraseHidden: true,
}); });
onPassphraseSubmit(shouldLeavePassphraseBlank ? '' : passphrase); onPassphraseSubmit(shouldLeavePassphraseBlank ? '' : passphrase, passphraseOnDevice);
} }
handleKeyPress(event: KeyboardEvent) { handleKeyPress(event: KeyboardEvent) {
@ -209,6 +216,13 @@ class Passphrase extends PureComponent<Props, State> {
); );
} }
const { device } = this.props;
const onDeviceOffer = !!(
device.features &&
device.features.capabilities &&
device.features.capabilities.includes('Capability_PassphraseEntry')
);
return ( return (
<Wrapper> <Wrapper>
<H5> <H5>
@ -270,6 +284,11 @@ class Passphrase extends PureComponent<Props, State> {
<Button isDisabled={!!error} onClick={() => this.submitPassphrase()}> <Button isDisabled={!!error} onClick={() => this.submitPassphrase()}>
<FormattedMessage {...l10nMessages.TR_ENTER} /> <FormattedMessage {...l10nMessages.TR_ENTER} />
</Button> </Button>
{onDeviceOffer && (
<OnDeviceButton isWhite onClick={() => this.submitPassphrase(false, true)}>
Enter passphrase on device
</OnDeviceButton>
)}
</Row> </Row>
<Footer> <Footer>
<P size="small"> <P size="small">

@ -36,33 +36,19 @@ import type { ImportAccountAction } from 'actions/ImportAccountActions';
import type { FiatRateAction } from 'services/CoingeckoService'; // this service has no action file, all is written inside one file import type { FiatRateAction } from 'services/CoingeckoService'; // this service has no action file, all is written inside one file
import type { import type {
Device, KnownDevice,
Features, UnknownDevice,
DeviceStatus, TransportEvent,
FirmwareRelease,
DeviceFirmwareStatus,
DeviceMode,
DeviceMessageType,
TransportMessageType,
UiMessageType,
BlockchainEvent, BlockchainEvent,
BlockchainLinkTransaction, UiEvent,
DeviceEvent,
AccountTransaction,
} from 'trezor-connect'; } from 'trezor-connect';
import type { RouterAction, LocationState } from 'connected-react-router'; import type { RouterAction, LocationState } from 'connected-react-router';
export type AcquiredDevice = $Exact<{ export type ExtendedDevice = {|
+type: 'acquired',
path: string,
+label: string,
+features: Features,
+firmware: DeviceFirmwareStatus,
+firmwareRelease: ?FirmwareRelease,
status: DeviceStatus,
+mode: DeviceMode,
state: ?string,
useEmptyPassphrase: boolean, useEmptyPassphrase: boolean,
remember: boolean, // device should be remembered remember: boolean, // device should be remembered
connected: boolean, // device is connected connected: boolean, // device is connected
available: boolean, // device cannot be used because of features.passphrase_protection is different then expected available: boolean, // device cannot be used because of features.passphrase_protection is different then expected
@ -70,29 +56,16 @@ export type AcquiredDevice = $Exact<{
instanceLabel: string, instanceLabel: string,
instanceName: ?string, instanceName: ?string,
ts: number, ts: number,
}>; |};
export type UnknownDevice = $Exact<{
+type: 'unacquired' | 'unreadable',
path: string,
+label: string,
+features: null,
state: ?string,
useEmptyPassphrase: boolean,
remember: boolean, // device should be remembered export type AcquiredDevice = {| ...KnownDevice, ...ExtendedDevice |};
connected: boolean, // device is connected export type UnacquiredDevice = {| ...UnknownDevice, ...ExtendedDevice |};
available: boolean, // device cannot be used because of features.passphrase_protection is different then expected
instance?: number,
instanceLabel: string,
instanceName: ?string,
ts: number,
}>;
export type { Device } from 'trezor-connect'; export type { Device } from 'trezor-connect';
export type TrezorDevice = AcquiredDevice | UnknownDevice; export type TrezorDevice = AcquiredDevice | UnacquiredDevice;
export type Transaction = BlockchainLinkTransaction & { export type Transaction = AccountTransaction & {
descriptor: string,
deviceState: string, deviceState: string,
network: string, network: string,
rejected?: boolean, rejected?: boolean,
@ -100,38 +73,11 @@ export type Transaction = BlockchainLinkTransaction & {
export type RouterLocationState = LocationState; export type RouterLocationState = LocationState;
// Cast event from TrezorConnect event listener to react Action
type DeviceEventAction = {
type: DeviceMessageType,
device: Device,
};
type TransportEventAction = {
type: TransportMessageType,
payload: any,
};
type UiEventAction = {
type: UiMessageType,
payload: any,
// payload: {
// device: Device;
// code?: string;
// },
};
// TODO: join this message with uiMessage
type IFrameHandshake = {
type: 'iframe_handshake',
payload: any,
};
export type Action = export type Action =
| RouterAction | RouterAction
| IFrameHandshake | TransportEvent
| TransportEventAction | DeviceEvent
| DeviceEventAction | UiEvent
| UiEventAction
| BlockchainEvent | BlockchainEvent
| SelectedAccountAction | SelectedAccountAction
| AccountAction | AccountAction

@ -1,58 +0,0 @@
/* @flow */
declare module 'bignumber.js' {
declare type $npm$big$number$object = number | string | T_BigNumber
declare type $npm$cmp$result = -1 | 0 | 1
declare type DIGIT = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
declare type ROUND_DOWN = 0
declare type ROUND_HALF_UP = 1
declare type ROUND_HALF_EVEN = 2
declare type ROUND_UP = 3
declare type RM = ROUND_DOWN | ROUND_HALF_UP | ROUND_HALF_EVEN | ROUND_UP
declare class T_BigNumber {
// Properties
static DP: number;
static RM: RM;
static E_NEG: number;
static E_POS: number;
c: Array<DIGIT>;
e: number;
s: -1 | 1;
// Constructors
static (value: $npm$big$number$object): T_BigNumber;
constructor(value: $npm$big$number$object): T_BigNumber;
// Methods
abs(): T_BigNumber;
div(n: $npm$big$number$object): T_BigNumber;
dividedBy(n: $npm$big$number$object): T_BigNumber;
eq(n: $npm$big$number$object): boolean;
gt(n: $npm$big$number$object): boolean;
isGreaterThan(n: $npm$big$number$object): boolean;
gte(n: $npm$big$number$object): boolean;
lt(n: $npm$big$number$object): boolean;
isLessThan(n: $npm$big$number$object): boolean;
lte(n: $npm$big$number$object): boolean;
isLessThanOrEqualTo(n: $npm$big$number$object): boolean;
isNaN(): boolean;
minus(n: $npm$big$number$object): T_BigNumber;
mod(n: $npm$big$number$object): T_BigNumber;
plus(n: $npm$big$number$object): T_BigNumber;
pow(exp: number): BigNumber;
sqrt(): T_BigNumber;
times(n: $npm$big$number$object): T_BigNumber;
toExponential(dp: ?number): string;
toFixed(dp: ?number): string;
toPrecision(sd: ?number): string;
toString(format?: number): string;
toNumber(): number;
valueOf(): string;
toJSON(): string;
}
//declare module.exports: typeof T_BigNumber
declare export default typeof T_BigNumber;
}

@ -1,5 +1,3 @@
/* @flow */
declare module 'redux' { declare module 'redux' {
/* /*
S = State S = State
@ -10,7 +8,7 @@ declare module 'redux' {
declare export type DispatchAPI<A> = (action: A) => A; declare export type DispatchAPI<A> = (action: A) => A;
// old Dispatch needs to stay as it is, because also "react-redux" is using this type // old Dispatch needs to stay as it is, because also "react-redux" is using this type
declare export type Dispatch<A: { type: $Subtype<string> }> = DispatchAPI<A>; declare export type Dispatch<A: { type: * }> = DispatchAPI<A>;
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>;
@ -21,7 +19,7 @@ declare module 'redux' {
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 PromiseDispatch<S, A> = <R>(action: PromiseAction<S, A, R>) => Promise<R>;
declare export type PayloadDispatch<S, A> = <R>(action: PayloadAction<S, A, R>) => R; declare export type PayloadDispatch<S, A> = <R>(action: PayloadAction<S, A, R>) => R;
declare export type PlainDispatch<A: {type: $Subtype<string>}> = DispatchAPI<A>; declare export type PlainDispatch<A: { type: * }> = 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> & PromiseDispatch<S, A> & PayloadDispatch<S, A>; declare export type ReduxDispatch<S, A> = PlainDispatch<A> & ThunkDispatch<S, A> & AsyncDispatch<S, A> & PromiseDispatch<S, A> & PayloadDispatch<S, A>;

@ -6,13 +6,13 @@ import * as ACCOUNT from 'actions/constants/account';
import type { Action, TrezorDevice } from 'flowtype'; import type { Action, TrezorDevice } from 'flowtype';
type AccountCommon = { type AccountCommon = {|
+imported: boolean, +imported: boolean,
+index: number, +index: number,
+network: string, // network id (shortcut) +network: string, // network id (shortcut)
+deviceID: string, // empty for imported accounts +deviceID: string, // empty for imported accounts
+deviceState: string, // empty for imported accounts +deviceState: string, // empty for imported accounts
+accountPath: Array<number>, // empty for imported accounts +accountPath: string, // empty for imported accounts
+descriptor: string, // address or xpub +descriptor: string, // address or xpub
balance: string, balance: string,
@ -21,22 +21,31 @@ type AccountCommon = {
empty: boolean, // account without transactions empty: boolean, // account without transactions
transactions: number, // deprecated transactions: number, // deprecated
}; |};
export type Account = export type Account =
| (AccountCommon & { | {|
networkType: 'ethereum', ...AccountCommon,
nonce: number, ...{|
}) networkType: 'ethereum',
| (AccountCommon & { nonce: string,
networkType: 'ripple', |},
sequence: number, |}
reserve: string, | {|
}) ...AccountCommon,
| (AccountCommon & { ...{|
networkType: 'bitcoin', networkType: 'ripple',
addressIndex: number, sequence: number,
}); reserve: string,
|},
|}
| {|
...AccountCommon,
...{|
networkType: 'bitcoin',
addressIndex: number,
|},
|};
export type State = Array<Account>; export type State = Array<Account>;
@ -51,14 +60,12 @@ export const findDeviceAccounts = (
return state.filter( return state.filter(
addr => addr =>
(addr.deviceState === device.state || (addr.deviceState === device.state ||
(addr.imported && addr.deviceID === (device.features || {}).device_id)) && (addr.imported && addr.deviceID === device.id)) &&
addr.network === network addr.network === network
); );
} }
return state.filter( return state.filter(
addr => addr => addr.deviceState === device.state || (addr.imported && addr.deviceID === device.id)
addr.deviceState === device.state ||
(addr.imported && addr.deviceID === (device.features || {}).device_id)
); );
}; };

@ -4,7 +4,7 @@ import { BLOCKCHAIN as BLOCKCHAIN_EVENT } from 'trezor-connect';
import * as BLOCKCHAIN_ACTION from 'actions/constants/blockchain'; import * as BLOCKCHAIN_ACTION from 'actions/constants/blockchain';
import type { Action } from 'flowtype'; import type { Action } from 'flowtype';
import type { BlockchainConnect, BlockchainError, BlockchainBlock } from 'trezor-connect'; import type { BlockchainInfo, BlockchainError, BlockchainBlock } from 'trezor-connect';
export type BlockchainFeeLevel = { export type BlockchainFeeLevel = {
name: string, name: string,
@ -50,16 +50,15 @@ const onStartSubscribe = (state: State, shortcut: string): State => {
]); ]);
}; };
const onConnect = (state: State, action: BlockchainConnect): State => { const onConnect = (state: State, info: BlockchainInfo): State => {
const shortcut = action.payload.coin.shortcut.toLowerCase(); const shortcut = info.coin.shortcut.toLowerCase();
const network = state.find(b => b.shortcut === shortcut); const network = state.find(b => b.shortcut === shortcut);
const { info } = action.payload;
if (network) { if (network) {
const others = state.filter(b => b !== network); const others = state.filter(b => b !== network);
return others.concat([ return others.concat([
{ {
...network, ...network,
block: info.block, block: info.blockHeight,
connected: true, connected: true,
connecting: false, connecting: false,
reconnectionAttempts: 0, reconnectionAttempts: 0,
@ -73,15 +72,15 @@ const onConnect = (state: State, action: BlockchainConnect): State => {
connected: true, connected: true,
connecting: false, connecting: false,
reconnectionAttempts: 0, reconnectionAttempts: 0,
block: info.block, block: info.blockHeight,
feeTimestamp: 0, feeTimestamp: 0,
feeLevels: [], feeLevels: [],
}, },
]); ]);
}; };
const onError = (state: State, action: BlockchainError): State => { const onError = (state: State, payload: BlockchainError): State => {
const shortcut = action.payload.coin.shortcut.toLowerCase(); const shortcut = payload.coin.shortcut.toLowerCase();
const network = state.find(b => b.shortcut === shortcut); const network = state.find(b => b.shortcut === shortcut);
if (network) { if (network) {
const others = state.filter(b => b !== network); const others = state.filter(b => b !== network);
@ -108,15 +107,15 @@ const onError = (state: State, action: BlockchainError): State => {
]); ]);
}; };
const onBlock = (state: State, action: BlockchainBlock): State => { const onBlock = (state: State, payload: BlockchainBlock): State => {
const shortcut = action.payload.coin.shortcut.toLowerCase(); const shortcut = payload.coin.shortcut.toLowerCase();
const network = state.find(b => b.shortcut === shortcut); const network = state.find(b => b.shortcut === shortcut);
if (network) { if (network) {
const others = state.filter(b => b !== network); const others = state.filter(b => b !== network);
return others.concat([ return others.concat([
{ {
...network, ...network,
block: action.payload.block, block: payload.blockHeight,
}, },
]); ]);
} }
@ -143,11 +142,11 @@ export default (state: State = initialState, action: Action): State => {
case BLOCKCHAIN_ACTION.START_SUBSCRIBE: case BLOCKCHAIN_ACTION.START_SUBSCRIBE:
return onStartSubscribe(state, action.shortcut); return onStartSubscribe(state, action.shortcut);
case BLOCKCHAIN_EVENT.CONNECT: case BLOCKCHAIN_EVENT.CONNECT:
return onConnect(state, action); return onConnect(state, action.payload);
case BLOCKCHAIN_EVENT.ERROR: case BLOCKCHAIN_EVENT.ERROR:
return onError(state, action); return onError(state, action.payload);
case BLOCKCHAIN_EVENT.BLOCK: case BLOCKCHAIN_EVENT.BLOCK:
return onBlock(state, action); return onBlock(state, action.payload);
case BLOCKCHAIN_ACTION.UPDATE_FEE: case BLOCKCHAIN_ACTION.UPDATE_FEE:
return updateFee(state, action.shortcut, action.feeLevels); return updateFee(state, action.shortcut, action.feeLevels);

@ -62,8 +62,9 @@ const mergeDevices = (current: TrezorDevice, upcoming: Device | TrezorDevice): T
} }
return { return {
...upcoming, ...upcoming,
features: null,
...extended, ...extended,
features: null,
state: null,
}; };
}; };
@ -79,9 +80,7 @@ const addDevice = (state: State, device: Device): State => {
} }
otherDevices = state.filter(d => affectedDevices.indexOf(d) === -1); otherDevices = state.filter(d => affectedDevices.indexOf(d) === -1);
} else { } else {
affectedDevices = state.filter( affectedDevices = state.filter(d => d.features && device.features && d.id === device.id);
d => d.features && device.features && d.features.device_id === device.features.device_id
);
const unacquiredDevices = state.filter(d => d.path.length > 0 && d.path === device.path); const unacquiredDevices = state.filter(d => d.path.length > 0 && d.path === device.path);
otherDevices = state.filter( otherDevices = state.filter(
d => affectedDevices.indexOf(d) < 0 && unacquiredDevices.indexOf(d) < 0 d => affectedDevices.indexOf(d) < 0 && unacquiredDevices.indexOf(d) < 0
@ -191,7 +190,7 @@ const changeDevice = (state: State, device: Device | TrezorDevice, extended: Obj
d => d =>
(d.features && (d.features &&
device.features && device.features &&
d.features.device_id === device.features.device_id && d.id === device.id &&
d.features.passphrase_protection === device.features.passphrase_protection) || d.features.passphrase_protection === device.features.passphrase_protection) ||
(d.features && d.path.length > 0 && d.path === device.path) (d.features && d.path.length > 0 && d.path === device.path)
); );
@ -247,7 +246,7 @@ const forgetDevice = (state: State, device: TrezorDevice): State =>
state.filter( state.filter(
d => d =>
d.remember || d.remember ||
(d.features && device.features && d.features.device_id !== device.features.device_id) || (d.features && device.features && d.id !== device.id) ||
(!d.features && d.path !== device.path) (!d.features && d.path !== device.path)
); );
@ -261,9 +260,7 @@ const forgetSingleDevice = (state: State, device: TrezorDevice): State => {
const disconnectDevice = (state: State, device: Device): State => { const disconnectDevice = (state: State, device: Device): State => {
const affectedDevices: State = state.filter( const affectedDevices: State = state.filter(
d => d => d.path === device.path || (d.features && device.features && d.id === device.id)
d.path === device.path ||
(d.features && device.features && d.features.device_id === device.features.device_id)
); );
const otherDevices: State = state.filter(d => affectedDevices.indexOf(d) === -1); const otherDevices: State = state.filter(d => affectedDevices.indexOf(d) === -1);
@ -308,9 +305,7 @@ const onSelectedDevice = (state: State, device: ?TrezorDevice): State => {
const onChangeWalletType = (state: State, device: TrezorDevice, hidden: boolean): State => { const onChangeWalletType = (state: State, device: TrezorDevice, hidden: boolean): State => {
const affectedDevices: State = state.filter( const affectedDevices: State = state.filter(
d => d => d.path === device.path || (d.features && device.features && d.id === device.id)
d.path === device.path ||
(d.features && device.features && d.features.device_id === device.features.device_id)
); );
const otherDevices: State = state.filter(d => affectedDevices.indexOf(d) === -1); const otherDevices: State = state.filter(d => affectedDevices.indexOf(d) === -1);
if (affectedDevices.length > 0) { if (affectedDevices.length > 0) {
@ -354,16 +349,16 @@ export default function devices(state: State = initialState, action: Action): St
case DEVICE.CONNECT: case DEVICE.CONNECT:
case DEVICE.CONNECT_UNACQUIRED: case DEVICE.CONNECT_UNACQUIRED:
return addDevice(state, action.device); return addDevice(state, action.payload);
case DEVICE.CHANGED: case DEVICE.CHANGED:
// return changeDevice(state, { ...action.device, connected: true, available: true }); // return changeDevice(state, { ...action.device, connected: true, available: true });
return changeDevice(state, action.device, { connected: true, available: true }); return changeDevice(state, action.payload, { connected: true, available: true });
// TODO: check if available will propagate to unavailable // TODO: check if available will propagate to unavailable
case DEVICE.DISCONNECT: case DEVICE.DISCONNECT:
// case DEVICE.DISCONNECT_UNACQUIRED: // case DEVICE.DISCONNECT_UNACQUIRED:
return disconnectDevice(state, action.device); return disconnectDevice(state, action.payload);
case WALLET.SET_SELECTED_DEVICE: case WALLET.SET_SELECTED_DEVICE:
return onSelectedDevice(state, action.device); return onSelectedDevice(state, action.device);

@ -1,7 +1,5 @@
/* @flow */ /* @flow */
import HDKey from 'hdkey';
import * as DISCOVERY from 'actions/constants/discovery'; import * as DISCOVERY from 'actions/constants/discovery';
import * as ACCOUNT from 'actions/constants/account'; import * as ACCOUNT from 'actions/constants/account';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
@ -27,10 +25,6 @@ export type Discovery = {
waitingForBlockchain: boolean, waitingForBlockchain: boolean,
fwNotSupported: boolean, fwNotSupported: boolean,
fwOutdated: boolean, fwOutdated: boolean,
publicKey: string, // used in ethereum only
chainCode: string, // used in ethereum only
hdKey: HDKey, // used in ethereum only
}; };
export type State = Array<Discovery>; export type State = Array<Discovery>;
@ -46,10 +40,6 @@ const defaultDiscovery: Discovery = {
waitingForBlockchain: false, waitingForBlockchain: false,
fwNotSupported: false, fwNotSupported: false,
fwOutdated: false, fwOutdated: false,
publicKey: '',
chainCode: '',
hdKey: null,
}; };
const findIndex = (state: State, network: string, deviceState: string): number => const findIndex = (state: State, network: string, deviceState: string): number =>
@ -63,18 +53,6 @@ const start = (state: State, action: DiscoveryStartAction): State => {
deviceState, deviceState,
}; };
if (action.networkType === 'ethereum') {
const hdKey = new HDKey();
hdKey.publicKey = Buffer.from(action.publicKey, 'hex');
hdKey.chainCode = Buffer.from(action.chainCode, 'hex');
instance.hdKey = hdKey;
instance.publicKey = action.publicKey;
instance.chainCode = action.chainCode;
instance.basePath = action.basePath;
}
const newState: State = [...state]; const newState: State = [...state];
const index: number = findIndex(state, action.network.shortcut, deviceState); const index: number = findIndex(state, action.network.shortcut, deviceState);
if (index >= 0) { if (index >= 0) {
@ -202,15 +180,8 @@ export default function discovery(state: State = initialState, action: Action):
return notSupported(state, action); return notSupported(state, action);
case DISCOVERY.FROM_STORAGE: case DISCOVERY.FROM_STORAGE:
return action.payload.map(d => { return action.payload.map(d => {
if (d.publicKey.length < 1) return d;
// recreate ethereum discovery HDKey
// deprecated: will be removed after switching to blockbook
const hdKey: HDKey = new HDKey();
hdKey.publicKey = Buffer.from(d.publicKey, 'hex');
hdKey.chainCode = Buffer.from(d.chainCode, 'hex');
return { return {
...d, ...d,
hdKey,
interrupted: false, interrupted: false,
waitingForDevice: false, waitingForDevice: false,
waitingForBlockchain: false, waitingForBlockchain: false,

@ -67,7 +67,7 @@ export default function modal(state: State = initialState, action: Action): Stat
case DEVICE.DISCONNECT: case DEVICE.DISCONNECT:
if ( if (
state.context === MODAL.CONTEXT_DEVICE && state.context === MODAL.CONTEXT_DEVICE &&
action.device.path === state.device.path action.payload.path === state.device.path
) { ) {
return initialState; return initialState;
} }
@ -76,16 +76,17 @@ export default function modal(state: State = initialState, action: Action): Stat
case UI.REQUEST_PIN: case UI.REQUEST_PIN:
case UI.INVALID_PIN: case UI.INVALID_PIN:
case UI.REQUEST_PASSPHRASE: case UI.REQUEST_PASSPHRASE:
case UI.REQUEST_PASSPHRASE_ON_DEVICE:
return { return {
context: MODAL.CONTEXT_DEVICE, context: MODAL.CONTEXT_DEVICE,
device: action.payload.device, device: (action.payload.device: any), // should be TrezorDevice, but ath this point it doesn't matter, Device is enough
windowType: action.type, windowType: action.type,
}; };
case UI.REQUEST_BUTTON: case UI.REQUEST_BUTTON:
return { return {
context: MODAL.CONTEXT_DEVICE, context: MODAL.CONTEXT_DEVICE,
device: action.payload.device, device: (action.payload.device: any), // should be TrezorDevice, but ath this point it doesn't matter, Device is enough
windowType: action.payload.code, windowType: action.payload.code,
}; };

@ -66,7 +66,7 @@ const closeNotification = (state: State, payload: any): State => {
export default function notification(state: State = initialState, action: Action): State { export default function notification(state: State = initialState, action: Action): State {
switch (action.type) { switch (action.type) {
case DEVICE.DISCONNECT: { case DEVICE.DISCONNECT: {
const { path } = action.device; // Flow warning const { path } = action.payload;
return state.filter(entry => entry.devicePath !== path); return state.filter(entry => entry.devicePath !== path);
} }

@ -17,12 +17,12 @@ const add = (state: State, payload: Transaction): State => {
const removeByDeviceState = (state: State, deviceState: ?string): State => const removeByDeviceState = (state: State, deviceState: ?string): State =>
state.filter(tx => tx.deviceState !== deviceState); state.filter(tx => tx.deviceState !== deviceState);
const removeByHash = (state: State, hash: string): State => state.filter(tx => tx.hash !== hash); const removeByHash = (state: State, hash: string): State => state.filter(tx => tx.txid !== hash);
const reject = (state: State, hash: string): State => const reject = (state: State, hash: string): State =>
state.map(tx => { state.map(tx => {
if (tx.hash === hash && !tx.rejected) { if (tx.txid === hash && !tx.rejected) {
return { ...tx, rejected: true }; return Object.assign({}, { rejected: true }, tx);
} }
return tx; return tx;
}); });

@ -1,7 +1,7 @@
/* @flow */ /* @flow */
import { TRANSPORT, UI } from 'trezor-connect'; import { TRANSPORT, IFRAME } from 'trezor-connect';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
import type { TransportInfo, BridgeInfo } from 'trezor-connect';
import type { Action } from 'flowtype'; import type { Action } from 'flowtype';
export type SelectedDevice = { export type SelectedDevice = {
@ -9,26 +9,14 @@ export type SelectedDevice = {
instance: ?number, instance: ?number,
}; };
export type LatestBridge = {
version: Array<number>,
directory: string,
packages: Array<{ name: string, url: string, signature?: string, preferred: boolean }>,
changelog: Array<string>,
};
export type State = { export type State = {
initialized: boolean, initialized: boolean,
error: ?string, error: ?string,
transport: transport:
| { | TransportInfo
type: string,
version: string,
outdated: boolean,
bridge: LatestBridge,
}
| { | {
type: null, type: null,
bridge: LatestBridge, bridge: ?BridgeInfo,
}, },
// browserState: { // browserState: {
// name: string; // name: string;
@ -46,12 +34,7 @@ const initialState: State = {
error: null, error: null,
transport: { transport: {
type: null, type: null,
bridge: { bridge: null,
version: [],
directory: '',
packages: [],
changelog: [],
},
}, },
browserState: {}, browserState: {},
acquiringDevice: false, acquiringDevice: false,
@ -66,11 +49,10 @@ export default function connect(state: State = initialState, action: Action): St
error: action.error, error: action.error,
}; };
// trezor-connect iframe loaded // trezor-connect iframe loaded
case UI.IFRAME_HANDSHAKE: case IFRAME.LOADED:
return { return {
...state, ...state,
initialized: true, initialized: true,
browserState: action.payload.browser,
}; };
// trezor-connect (trezor-link) initialized // trezor-connect (trezor-link) initialized
case TRANSPORT.START: case TRANSPORT.START:

@ -98,7 +98,7 @@ export default function wallet(state: State = initialState, action: Action): Sta
}; };
case DEVICE.DISCONNECT: case DEVICE.DISCONNECT:
if (state.disconnectRequest && action.device.path === state.disconnectRequest.path) { if (state.disconnectRequest && action.payload.path === state.disconnectRequest.path) {
return { return {
...state, ...state,
disconnectRequest: null, disconnectRequest: null,

@ -27,11 +27,7 @@ export const getSelectedDevice = (state: State): ?TrezorDevice => {
if (d.mode === 'bootloader' && d.path === locationState.device) { if (d.mode === 'bootloader' && d.path === locationState.device) {
return true; return true;
} }
if ( if (d.features && d.id === locationState.device && d.instance === instance) {
d.features &&
d.features.device_id === locationState.device &&
d.instance === instance
) {
return true; return true;
} }
return false; return false;
@ -46,7 +42,7 @@ export const findDevice = (
): ?TrezorDevice => ): ?TrezorDevice =>
devices.find(d => { devices.find(d => {
// TODO: && (instance && d.instance === instance) // TODO: && (instance && d.instance === instance)
if (d.features && d.features.device_id === deviceId && d.state === deviceState) { if (d.features && d.id === deviceId && d.state === deviceState) {
return true; return true;
} }
return false; return false;
@ -60,9 +56,7 @@ export const getDuplicateInstanceNumber = (
// find device(s) with the same features.device_id // find device(s) with the same features.device_id
// and sort them by instance number // and sort them by instance number
const affectedDevices: Array<TrezorDevice> = devices const affectedDevices: Array<TrezorDevice> = devices
.filter( .filter(d => d.features && device.features && d.id === device.id)
d => d.features && device.features && d.features.device_id === device.features.device_id
)
.sort((a, b) => { .sort((a, b) => {
if (!a.instance) { if (!a.instance) {
return -1; return -1;
@ -92,8 +86,7 @@ export const getSelectedAccount = (state: State): ?Account => {
return state.accounts.find( return state.accounts.find(
a => a =>
a.imported === isImported && a.imported === isImported &&
(a.deviceState === device.state || (a.deviceState === device.state || (a.imported && a.deviceID === device.id)) &&
(a.imported && a.deviceID === (device.features || {}).device_id)) &&
a.index === index && a.index === index &&
a.network === locationState.network a.network === locationState.network
); );
@ -129,8 +122,8 @@ export const getAccountPendingTx = (
export const getPendingSequence = (pending: Array<Transaction>): number => export const getPendingSequence = (pending: Array<Transaction>): number =>
pending.reduce((value: number, tx: Transaction): number => { pending.reduce((value: number, tx: Transaction): number => {
if (tx.rejected) return value; if (!tx.ethereumSpecific || tx.rejected) return value;
return Math.max(value, tx.sequence + 1); return Math.max(value, tx.ethereumSpecific.nonce + 1);
}, 0); }, 0);
export const getPendingAmount = ( export const getPendingAmount = (
@ -139,7 +132,7 @@ export const getPendingAmount = (
token: boolean = false token: boolean = false
): BigNumber => ): BigNumber =>
pending.reduce((value: BigNumber, tx: Transaction): BigNumber => { pending.reduce((value: BigNumber, tx: Transaction): BigNumber => {
if (tx.type !== 'send') return value; if (tx.type !== 'sent') return value;
if (!token) { if (!token) {
// regular transactions // regular transactions
// add fees from token txs and amount from regular txs // add fees from token txs and amount from regular txs
@ -147,9 +140,9 @@ export const getPendingAmount = (
} }
if (tx.tokens) { if (tx.tokens) {
// token transactions // token transactions
const allTokens = tx.tokens.filter(t => t.shortcut === currency); const allTokens = tx.tokens.filter(t => t.symbol === currency);
const tokensValue: BigNumber = allTokens.reduce( const tokensValue: BigNumber = allTokens.reduce(
(tv, t) => new BigNumber(value).plus(t.value), (tv, t) => new BigNumber(value).plus(t.amount),
new BigNumber('0') new BigNumber('0')
); );
return new BigNumber(value).plus(tokensValue); return new BigNumber(value).plus(tokensValue);

@ -34,10 +34,10 @@ const LogService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch
); );
break; break;
case DEVICE.CONNECT: case DEVICE.CONNECT:
api.dispatch(LogActions.add('Device connected', action.device)); api.dispatch(LogActions.add('Device connected', action.payload));
break; break;
case DEVICE.DISCONNECT: case DEVICE.DISCONNECT:
api.dispatch(LogActions.add('Device disconnected', action.device)); api.dispatch(LogActions.add('Device disconnected', action.payload));
break; break;
case DISCOVERY.START: case DISCOVERY.START:
api.dispatch(LogActions.add('Discovery started', action)); api.dispatch(LogActions.add('Discovery started', action));

@ -30,7 +30,7 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
} else if (action.type === BLOCKCHAIN_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.payload));
} else if (action.type === CONNECT.REMEMBER_REQUEST) { } else if (action.type === CONNECT.REMEMBER_REQUEST) {
api.dispatch(ModalActions.onRememberRequest(prevModalState)); api.dispatch(ModalActions.onRememberRequest(prevModalState));
} else if (action.type === CONNECT.FORGET) { } else if (action.type === CONNECT.FORGET) {
@ -48,7 +48,7 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
api.dispatch(RouterActions.selectFirstAvailableDevice()); api.dispatch(RouterActions.selectFirstAvailableDevice());
} }
} else if (action.type === DEVICE.CONNECT || action.type === DEVICE.CONNECT_UNACQUIRED) { } else if (action.type === DEVICE.CONNECT || action.type === DEVICE.CONNECT_UNACQUIRED) {
api.dispatch(ModalActions.onDeviceConnect(action.device)); api.dispatch(ModalActions.onDeviceConnect(action.payload));
} else if (action.type === CONNECT.DUPLICATE) { } else if (action.type === CONNECT.DUPLICATE) {
api.dispatch(RouterActions.selectDevice(action.device)); api.dispatch(RouterActions.selectDevice(action.device));
} else if (action.type === BLOCKCHAIN.BLOCK) { } else if (action.type === BLOCKCHAIN.BLOCK) {

@ -56,7 +56,7 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
api.dispatch(TrezorConnectActions.requestWalletType()); api.dispatch(TrezorConnectActions.requestWalletType());
break; break;
case DEVICE.CONNECT: case DEVICE.CONNECT:
api.dispatch(WalletActions.clearUnavailableDevicesData(prevState, action.device)); api.dispatch(WalletActions.clearUnavailableDevicesData(prevState, action.payload));
break; break;
default: { default: {
break; break;

@ -42,9 +42,9 @@ describe('device utils', () => {
}); });
it('isWebUSB', () => { it('isWebUSB', () => {
expect(utils.isWebUSB({ type: 'webusb', version: '1.6.0' })).toBe(true); expect(utils.isWebUSB({ type: 'WebUsbPlugin', version: '1.6.0' })).toBe(true);
expect(utils.isWebUSB({ type: 'aaaa', version: 'aaaaaa' })).toBe(false); expect(utils.isWebUSB({ type: 'aaaa', version: 'aaaaaa' })).toBe(false);
expect(utils.isWebUSB({ type: 'webusb' })).toBe(true); expect(utils.isWebUSB({ type: 'WebUsbPlugin' })).toBe(true);
}); });
it('isDisabled', () => { it('isDisabled', () => {

@ -0,0 +1,114 @@
/* @flow */
import { toDecimalAmount } from 'utils/formatUtils';
import type { AccountInfo, AccountTransaction } from 'trezor-connect';
import type { Account, Transaction, Network, TrezorDevice } from 'flowtype';
// Merge fresh AccountInfo into existing Account
export const mergeAccount = (
info: AccountInfo,
account: Account,
network: Network,
block: number
): Account => {
if (account.networkType === 'ethereum') {
const nonce = info.misc && info.misc.nonce ? info.misc.nonce : '0';
return {
networkType: 'ethereum',
...account,
balance: toDecimalAmount(info.balance, network.decimals),
availableBalance: toDecimalAmount(info.availableBalance, network.decimals),
block,
transactions: info.history.total,
empty: account.empty,
nonce,
};
}
if (account.networkType === 'ripple') {
const sequence = info.misc && info.misc.sequence ? info.misc.sequence : 0;
const reserve = info.misc && info.misc.reserve ? info.misc.reserve : '0';
return {
...account,
balance: toDecimalAmount(info.balance, network.decimals),
availableBalance: toDecimalAmount(info.availableBalance, network.decimals),
block,
empty: info.empty,
networkType: 'ripple',
sequence,
reserve: toDecimalAmount(reserve || '0', network.decimals),
};
}
return account;
};
type EnhanceAccountOptions = {
index: number,
network: Network,
device: TrezorDevice,
imported?: boolean,
block?: number,
};
// Create Account from AccountInfo
export const enhanceAccount = (account: AccountInfo, options: EnhanceAccountOptions): Account => {
if (options.network.type === 'ethereum') {
const nonce = account.misc && account.misc.nonce ? account.misc.nonce : '0';
return {
imported: !!options.imported,
index: options.index,
network: options.network.shortcut,
deviceID: options.device.id || '0',
deviceState: options.device.state || '0',
accountPath: account.path,
descriptor: account.descriptor,
balance: toDecimalAmount(account.balance, options.network.decimals),
availableBalance: toDecimalAmount(account.availableBalance, options.network.decimals),
block: options.block || 0,
transactions: account.history.total,
empty: account.empty,
networkType: 'ethereum',
nonce,
};
}
const sequence = account.misc && account.misc.sequence ? account.misc.sequence : 0;
const reserve = account.misc && account.misc.reserve ? account.misc.reserve : '0';
return {
imported: !!options.imported,
index: options.index,
network: options.network.shortcut,
deviceID: options.device.id || '0',
deviceState: options.device.state || '0',
accountPath: account.path,
descriptor: account.descriptor,
balance: toDecimalAmount(account.balance, options.network.decimals),
availableBalance: toDecimalAmount(account.availableBalance, options.network.decimals),
block: options.block || 0,
transactions: 0,
empty: account.empty,
networkType: 'ripple',
sequence,
reserve: toDecimalAmount(reserve, options.network.decimals),
};
};
export const enhanceTransaction = (
account: Account,
tx: AccountTransaction,
network: Network
): Transaction => {
return {
...tx,
descriptor: account.descriptor,
deviceState: account.deviceState,
network: account.network,
amount: toDecimalAmount(tx.amount, network.decimals),
fee: toDecimalAmount(tx.fee, network.decimals),
};
};

@ -79,7 +79,8 @@ export const getStatusName = (deviceStatus: string, intl: IntlShape): string =>
} }
}; };
export const isWebUSB = (transport: Transport) => !!(transport.type && transport.type === 'webusb'); export const isWebUSB = (transport: Transport) =>
!!(transport.type && transport.type === 'WebUsbPlugin');
export const isDisabled = ( export const isDisabled = (
selectedDevice: TrezorDevice, selectedDevice: TrezorDevice,

@ -4,7 +4,7 @@ import type { TrezorDevice } from 'flowtype';
const getOldWalletUrl = (device: ?TrezorDevice): string => { const getOldWalletUrl = (device: ?TrezorDevice): string => {
if (!device || !device.firmwareRelease) return urlConstants.OLD_WALLET_BETA; if (!device || !device.firmwareRelease) return urlConstants.OLD_WALLET_BETA;
const release = device.firmwareRelease; const { release } = device.firmwareRelease;
const url = release.channel === 'beta' ? urlConstants.OLD_WALLET_BETA : urlConstants.OLD_WALLET; const url = release.channel === 'beta' ? urlConstants.OLD_WALLET_BETA : urlConstants.OLD_WALLET;
return url; return url;
}; };
@ -12,7 +12,7 @@ const getOldWalletUrl = (device: ?TrezorDevice): string => {
// TODO: use uri template to build urls // TODO: use uri template to build urls
const getOldWalletReleaseUrl = (device: ?TrezorDevice): string => { const getOldWalletReleaseUrl = (device: ?TrezorDevice): string => {
if (!device || !device.firmwareRelease) return urlConstants.OLD_WALLET_BETA; if (!device || !device.firmwareRelease) return urlConstants.OLD_WALLET_BETA;
const release = device.firmwareRelease; const { release } = device.firmwareRelease;
const url = getOldWalletUrl(device); const url = getOldWalletUrl(device);
const version = release.version.join('.'); const version = release.version.join('.');
return `${url}?fw=${version}`; return `${url}?fw=${version}`;

@ -79,7 +79,6 @@ const LearnMoreText = styled.span`
const SelectWrapper = styled(Select)` const SelectWrapper = styled(Select)`
margin-right: 10px; margin-right: 10px;
width: 180px; width: 180px;
margin-bottom: 5px;
`; `;
const Download = styled.div` const Download = styled.div`
@ -117,21 +116,24 @@ const GoBack = styled.span`
class InstallBridge extends PureComponent<Props, State> { class InstallBridge extends PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const { transport } = props;
const packages = transport.bridge ? transport.bridge.packages : [];
const latestVersion = transport.bridge ? transport.bridge.version.join('.') : '';
const installers = props.transport.bridge.packages.map(p => ({ const installers = packages.map(p => ({
label: p.name, label: p.name,
value: p.url, value: p.url,
signature: p.signature, signature: p.signature,
preferred: p.preferred, preferred: !!p.preferred,
})); }));
const currentTarget: ?InstallTarget = installers.find(i => i.preferred === true); const currentTarget: ?InstallTarget = installers.find(i => i.preferred === true);
this.state = { this.state = {
currentVersion: currentVersion:
props.transport.type && props.transport.type === 'bridge' transport.type && transport.type === 'bridge'
? `Your version ${props.transport.version}` ? `Your version ${transport.version}`
: 'Not installed', : 'Not installed',
latestVersion: props.transport.bridge.version.join('.'), latestVersion,
installers, installers,
target: currentTarget || installers[0], target: currentTarget || installers[0],
uri: 'https://wallet.trezor.io/data/', uri: 'https://wallet.trezor.io/data/',

@ -46,8 +46,8 @@ class CoinMenu extends PureComponent<Props> {
getBaseUrl() { getBaseUrl() {
const { selectedDevice } = this.props.wallet; const { selectedDevice } = this.props.wallet;
let baseUrl = ''; let baseUrl = '';
if (selectedDevice && selectedDevice.features) { if (selectedDevice && selectedDevice.id) {
baseUrl = `/device/${selectedDevice.features.device_id}`; baseUrl = `/device/${selectedDevice.id}`;
if (selectedDevice.instance) { if (selectedDevice.instance) {
baseUrl += `:${selectedDevice.instance}`; baseUrl += `:${selectedDevice.instance}`;
} }

@ -68,6 +68,10 @@ class MenuItems extends PureComponent {
render() { render() {
if (!this.showDeviceMenu()) return null; if (!this.showDeviceMenu()) return null;
const { device } = this.props; const { device } = this.props;
const changeWalletButton =
device.features && device.features.passphrase_always_on_device
? l10nMessages.TR_CHANGE_PASSPHRASE
: l10nMessages.TR_CHANGE_WALLET_TYPE;
return ( return (
<Wrapper> <Wrapper>
{/* <Item onClick={() => { {/* <Item onClick={() => {
@ -92,7 +96,7 @@ class MenuItems extends PureComponent {
/> />
</IconWrapper> </IconWrapper>
<Label> <Label>
<FormattedMessage {...l10nMessages.TR_CHANGE_WALLET_TYPE} /> <FormattedMessage {...changeWalletButton} />
</Label> </Label>
</Item> </Item>
)} )}

@ -7,6 +7,10 @@ const definedMessages: Messages = defineMessages({
id: 'TR_CHANGE_WALLET_TYPE', id: 'TR_CHANGE_WALLET_TYPE',
defaultMessage: 'Change wallet type', defaultMessage: 'Change wallet type',
}, },
TR_CHANGE_PASSPHRASE: {
id: 'TR_CHANGE_PASSPHRASE',
defaultMessage: 'Change passphrase',
},
TR_RENEW_SESSION: { TR_RENEW_SESSION: {
id: 'TR_RENEW_SESSION', id: 'TR_RENEW_SESSION',
defaultMessage: 'Renew session', defaultMessage: 'Renew session',

@ -31,7 +31,7 @@ const PendingTransactions = (props: Props) => {
<NoTransactions>There are no pending transactions</NoTransactions> <NoTransactions>There are no pending transactions</NoTransactions>
)} )}
{pending.map(tx => ( {pending.map(tx => (
<Transaction key={tx.hash} network={props.network} tx={tx} /> <Transaction key={tx.txid} network={props.network} tx={tx} />
))} ))}
</Wrapper> </Wrapper>
); );

@ -521,14 +521,13 @@ const AccountSend = (props: Props) => {
</AdvancedForm> </AdvancedForm>
)} )}
{props.selectedAccount.pending.length > 0 || {props.selectedAccount.pending.length > 0 && (
(account.imported && ( <PendingTransactions
<PendingTransactions pending={props.selectedAccount.pending}
pending={props.selectedAccount.pending} tokens={props.selectedAccount.tokens}
tokens={props.selectedAccount.tokens} network={network}
network={network} />
/> )}
))}
</Content> </Content>
); );
}; };

@ -173,7 +173,7 @@ const AccountSummary = (props: Props) => {
{tokens.length < 1 && <AddTokenMessage />} {tokens.length < 1 && <AddTokenMessage />}
{tokens.map(token => ( {tokens.map(token => (
<AddedToken <AddedToken
key={token.symbol} key={token.address}
token={token} token={token}
pending={pending} pending={pending}
removeToken={props.removeToken} removeToken={props.removeToken}

@ -63,8 +63,8 @@ const StyledH4 = styled(H4)`
const getBaseUrl = device => { const getBaseUrl = device => {
let baseUrl = ''; let baseUrl = '';
if (device && device.features) { if (device && device.id) {
baseUrl = `/device/${device.features.device_id}`; baseUrl = `/device/${device.id}`;
if (device.instance) { if (device.instance) {
baseUrl += `:${device.instance}`; baseUrl += `:${device.instance}`;
} }

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save