From 9661c33931a1024808e4b4c4334696b56d3abaf1 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Fri, 6 Jul 2018 12:23:19 -0300 Subject: [PATCH 01/13] Detect and react to transport and connection events --- app/actions/ClientActions.js | 2 + app/actions/TrezorActions.js | 91 +++++++ app/config.js | 3 + app/index.js | 7 + app/main.development.js | 8 +- app/main_dev/externalRequests.js | 17 ++ app/reducers/index.js | 2 + app/reducers/trezor.js | 37 +++ app/wallet/daemon.js | 4 + package.json | 1 + yarn.lock | 447 ++++++++++++++++++++++++++++++- 11 files changed, 614 insertions(+), 5 deletions(-) create mode 100644 app/actions/TrezorActions.js create mode 100644 app/reducers/trezor.js diff --git a/app/actions/ClientActions.js b/app/actions/ClientActions.js index ed423cec31..0c490b446a 100644 --- a/app/actions/ClientActions.js +++ b/app/actions/ClientActions.js @@ -17,6 +17,7 @@ import { getVettedProposals } from "./GovernanceActions"; import { rawHashToHex } from "../helpers/byteActions"; import * as da from "../middleware/dcrdataapi"; import { EXTERNALREQUEST_DCRDATA, EXTERNALREQUEST_POLITEIA } from "main_dev/externalRequests"; +import * as trezorActions from "./TrezorActions"; export const goToTransactionHistory = () => (dispatch) => { dispatch(pushHistory("/transactions/history")); @@ -60,6 +61,7 @@ const startWalletServicesTrigger = () => (dispatch, getState) => new Promise((re await dispatch(getStartupWalletInfo()); await dispatch(transactionNtfnsStart()); await dispatch(accountNtfnsStart()); + await dispatch(trezorActions.loadDeviceList()); await dispatch(pushHistory("/home")); resolve(); diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js new file mode 100644 index 0000000000..c6cdf25a4a --- /dev/null +++ b/app/actions/TrezorActions.js @@ -0,0 +1,91 @@ +import * as trezorjs from "trezor.js"; +import trezorTransports from "trezor-link"; +import * as wallet from "wallet"; +import { EXTERNALREQUEST_TREZOR_BRIDGE } from "main_dev/externalRequests"; + +export const TRZ_LOADDEVICELIST_ATTEMPT = "TRZ_LOADDEVICELIST_ATTEMPT"; +export const TRZ_LOADDEVICELIST_FAILED = "TRZ_LOADDEVICELIST_FAILED"; +export const TRZ_LOADDEVICELIST_SUCCESS = "TRZ_LOADDEVICELIST_SUCCESS"; +export const TRZ_DEVICELISTTRANSPORT_LOST = "TRZ_DEVICELISTTRANSPORT_LOST"; +export const TRZ_SELECTEDDEVICE_CHANGED = "TRZ_SELECTEDDEVICE_CHANGED"; + +export const loadDeviceList = () => (dispatch, getState) => { + return new Promise((resolve, reject) => { + if (!getState().trezor.enabled) return; + wallet.allowExternalRequest(EXTERNALREQUEST_TREZOR_BRIDGE); + + dispatch({ type: TRZ_LOADDEVICELIST_ATTEMPT }); + const debug = getState().trezor.debug; + + // TODO: decide whether we want to provide our own config blob. + const configUrl = "https://wallet.trezor.io/data/config_signed.bin?" + + Date.now(); + + const opts = { debug, debugInfo: debug, configUrl, + transport: new trezorTransports.BridgeV2() }; + const devList = new trezorjs.DeviceList(opts); + let resolvedTransport = false; + + devList.on("transport", t => { + console.log("transport", t); + if (resolvedTransport) return; + resolvedTransport = true; // resolved with success + dispatch({ deviceList: devList, type: TRZ_LOADDEVICELIST_SUCCESS }); + resolve(t); + }); + + devList.on("error", err => { + console.log("error", err); + if (!resolvedTransport && err.message.includes("ECONNREFUSED")) { + resolvedTransport = true; // resolved with failure + dispatch({ error: err.message, type: TRZ_LOADDEVICELIST_FAILED }); + reject(err); + } else if (err.message.includes("socket hang up")) { + // this might happen any time throughout the app lifetime if the bridge + // service is shutdown for any reason + dispatch({ error: err.message, type: TRZ_DEVICELISTTRANSPORT_LOST }); + } + }); + + devList.on("connect", device => { + console.log("connect", Object.keys(devList.devices), device); + const currentDevice = getState().trezor.device; + if (!currentDevice) { + // first device connected. Use it. + dispatch({ device, type: TRZ_SELECTEDDEVICE_CHANGED }); + } + }); + + devList.on("disconnect", device => { + console.log("disconnect", Object.keys(devList.devices), device); + const currentDevice = getState().trezor.device; + if (currentDevice && device.originalDescriptor.path === currentDevice.originalDescriptor.path ) { + const devicePaths = Object.keys(devList.devices); + + // we were using the device that was just disconnected. Pick a new + // device to use. + if (devicePaths.length === 0) { + // no more devices left to use + dispatch({ device: null, type: TRZ_SELECTEDDEVICE_CHANGED }); + } else { + dispatch({ device: devList.devices[devicePaths[0]], type: TRZ_SELECTEDDEVICE_CHANGED }); + } + } + }); + + devList.on("connectUnacquired", device => { + console.log("connect unacquired", device); + }); + + devList.on("disconnectUnacquired", device => { + console.log("disconnect unacquired", device); + }); + + }); +}; + +export const selectDevice = (path) => async (dispatch, getState) => { + const devList = getState().trezor.deviceList; + if (!devList.devices[path]) return; + dispatch({ device: devList.devices[path], type: TRZ_SELECTEDDEVICE_CHANGED }); +}; diff --git a/app/config.js b/app/config.js index 54f171a33b..e4a19a699e 100644 --- a/app/config.js +++ b/app/config.js @@ -44,6 +44,9 @@ export function initWalletCfg(testnet, walletPath) { if (!config.has("politeia_last_access_block")) { config.set("politeia_last_access_block", 0); } + if (!config.has("trezor")) { + config.set("trezor", false); + } stakePoolInfo(function(foundStakePoolConfigs) { if (foundStakePoolConfigs !== null) { updateStakePoolConfig(config, foundStakePoolConfigs); diff --git a/app/index.js b/app/index.js index 930277d257..98780f3971 100644 --- a/app/index.js +++ b/app/index.js @@ -385,6 +385,13 @@ var initialState = { proposals: {}, // map from proposal token (id) to proposal details lastVettedFetchTime: new Date(0), // time when vetted proposals were requested }, + trezor: { + enabled: true, + debug: true, + deviceList: null, + transportError: false, + device: null, + }, locales: locales }; diff --git a/app/main.development.js b/app/main.development.js index 3ed2ffe727..d485465393 100644 --- a/app/main.development.js +++ b/app/main.development.js @@ -7,7 +7,7 @@ import { createLogger, lastLogLine, GetDcrdLogs, GetDcrwalletLogs } from "./main import { OPTIONS, USAGE_MESSAGE, VERSION_MESSAGE, BOTH_CONNECTION_ERR_MESSAGE, MAX_LOG_LENGTH } from "./main_dev/constants"; import { getWalletsDirectoryPath, getWalletsDirectoryPathNetwork, appDataDirectory } from "./main_dev/paths"; import { getGlobalCfgPath, checkAndInitWalletCfg } from "./main_dev/paths"; -import { installSessionHandlers, reloadAllowedExternalRequests, allowStakepoolRequests } from "./main_dev/externalRequests"; +import { installSessionHandlers, reloadAllowedExternalRequests, allowStakepoolRequests, allowExternalRequest } from "./main_dev/externalRequests"; import { setupProxy } from "./main_dev/proxy"; import { cleanShutdown, GetDcrdPID, GetDcrwPID } from "./main_dev/launch"; import { getAvailableWallets, startDaemon, createWallet, removeWallet, stopDaemon, stopWallet, startWallet, checkDaemon, deleteDaemon, setWatchingOnlyWallet, getWatchingOnlyWallet, getDaemonInfo } from "./main_dev/ipc"; @@ -114,11 +114,17 @@ ipcMain.on("reload-allowed-external-request", (event) => { reloadAllowedExternalRequests(); event.returnValue = true; }); + ipcMain.on("allow-stakepool-host", (event, host) => { allowStakepoolRequests(host); event.returnValue = true; }); +ipcMain.on("allow-external-request", (event, requestType) => { + allowExternalRequest(requestType); + event.returnValue = true; +}); + ipcMain.on("setup-proxy", () => { setupProxy(logger); }); diff --git a/app/main_dev/externalRequests.js b/app/main_dev/externalRequests.js index 6299efdf5f..3132970abf 100644 --- a/app/main_dev/externalRequests.js +++ b/app/main_dev/externalRequests.js @@ -16,6 +16,7 @@ export const EXTERNALREQUEST_STAKEPOOL_LISTING = "EXTERNALREQUEST_STAKEPOOL_LIST export const EXTERNALREQUEST_UPDATE_CHECK = "EXTERNALREQUEST_UPDATE_CHECK"; export const EXTERNALREQUEST_POLITEIA = "EXTERNALREQUEST_POLITEIA"; export const EXTERNALREQUEST_DCRDATA = "EXTERNALREQUEST_DCRDATA"; +export const EXTERNALREQUEST_TREZOR_BRIDGE = "EXTERNALREQUEST_TREZOR_BRIDGE"; // These are the requests allowed when the standard privacy mode is selected. export const STANDARD_EXTERNAL_REQUESTS = [ @@ -67,6 +68,11 @@ export const installSessionHandlers = (mainLogger) => { callback({ cancel: true, requestHeaders: details.requestHeaders }); } else { logger.log("verbose", details.method + " " + details.url); + if (allowedExternalRequests[EXTERNALREQUEST_TREZOR_BRIDGE] && /^http:\/\/127.0.0.1:21325\//.test(details.url)) { + // trezor bridge requires this as an origin to prevent unwanted access. + details.requestHeaders["Origin"] = "https://dummy-origin-to-fool-trezor-bridge.trezor.io"; + } + callback({ cancel: false, requestHeaders: details.requestHeaders }); } }); @@ -98,6 +104,17 @@ export const allowExternalRequest = (externalReqType) => { case EXTERNALREQUEST_DCRDATA: addAllowedURL(DCRDATA_URL_TESTNET); addAllowedURL(DCRDATA_URL_MAINNET); + break; + case EXTERNALREQUEST_TREZOR_BRIDGE: + addAllowedURL(/^http:\/\/127.0.0.1:21324\//); + addAllowedURL(/^http:\/\/127.0.0.1:21325\//); + + // TODO: decide whether we want to provide our own signed config + addAllowedURL(/^https:\/\/wallet.trezor.io\/data\/config_signed.bin\?[\d]+$/); + + // TODO: decide if we wanna block this + addAllowedURL(/^https:\/\/wallet.trezor.io\/data\/bridge\/latest.txt\?[\d]+$/); + break; default: logger.log("error", "Unknown external request type: " + externalReqType); diff --git a/app/reducers/index.js b/app/reducers/index.js index 3c58ca0ebe..b124d85f9b 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -14,6 +14,7 @@ import sidebar from "./sidebar"; import snackbar from "./snackbar"; import statistics from "./statistics"; import governance from "./governance"; +import trezor from "./trezor"; const rootReducer = combineReducers({ grpc, @@ -30,6 +31,7 @@ const rootReducer = combineReducers({ snackbar, statistics, governance, + trezor, }); export default rootReducer; diff --git a/app/reducers/trezor.js b/app/reducers/trezor.js new file mode 100644 index 0000000000..5efc04a07b --- /dev/null +++ b/app/reducers/trezor.js @@ -0,0 +1,37 @@ +import { + TRZ_LOADDEVICELIST_ATTEMPT, TRZ_LOADDEVICELIST_FAILED, TRZ_LOADDEVICELIST_SUCCESS, + TRZ_DEVICELISTTRANSPORT_LOST, + TRZ_SELECTEDDEVICE_CHANGED, +} from "actions/TrezorActions"; + +export default function trezor(state = {}, action) { + switch (action.type) { + case TRZ_LOADDEVICELIST_ATTEMPT: + return { ...state, + deviceList: null, + transportError: false, + device: null, + }; + case TRZ_LOADDEVICELIST_SUCCESS: + return { ...state, + deviceList: action.deviceList, + transportError: false, + }; + case TRZ_LOADDEVICELIST_FAILED: + return { ...state, + transportError: action.error, + }; + case TRZ_DEVICELISTTRANSPORT_LOST: + return { ...state, + deviceList: null, + transportError: action.error, + device: null, + }; + case TRZ_SELECTEDDEVICE_CHANGED: + return { ...state, + device: action.device, + }; + default: + return state; + } +} diff --git a/app/wallet/daemon.js b/app/wallet/daemon.js index 57092ff787..4bd8e83d45 100644 --- a/app/wallet/daemon.js +++ b/app/wallet/daemon.js @@ -137,6 +137,10 @@ export const reloadAllowedExternalRequests = log(() => Promise .resolve(ipcRenderer.sendSync("reload-allowed-external-request")) , "Reload allowed external request"); +export const allowExternalRequest = log(requestType => Promise + .resolve(ipcRenderer.sendSync("allow-external-request", requestType)) + , "Allow External Request"); + export const allowStakePoolHost = log(host => Promise .resolve(ipcRenderer.sendSync("allow-stakepool-host", host)) , "Allow StakePool Host"); diff --git a/package.json b/package.json index e2ddbb2ce0..e71a38e191 100644 --- a/package.json +++ b/package.json @@ -234,6 +234,7 @@ "string-argv": "0.1.1", "stylelint": "^9.7.0", "timezone-mock": "^1.0.2", + "trezor.js": "^6.17.6", "winston": "^2.3.1" }, "devEngines": { diff --git a/yarn.lock b/yarn.lock index 5838d41b5c..1a0349d93b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1038,11 +1038,21 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.11.0.tgz#ddd0d67a3b6c3810dd1a59e36675fa82de5e19ae" integrity sha512-R4Dvw6KjSYn/SpvjRchBOwXr14vVVcFXCtnM3f0aLvlJS8a599rrcEoihcP2/+Z/f75E5GNPd4aWM7j1yei9og== +"@types/node@^10.11.7": + version "10.12.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.10.tgz#4fa76e6598b7de3f0cb6ec3abacc4f59e5b3a2ce" + integrity sha512-8xZEYckCbUVgK8Eg7lf5Iy4COKJ5uXlnIOnePN0WUwSQggy9tolM+tDJf7wMOnT/JT/W9xDYIaYggt3mRV2O5w== + "@types/node@^8.0.24": version "8.10.30" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.30.tgz#2c82cbed5f79d72280c131d2acffa88fbd8dd353" integrity sha512-Le8HGMI5gjFSBqcCuKP/wfHC19oURzkU2D+ERIescUoJd+CmNEMYBib9LQ4zj1HHEZOJQWhw2ZTnbD8weASh/Q== +"@types/semver@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" + integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== + "@webassemblyjs/ast@1.7.10": version "1.7.10" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.10.tgz#0cfc61d61286240b72fc522cb755613699eea40a" @@ -1600,6 +1610,14 @@ asap@~2.0.3: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= +ascli@~0.3: + version "0.3.0" + resolved "https://registry.yarnpkg.com/ascli/-/ascli-0.3.0.tgz#5e66230e5219fe3e8952a4efb4f20fae596a813a" + integrity sha1-XmYjDlIZ/j6JUqTvtPIPrllqgTo= + dependencies: + colour latest + optjs latest + asn1.js@^4.0.0: version "4.10.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" @@ -1971,6 +1989,13 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base-x@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.5.tgz#d3ada59afed05b921ab581ec3112e6444ba0795a" + integrity sha512-C3picSgzPSLE+jW3tcBzJoGwitOtazb5B+5YmAxZm2ybmTi9LNgAtDO/jjVEBZwHoXmDBZ9m/IELj3elJVRBcA== + dependencies: + safe-buffer "^5.0.1" + base64-js@^1.0.2, base64-js@^1.2.3: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" @@ -1989,6 +2014,14 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +bchaddrjs@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/bchaddrjs/-/bchaddrjs-0.2.3.tgz#c3008ac85d7c7e583e83a30604c427b5bed26c68" + integrity sha512-0DVW8q3UFQFhrvt8Fowpkk+WvkYTZTSD1vGCQHrtMHZjRL6G/SoW0mgrREmgO1F/8TJ+Julri4UBWA8Gr7C5Yw== + dependencies: + bs58check "^2.1.2" + cashaddrjs "^0.2.9" + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -1996,16 +2029,106 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bech32@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.3.tgz#bd47a8986bbb3eec34a56a097a84b8d3e9a2dfcd" + integrity sha512-yuVFUvrNcoJi0sv5phmqc6P+Fl1HjRDRNOOkHY2X/3LBy2bIGNSFx4fZ95HMaXHupuS7cZR15AsvtmCIF4UEyg== + +big-integer@^1.3.19, big-integer@^1.6.34: + version "1.6.36" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.36.tgz#78631076265d4ae3555c04f85e7d9d2f3a071a36" + integrity sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg== + big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== +bigi@0.2.0, bigi@0.2.x: + version "0.2.0" + resolved "https://registry.yarnpkg.com/bigi/-/bigi-0.2.0.tgz#8bee26348b99c4ae2ed20481fb12384c32792f74" + integrity sha1-i+4mNIuZxK4u0gSB+xI4TDJ5L3Q= + +bigi@^1.1.0, bigi@^1.2.1, bigi@^1.4.0, bigi@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825" + integrity sha1-nGZalfiLiwj8Bc/XMfVhhZ1yWCU= + binary-extensions@^1.0.0: version "1.12.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" integrity sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg== +binstring@0.2.x, binstring@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/binstring/-/binstring-0.2.1.tgz#8a174d301f6d54efda550dd98bb4cb524eacd75d" + integrity sha1-ihdNMB9tVO/aVQ3Zi7TLUk6s110= + +bip66@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" + integrity sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI= + dependencies: + safe-buffer "^5.0.1" + +bitcoin-ops@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278" + integrity sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow== + +bitcoin-script@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/bitcoin-script/-/bitcoin-script-0.1.1.tgz#52c504dddc1e3b1317a7b6567a88981b3ef3929c" + integrity sha1-UsUE3dweOxMXp7ZWeoiYGz7zkpw= + dependencies: + big-integer "^1.3.19" + bigi "^1.2.1" + coinkey "^0.1.0" + ecdsa "^0.6.0" + js-beautify "^1.5.4" + ripemd160 "^0.2.0" + secure-random "^1.1.1" + sha1 "^1.1.0" + sha256 "^0.1.1" + +bitcoinjs-lib-zcash@^3.5.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/bitcoinjs-lib-zcash/-/bitcoinjs-lib-zcash-3.5.3.tgz#19c74f36a2f9b0bd6680a02087567db8454639e5" + integrity sha512-F2QfhMV/M+8ffWlm01LTQFn6r9NxhqsuQDWC/BNrsbOZMZMrVQvBI+AjH5u+qkSOBl3DubVSXluIZVSKcELA6A== + dependencies: + bech32 "^1.1.2" + bigi "^1.4.0" + bip66 "^1.1.0" + bitcoin-ops "^1.3.0" + bitcoin-script "^0.1.1" + blake2b "^2.1.2" + bs58check "^2.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.3" + ecurve "^1.0.0" + merkle-lib "^2.0.10" + pushdata-bitcoin "^1.0.1" + randombytes "^2.0.1" + safe-buffer "^5.0.1" + typeforce "^1.11.3" + varuint-bitcoin "^1.0.4" + wif "^2.0.1" + +blake2b-wasm@^1.1.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/blake2b-wasm/-/blake2b-wasm-1.1.7.tgz#e4d075da10068e5d4c3ec1fb9accc4d186c55d81" + integrity sha512-oFIHvXhlz/DUgF0kq5B1CqxIDjIJwh9iDeUUGQUcvgiGz7Wdw03McEO7CfLBy7QKGdsydcMCgO9jFNBAFCtFcA== + dependencies: + nanoassert "^1.0.0" + +blake2b@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/blake2b/-/blake2b-2.1.3.tgz#f5388be424768e7c6327025dad0c3c6d83351bca" + integrity sha512-pkDss4xFVbMb4270aCyGD3qLv92314Et+FsKzilCLxDz5DuZ2/1g3w4nmBbu6nKApPspnjG7JcwTjGZnduB1yg== + dependencies: + blake2b-wasm "^1.1.0" + nanoassert "^1.0.0" + block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -2189,6 +2312,30 @@ browserslist@^4.1.0: electron-to-chromium "^1.3.62" node-releases "^1.0.0-alpha.11" +bs58@0.3.x: + version "0.3.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-0.3.0.tgz#cb48107bf446727d3e17b21102da73ca89109588" + integrity sha1-y0gQe/RGcn0+F7IRAtpzyokQlYg= + dependencies: + bigi "0.2.0" + binstring "~0.2.0" + +bs58@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha1-vhYedsNU9veIrkBx9j806MTwpCo= + dependencies: + base-x "^3.0.2" + +bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" + integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== + dependencies: + bs58 "^4.0.0" + create-hash "^1.1.0" + safe-buffer "^5.1.2" + bser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" @@ -2233,6 +2380,11 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" +bufferview@~1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bufferview/-/bufferview-1.0.1.tgz#7afd74a45f937fa422a1d338c08bbfdc76cd725d" + integrity sha1-ev10pF+Tf6QiodM4wIu/3HbNcl0= + builder-util-runtime@6.1.0, builder-util-runtime@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-6.1.0.tgz#85f0c7bddbe4950ad708a1455b6ba79e16f2b731" @@ -2273,6 +2425,14 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +bytebuffer-old-fixed-webpack@3.5.6: + version "3.5.6" + resolved "https://registry.yarnpkg.com/bytebuffer-old-fixed-webpack/-/bytebuffer-old-fixed-webpack-3.5.6.tgz#5adc419c6a9b4692f217206703ec7431c759aa3f" + integrity sha1-WtxBnGqbRpLyFyBnA+x0McdZqj8= + dependencies: + bufferview "~1" + long "~2 >=2.2.3" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -2391,6 +2551,13 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +cashaddrjs@^0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/cashaddrjs/-/cashaddrjs-0.2.9.tgz#e38e323e11e0ab761767006b3938a36278dadec9" + integrity sha512-DhJF4iAH0/RM3UjHDHKRxzs09YGL9px+oTyizzydanhC7jTyM2aJ+aLKA96vZGTTWayvvr2iDF2l13lpqXiRFg== + dependencies: + big-integer "^1.6.34" + ccount@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.3.tgz#f1cec43f332e2ea5a569fd46f9f5bde4e6102aff" @@ -2450,6 +2617,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +"charenc@>= 0.0.1": + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + cheerio@^1.0.0-rc.2: version "1.0.0-rc.2" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" @@ -2593,6 +2765,23 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +coinkey@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/coinkey/-/coinkey-0.1.0.tgz#bdf2a953dcfe4fd70fdba3000c787ff369d8294c" + integrity sha1-vfKpU9z+T9cP26MADHh/82nYKUw= + dependencies: + coinstring "~0.2.0" + eckey "~0.4.0" + secure-random "~0.2.0" + +coinstring@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/coinstring/-/coinstring-0.2.0.tgz#fa2820497bb9e35b7cfa116f048219ca6f3f348f" + integrity sha1-+iggSXu541t8+hFvBIIZym8/NI8= + dependencies: + bs58 "0.3.x" + crypto-hashing "~0.3.0" + collapse-white-space@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.4.tgz#ce05cf49e54c3277ae573036a26851ba430a0091" @@ -2638,6 +2827,11 @@ colors@1.0.x: resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= +colour@latest: + version "0.7.1" + resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" + integrity sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g= + combined-stream@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" @@ -2662,6 +2856,11 @@ commander@^2.15.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" integrity sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ== +commander@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" @@ -2712,6 +2911,14 @@ concurrently@^4.0.1: tree-kill "^1.1.0" yargs "^12.0.1" +config-chain@~1.1.5: + version "1.1.12" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" + integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + configstore@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" @@ -2751,6 +2958,11 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +convert-hex@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/convert-hex/-/convert-hex-0.1.0.tgz#08c04568922c27776b8a2e81a95d393362ea0b65" + integrity sha1-CMBFaJIsJ3drii6BqV05M2LqC2U= + convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.1: version "1.6.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" @@ -2758,6 +2970,11 @@ convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.1: dependencies: safe-buffer "~5.1.1" +convert-string@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/convert-string/-/convert-string-0.1.0.tgz#79ce41a9bb0d03bcf72cdc6a8f3c56fbbc64410a" + integrity sha1-ec5BqbsNA7z3LNxqjzxW+7xkQQo= + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -2840,7 +3057,7 @@ create-hash@^1.1.0, create-hash@^1.1.2: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.3, create-hmac@^1.1.4: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -2894,6 +3111,11 @@ cross-unzip@0.0.2: resolved "https://registry.yarnpkg.com/cross-unzip/-/cross-unzip-0.0.2.tgz#5183bc47a09559befcf98cc4657964999359372f" integrity sha1-UYO8R6CVWb78+YzEZXlkmZNZNy8= +"crypt@>= 0.0.1": + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -2918,6 +3140,14 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" +crypto-hashing@~0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/crypto-hashing/-/crypto-hashing-0.3.1.tgz#0195548db8bdef50aa9d526514cc546e1e62fbce" + integrity sha1-AZVUjbi971CqnVJlFMxUbh5i+84= + dependencies: + binstring "0.2.x" + ripemd160 "~0.2.0" + crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" @@ -3436,6 +3666,58 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecdsa@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/ecdsa/-/ecdsa-0.6.0.tgz#35e9887b6f418ec7b98380170334dc2763a6b317" + integrity sha1-NemIe29Bjse5g4AXAzTcJ2Omsxc= + dependencies: + bigi "^1.2.1" + ecurve "^1.0.0" + +eckey@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/eckey/-/eckey-0.4.2.tgz#cea53b7d529e42168f2c8597a7e8d32bc9e39436" + integrity sha1-zqU7fVKeQhaPLIWXp+jTK8njlDY= + dependencies: + bigi "0.2.x" + ecurve "~0.3.0" + ecurve-names "~0.3.0" + +ecurve-names@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/ecurve-names/-/ecurve-names-0.3.0.tgz#f9525e403f44a35f7bc17557ff7e41091931d59c" + integrity sha1-+VJeQD9Eo197wXVX/35BCRkx1Zw= + dependencies: + bigi "0.2.x" + ecurve "~0.3.0" + +ecurve@^1.0.0, ecurve@^1.0.2, ecurve@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/ecurve/-/ecurve-1.0.6.tgz#dfdabbb7149f8d8b78816be5a7d5b83fcf6de797" + integrity sha512-/BzEjNfiSuB7jIWKcS/z8FK9jNjmEWvUV2YZ4RLSmcDtP7Lq0m6FvDuSnJpBlDpGRpfRQeTLGLBI8H+kEv0r+w== + dependencies: + bigi "^1.1.0" + safe-buffer "^5.0.1" + +ecurve@~0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ecurve/-/ecurve-0.3.2.tgz#badeff9ef95399eea2e17d1b533f010484240b50" + integrity sha1-ut7/nvlTme6i4X0bUz8BBIQkC1A= + dependencies: + bigi "0.2.x" + +editorconfig@^0.15.0: + version "0.15.2" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.2.tgz#047be983abb9ab3c2eefe5199cb2b7c5689f0702" + integrity sha512-GWjSI19PVJAM9IZRGOS+YKI8LN+/sjkSjNyvxL5ucqP9/IqtYNXBaQ/6c/hkPNYQHyOHra2KoXZI/JVpuqwmcQ== + dependencies: + "@types/node" "^10.11.7" + "@types/semver" "^5.5.0" + commander "^2.19.0" + lru-cache "^4.1.3" + semver "^5.6.0" + sigmund "^1.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -5982,6 +6264,16 @@ jest@^23.6.0: import-local "^1.0.0" jest-cli "^23.6.0" +js-beautify@^1.5.4: + version "1.8.8" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.8.8.tgz#1eb175b73a3571a5f1ed8d98e7cf2b05bfa98471" + integrity sha512-qVNq7ZZ7ZbLdzorvSlRDadS0Rh5oyItaE95v6I4wbbuSiijxn7SnnsV6dvKlcXuO2jX7lK8tn9fBulx34K/Ejg== + dependencies: + config-chain "~1.1.5" + editorconfig "^0.15.0" + mkdirp "~0.5.0" + nopt "~4.0.1" + js-levenshtein@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.3.tgz#3ef627df48ec8cf24bacf05c0f184ff30ef413c5" @@ -6366,6 +6658,11 @@ lolex@^2.3.2, lolex@^2.7.4: resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.5.tgz#113001d56bfc7e02d56e36291cc5c413d1aa0733" integrity sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q== +"long@~2 >=2.2.3": + version "2.4.0" + resolved "https://registry.yarnpkg.com/long/-/long-2.4.0.tgz#9fa180bb1d9500cdc29c4156766a1995e1f4524f" + integrity sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8= + longest-streak@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.2.tgz#2421b6ba939a443bb9ffebf596585a50b4c38e2e" @@ -6404,6 +6701,14 @@ lru-cache@^4.0.1, lru-cache@^4.1.1: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@^4.1.3: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -6576,6 +6881,11 @@ merge@^1.2.0: resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" integrity sha1-dTHjnUlJwoGma4xabgJl6LBYlNo= +merkle-lib@^2.0.10: + version "2.0.10" + resolved "https://registry.yarnpkg.com/merkle-lib/-/merkle-lib-2.0.10.tgz#82b8dbae75e27a7785388b73f9d7725d0f6f3326" + integrity sha1-grjbrnXieneFOItz+ddyXQ9vMyY= + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -6805,6 +7115,11 @@ nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.0.tgz#574e360e4d954ab16966ec102c0c049fd961a099" integrity sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw== +nanoassert@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-1.1.0.tgz#4f3152e09540fde28c76f44b19bbcd1d5a42478d" + integrity sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40= + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -6891,7 +7206,7 @@ node-addon-loader@decred/node-addon-loader#master: dependencies: loader-utils "^1.1.0" -node-fetch@^1.0.1: +node-fetch@^1.0.1, node-fetch@^1.6.0: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== @@ -7034,7 +7349,7 @@ nomnom@~1.6.2: dependencies: abbrev "1" -nopt@^4.0.1: +nopt@^4.0.1, nopt@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= @@ -7233,7 +7548,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.0.4: +object.values@^1.0.3, object.values@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a" integrity sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo= @@ -7284,6 +7599,11 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" +optjs@latest: + version "3.2.2" + resolved "https://registry.yarnpkg.com/optjs/-/optjs-3.2.2.tgz#69a6ce89c442a44403141ad2f9b370bd5bb6f4ee" + integrity sha1-aabOicRCpEQDFBrS+bNwvVu29O4= + os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" @@ -7935,6 +8255,19 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, loose-envify "^1.3.1" object-assign "^4.1.1" +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= + +protobufjs-old-fixed-webpack@3.8.5: + version "3.8.5" + resolved "https://registry.yarnpkg.com/protobufjs-old-fixed-webpack/-/protobufjs-old-fixed-webpack-3.8.5.tgz#5813c1af9f1d136bbf39f4f9f2e6f3e43c389d06" + integrity sha1-WBPBr58dE2u/OfT58ubz5Dw4nQY= + dependencies: + ascli "~0.3" + bytebuffer-old-fixed-webpack "3.5.6" + proxy-addr@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" @@ -8001,6 +8334,13 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +pushdata-bitcoin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz#15931d3cd967ade52206f523aa7331aef7d43af7" + integrity sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc= + dependencies: + bitcoin-ops "^1.3.0" + qr-image@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/qr-image/-/qr-image-3.2.0.tgz#9fa8295beae50c4a149cf9f909a1db464a8672e8" @@ -8917,6 +9257,11 @@ rimraf@~2.4.0: dependencies: glob "^6.0.1" +ripemd160@^0.2.0, ripemd160@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-0.2.1.tgz#dee19248a3e1c815ff9aea39e753a337f56a243d" + integrity sha1-3uGSSKPhyBX/muo551OjN/VqJD0= + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -9043,6 +9388,21 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" +secure-random@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/secure-random/-/secure-random-1.1.1.tgz#0880f2d8c5185f4bcb4684058c836b4ddb07145a" + integrity sha1-CIDy2MUYX0vLRoQFjINrTdsHFFo= + +secure-random@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/secure-random/-/secure-random-0.2.1.tgz#1c2f08cb94d8c06deff52721a6045bba96f85a9a" + integrity sha1-HC8Iy5TYwG3v9SchpgRbupb4Wpo= + +semver-compare@1.0.0, semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -9142,6 +9502,22 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +sha1@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/sha1/-/sha1-1.1.1.tgz#addaa7a93168f393f19eb2b15091618e2700f848" + integrity sha1-rdqnqTFo85PxnrKxUJFhjicA+Eg= + dependencies: + charenc ">= 0.0.1" + crypt ">= 0.0.1" + +sha256@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/sha256/-/sha256-0.1.1.tgz#34296f90498da3e8c6b06fffe8e860dba299f902" + integrity sha1-NClvkEmNo+jGsG//6Ohg26KZ+QI= + dependencies: + convert-hex "~0.1.0" + convert-string "~0.1.0" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -9159,6 +9535,11 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +sigmund@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -9925,6 +10306,35 @@ tree-kill@^1.1.0: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" integrity sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg== +trezor-link@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/trezor-link/-/trezor-link-1.6.0.tgz#d79fec3e6ecf57c74c11ffbe37ebbd9118a91826" + integrity sha512-wJnd6pUn2WYPyoqqGpPTyixUBvwKPwtq+CZ+uQFL03ttFxHzYzCCgy2cDe8gievY1S2L2MDQNX3+GovhFOzxjg== + dependencies: + bigi "^1.4.1" + ecurve "^1.0.3" + json-stable-stringify "^1.0.1" + node-fetch "^1.6.0" + object.values "^1.0.3" + protobufjs-old-fixed-webpack "3.8.5" + semver-compare "^1.0.0" + whatwg-fetch "0.11.0" + +trezor.js@^6.17.6: + version "6.19.0" + resolved "https://registry.yarnpkg.com/trezor.js/-/trezor.js-6.19.0.tgz#f71b0ca7c64acc631dcf3c6d34d59a4b4dc19369" + integrity sha512-LmsqUWya1psbON2s4ym8CXGVpASIsCXTIS8v1LTUKMXlNQ1uJnFsqt8FadwQ2Q1pX3TYyZS1kHYntT8+Zfe+wg== + dependencies: + bchaddrjs "^0.2.1" + bitcoinjs-lib-zcash "^3.5.2" + ecurve "^1.0.2" + node-fetch "^1.6.0" + randombytes "^2.0.1" + semver-compare "1.0.0" + trezor-link "1.6.0" + unorm "^1.3.3" + whatwg-fetch "0.11.0" + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -10009,6 +10419,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typeforce@^1.11.3: + version "1.16.0" + resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.16.0.tgz#060f871420f4ed90d411e0606bebc62a0889ad55" + integrity sha512-V60F7OHPH7vPlgIU73vYyeebKxWjQqCTlge+MvKlVn09PIhCOi/ZotowYdgREHB5S1dyHOr906ui6NheYXjlVQ== + ua-parser-js@^0.7.18: version "0.7.18" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" @@ -10186,6 +10601,11 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +unorm@^1.3.3: + version "1.4.1" + resolved "https://registry.yarnpkg.com/unorm/-/unorm-1.4.1.tgz#364200d5f13646ca8bcd44490271335614792300" + integrity sha1-NkIA1fE2RsqLzURJAnEzVhR5IwA= + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -10341,6 +10761,13 @@ value-equal@^0.4.0: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw== +varuint-bitcoin@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/varuint-bitcoin/-/varuint-bitcoin-1.1.0.tgz#7a343f50537607af6a3059312b9782a170894540" + integrity sha512-jCEPG+COU/1Rp84neKTyDJQr478/hAfVp5xxYn09QEH0yBjbmPeMfuuQIrp+BUD83hybtYZKhr5elV3bvdV1bA== + dependencies: + safe-buffer "^5.1.1" + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -10530,6 +10957,11 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: dependencies: iconv-lite "0.4.23" +whatwg-fetch@0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.11.0.tgz#46b1d18d0aa99955971ef1a2f5aac506add28815" + integrity sha1-RrHRjQqpmVWXHvGi9arFBq3SiBU= + whatwg-fetch@>=0.10.0: version "3.0.0" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" @@ -10584,6 +11016,13 @@ widest-line@^2.0.0: dependencies: string-width "^2.1.1" +wif@^2.0.1: + version "2.0.6" + resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" + integrity sha1-CNP1IFbGZnkplyb63g1DKudLRwQ= + dependencies: + bs58check "<3.0.0" + winston@^2.3.1: version "2.4.4" resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.4.tgz#a01e4d1d0a103cf4eada6fc1f886b3110d71c34b" From 5d1f60fbc10c5919e7f97f6ce5a50896cafd507d Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Fri, 6 Jul 2018 16:41:49 -0300 Subject: [PATCH 02/13] Trezor sign tx support --- app/actions/TrezorActions.js | 184 +++++++++++++++++- .../buttons/SendTransactionButton.js | 48 +++-- app/connectors/send.js | 4 + app/selectors.js | 10 +- app/wallet/constants.js | 8 +- 5 files changed, 232 insertions(+), 22 deletions(-) diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index c6cdf25a4a..24ae084b02 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -1,7 +1,30 @@ import * as trezorjs from "trezor.js"; import trezorTransports from "trezor-link"; import * as wallet from "wallet"; +import * as selectors from "selectors"; +import { sprintf } from "sprintf-js"; +import { rawHashToHex, rawToHex, hexToRaw } from "helpers"; +import { publishTransactionAttempt } from "./ControlActions"; + import { EXTERNALREQUEST_TREZOR_BRIDGE } from "main_dev/externalRequests"; +import { SIGNTX_ATTEMPT, SIGNTX_FAILED, SIGNTX_SUCCESS } from "./ControlActions"; + +const hardeningConstant = 0x80000000; + +// Right now (2018-07-06) dcrwallet only supports a single account on watch only +// wallets. Therefore we are limited to using this single account when signing +// transactions via trezor. +const WALLET_ACCOUNT = 0; + +function addressPath(index, branch, account, coinType) { + return [ + (44 | hardeningConstant) >>> 0, // purpose + ((coinType || 0)| hardeningConstant) >>> 0, // coin type + ((account || 0) | hardeningConstant) >>> 0, // account + (branch || 0) >>> 0, // branch + index >>> 0 // index + ]; +} export const TRZ_LOADDEVICELIST_ATTEMPT = "TRZ_LOADDEVICELIST_ATTEMPT"; export const TRZ_LOADDEVICELIST_FAILED = "TRZ_LOADDEVICELIST_FAILED"; @@ -78,7 +101,7 @@ export const loadDeviceList = () => (dispatch, getState) => { }); devList.on("disconnectUnacquired", device => { - console.log("disconnect unacquired", device); + console.log("d.catch(error => dispatch({ error, type: SIGNTX_FAILED }));isconnect unacquired", device); }); }); @@ -89,3 +112,162 @@ export const selectDevice = (path) => async (dispatch, getState) => { if (!devList.devices[path]) return; dispatch({ device: devList.devices[path], type: TRZ_SELECTEDDEVICE_CHANGED }); }; + +export const signTransactionAttemptTrezor = (rawUnsigTx, constructTxResponse) => async (dispatch, getState) => { + dispatch({ type: SIGNTX_ATTEMPT }); + + const { grpc: { decodeMessageService, walletService } } = getState(); + const chainParams = selectors.chainParams(getState()); + + const device = selectors.trezorDevice(getState()); + // TODO: handle not having device + + console.log("construct tx response", constructTxResponse); + + try { + const decodedUnsigTxResp = await wallet.decodeTransaction(decodeMessageService, rawUnsigTx); + const decodedUnsigTx = decodedUnsigTxResp.getTransaction(); + const inputTxs = await wallet.getInputTransactions(walletService, + decodeMessageService, decodedUnsigTx); + const refTxs = inputTxs.map(walletTxToRefTx); + + const changeIndex = constructTxResponse.getChangeIndex(); + const txInfo = await dispatch(walletTxToBtcjsTx(decodedUnsigTx, + changeIndex, inputTxs)); + + const signedRaw = await device.run(async session => { + const signedResp = await session.signTx(txInfo.inputs, txInfo.outputs, + refTxs, chainParams.trezorCoinName, 0); + return signedResp.message.serialized.serialized_tx; + }); + + dispatch({ type: SIGNTX_SUCCESS }); + dispatch(publishTransactionAttempt(hexToRaw(signedRaw))); + + } catch (error) { + dispatch({ error, type: SIGNTX_FAILED }); + } +}; + +// walletTxToBtcjsTx converts a tx decoded by the decred wallet (ie, +// returned from the decodeRawTransaction call) into a bitcoinjs-compatible +// transaction (to be used in trezor) +export const walletTxToBtcjsTx = (tx, changeIndex, inputTxs) => async (dispatch, getState) => { + const { grpc: { walletService } } = getState(); + const chainParams = selectors.chainParams(getState()); + + const inputTxsMap = inputTxs.reduce((m, tx) => { + m[rawHashToHex(tx.getTransactionHash())] = tx; + return m; + }, {}); + + const inputs = []; + for (const inp of tx.getInputsList()) { + const inputTx = inputTxsMap[rawHashToHex(inp.getPreviousTransactionHash())]; + if (!inputTx) throw "Cannot sign transaction without knowing source tx " + + rawHashToHex(inp.getPreviousTransactionHash()); + + const inputTxOut = inputTx.getOutputsList()[inp.getPreviousTransactionIndex()]; + if (!inputTxOut) throw sprintf("Trying to use unknown outpoint %s:%d as input", + rawHashToHex(inp.getPreviousTransactionHash()), inp.getPreviousTransactionIndex()); + + const addr = inputTxOut.getAddressesList()[0]; + if (!addr) throw sprintf("Outpoint %s:%d does not have addresses.", + rawHashToHex(inp.getPreviousTransactionHash()), inp.getPreviousTransactionIndex()); + + const addrValidResp = await wallet.validateAddress(walletService, addr); + if (!addrValidResp.getIsValid()) throw "Input has an invalid address " + addr; + + // Trezor firmware (mcu) currently (2018-06-25) only support signing + // when all inputs of the transaction are from the wallet. This happens + // due to the fact that trezor firmware re-calculates the source + // pkscript given the address_n of the input, instead of using it (the + // pkscript) directly when hashing the tx prior to signing. This needs + // to be changed so that we can perform more advanced types of + // transactions. + if (!addrValidResp.getIsMine()) throw "Trezor only supports signing when all inputs are from the wallet."; + + const addrIndex = addrValidResp.getIndex(); + const addrBranch = addrValidResp.getIsInternal() ? 1 : 0; + inputs.push({ + prev_hash: rawHashToHex(inp.getPreviousTransactionHash()), + prev_index: inp.getPreviousTransactionIndex(), + amount: inp.getAmountIn(), + sequence: inp.getSequence(), + address_n: addressPath(addrIndex, addrBranch, WALLET_ACCOUNT, + chainParams.HDCoinType), + + // FIXME: this needs to be supported on trezor.js. + // decredTree: inp.getTree(), + // decredScriptVersion: 0, + }); + } + + const outputs = []; + for (const outp of tx.getOutputsList()) { + if (outp.getAddressesList().length != 1) { + // TODO: this will be true on OP_RETURNs. Support those. + throw "Output has different number of addresses than expected"; + } + + let addr = outp.getAddressesList()[0]; + const addrValidResp = await wallet.validateAddress(walletService, addr); + if (!addrValidResp.getIsValid()) throw "Not a valid address: " + addr; + let address_n = null; + + if (outp.getIndex() === changeIndex) { + const addrIndex = addrValidResp.getIndex(); + const addrBranch = addrValidResp.getIsInternal() ? 1 : 0; + address_n = addressPath(addrIndex, addrBranch, WALLET_ACCOUNT, + chainParams.HDCoinType); + addr = null; + } + + outputs.push({ + amount: outp.getValue(), + script_type: "PAYTOADDRESS", // needs to change on OP_RETURNs + address: addr, + address_n: address_n, + }); + } + + const txInfo = { + lock_time: tx.getLockTime(), + version: tx.getVersion(), + expiry: tx.getExpiry(), + inputs, + outputs + }; + + return txInfo; +}; + +// walletTxToRefTx converts a tx decoded by the decred wallet into a trezor +// RefTransaction object to be used with SignTx. +export function walletTxToRefTx(tx) { + const inputs = tx.getInputsList().map(inp => ({ + amount: inp.getAmountIn(), + prev_hash: rawHashToHex(inp.getPreviousTransactionHash()), + prev_index: inp.getPreviousTransactionIndex(), + + // TODO: this needs to be supported on trezor.js + // decredTree: inp.getTree(), + // decredScriptVersion: 0, + })); + + const bin_outputs = tx.getOutputsList().map(outp => ({ + amount: outp.getValue(), + script_pubkey: rawToHex(outp.getScript()), + })); + + const txInfo = { + hash: rawHashToHex(tx.getTransactionHash()), + lock_time: tx.getLockTime(), + version: tx.getVersion(), + expiry: tx.getExpiry(), + inputs, + bin_outputs, + }; + + return txInfo; +} diff --git a/app/components/buttons/SendTransactionButton.js b/app/components/buttons/SendTransactionButton.js index 9102838256..510e93e210 100644 --- a/app/components/buttons/SendTransactionButton.js +++ b/app/components/buttons/SendTransactionButton.js @@ -1,5 +1,6 @@ import { send } from "connectors"; import { PassphraseModalButton } from "./index"; +import KeyBlueButton from "./KeyBlueButton"; import { FormattedMessage as T } from "react-intl"; @autobind @@ -16,22 +17,41 @@ class SendTransactionButton extends React.Component { onSubmit && onSubmit(); } + async onAttemptSignTransactionTrezor() { + const { unsignedTransaction, onAttemptSignTransactionTrezor, + constructTxResponse, disabled, onSubmit } = this.props; + if (disabled || !onAttemptSignTransactionTrezor) return; + await onAttemptSignTransactionTrezor(unsignedTransaction, constructTxResponse); + onSubmit && onSubmit(); + } + render() { - const { disabled, isSendingTransaction, onShow, showModal, children } = this.props; + const { disabled, isSendingTransaction, children, isTrezor } = this.props; - return ( - } - modalDescription={children} - showModal={showModal} - onShow={onShow} - disabled={disabled || isSendingTransaction} - className="content-send" - onSubmit={this.onAttemptSignTransaction} - loading={isSendingTransaction} - buttonLabel={} - /> - ); + if (isTrezor) { + return ( + + + + ); + } else { + return ( + } + modalDescription={children} + disabled={disabled || isSendingTransaction} + className="content-send" + onSubmit={this.onAttemptSignTransaction} + loading={isSendingTransaction} + buttonLabel={} + /> + ); + } } } diff --git a/app/connectors/send.js b/app/connectors/send.js index 610c38fb9b..1511100a51 100644 --- a/app/connectors/send.js +++ b/app/connectors/send.js @@ -3,6 +3,7 @@ import { bindActionCreators } from "redux"; import { selectorMap } from "../fp"; import * as sel from "../selectors"; import * as ca from "../actions/ControlActions"; +import * as tza from "../actions/TrezorActions"; const mapStateToProps = selectorMap({ defaultSpendingAccount: sel.defaultSpendingAccount, @@ -17,6 +18,8 @@ const mapStateToProps = selectorMap({ unitDivisor: sel.unitDivisor, constructTxLowBalance: sel.constructTxLowBalance, isTransactionsSendTabDisabled: sel.isTransactionsSendTabDisabled, + constructTxResponse: sel.constructTxResponse, + isTrezor: sel.isTrezor, }); const mapDispatchToProps = dispatch => bindActionCreators({ @@ -25,6 +28,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({ onClearTransaction: ca.clearTransaction, getNextAddressAttempt: ca.getNextAddressAttempt, validateAddress: ca.validateAddress, + onAttemptSignTransactionTrezor: tza.signTransactionAttemptTrezor, }, dispatch); export default connect(mapStateToProps, mapDispatchToProps); diff --git a/app/selectors.js b/app/selectors.js index e229ac3f03..4ba70e5450 100644 --- a/app/selectors.js +++ b/app/selectors.js @@ -613,7 +613,7 @@ export const defaultSpendingAccount = createSelector( export const changePassphraseRequestAttempt = get([ "control", "changePassphraseRequestAttempt" ]); export const constructTxLowBalance = get([ "control", "constructTxLowBalance" ]); -const constructTxResponse = get([ "control", "constructTxResponse" ]); +export const constructTxResponse = get([ "control", "constructTxResponse" ]); const constructTxRequestAttempt = get([ "control", "constructTxRequestAttempt" ]); const signTransactionRequestAttempt = get([ "control", "signTransactionRequestAttempt" ]); export const signTransactionError = get([ "control", "signTransactionError" ]); @@ -918,10 +918,12 @@ export const stakeRewardsStats = createSelector( export const modalVisible = get([ "control", "modalVisible" ]); export const aboutModalMacOSVisible = get([ "control", "aboutModalMacOSVisible" ]); -export const isSignMessageDisabled = isWatchingOnly; +export const isTrezor = get([ "trezor", "enabled" ]); + +export const isSignMessageDisabled = and(isWatchingOnly, not(isTrezor)); export const isCreateAccountDisabled = isWatchingOnly; export const isChangePassPhraseDisabled = isWatchingOnly; -export const isTransactionsSendTabDisabled = isWatchingOnly; +export const isTransactionsSendTabDisabled = and(isWatchingOnly, not(isTrezor)); export const isTicketPurchaseTabDisabled = isWatchingOnly; export const politeiaURL = createSelector( @@ -974,3 +976,5 @@ export const initialProposalLoading = createSelector( [ proposalsDetails, getVettedProposalsAttempt ], ( proposals, getVettedAttempt ) => (Object.keys(proposals).length === 0) && getVettedAttempt ); + +export const trezorDevice = get([ "trezor", "device" ]); diff --git a/app/wallet/constants.js b/app/wallet/constants.js index 8019d34aea..c06167c8ae 100644 --- a/app/wallet/constants.js +++ b/app/wallet/constants.js @@ -7,15 +7,15 @@ export const TestNetParams = { SStxChangeMaturity: 1, GenesisTimestamp: 1489550400, TargetTimePerBlock: 2 * 60, // in seconds + WorkDiffWindowSize: 144, // no way to know which one the wallet is using right now, so we record both // types for the moment. LegacyHDCoinType: 11, HDCoinType: 1, - WorkDiffWindowSize: 144, - TreasuryAddress: "TcrypGAcGCRVXrES7hWqVZb5oLJKCZEtoL1", + trezorCoinName: "Decred Testnet", }; export const MainNetParams = { @@ -25,13 +25,13 @@ export const MainNetParams = { SStxChangeMaturity: 1, GenesisTimestamp: 1454954400, TargetTimePerBlock: 5 * 60, // in seconds + WorkDiffWindowSize: 144, // no way to know which one the wallet is using right now, so we record both // types for the moment. LegacyHDCoinType: 20, HDCoinType: 42, - WorkDiffWindowSize: 144, - TreasuryAddress: "Dcur2mcGjmENx4DhNqDctW5wJCVyT3Qeqkx", + trezorCoinName: "Decred", }; From 4ab680ffdc102ae569651f838d6e4eb00d221b09 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Mon, 9 Jul 2018 14:16:57 -0300 Subject: [PATCH 03/13] Pin and Passphrase modals --- app/actions/TrezorActions.js | 135 +++++++++++++++++- app/components/modals/trezor/Modals.js | 36 +++++ .../modals/trezor/PassPhraseModal.js | 38 +++++ app/components/modals/trezor/PinModal.js | 75 ++++++++++ app/components/modals/trezor/index.js | 1 + app/connectors/index.js | 1 + app/connectors/trezor.js | 20 +++ app/containers/App.js | 4 +- app/index.js | 6 + app/reducers/trezor.js | 35 +++++ app/selectors.js | 2 + app/style/Trezor.less | 27 ++++ 12 files changed, 376 insertions(+), 4 deletions(-) create mode 100644 app/components/modals/trezor/Modals.js create mode 100644 app/components/modals/trezor/PassPhraseModal.js create mode 100644 app/components/modals/trezor/PinModal.js create mode 100644 app/components/modals/trezor/index.js create mode 100644 app/connectors/trezor.js create mode 100644 app/style/Trezor.less diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index 24ae084b02..6934f4f469 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -76,6 +76,7 @@ export const loadDeviceList = () => (dispatch, getState) => { if (!currentDevice) { // first device connected. Use it. dispatch({ device, type: TRZ_SELECTEDDEVICE_CHANGED }); + setDeviceListeners(device, dispatch); } }); @@ -111,6 +112,127 @@ export const selectDevice = (path) => async (dispatch, getState) => { const devList = getState().trezor.deviceList; if (!devList.devices[path]) return; dispatch({ device: devList.devices[path], type: TRZ_SELECTEDDEVICE_CHANGED }); + setDeviceListeners(devList.devices[path], dispatch); +}; + +export const TRZ_PIN_REQUESTED = "TRZ_PIN_REQUESTED"; +export const TRZ_PIN_ENTERED = "TRZ_PIN_ENTERED"; +export const TRZ_PIN_CANCELED = "TRZ_PIN_CANCELED"; +export const TRZ_PASSPHRASE_REQUESTED = "TRZ_PASSPHRASE_REQUESTED"; +export const TRZ_PASSPHRASE_ENTERED = "TRZ_PASSPHRASE_ENTERED"; +export const TRZ_PASSPHRASE_CANCELED = "TRZ_PASSPHRASE_CANCELED"; + +function setDeviceListeners(device, dispatch) { + device.on("pin", (pinMessage, pinCallBack) => { + dispatch({ pinMessage, pinCallBack, type: TRZ_PIN_REQUESTED }); + }); + + device.on("passphrase", (passPhraseCallBack) => { + dispatch({ passPhraseCallBack, type: TRZ_PASSPHRASE_REQUESTED }); + }); +} + +// deviceRun is the main function for executing trezor operations. This handles +// cleanup for cancellations and device disconnections during mid-operation (eg: +// someone disconnected trezor while it was waiting for a pin input). +// In general, fn itself shouldn't handle errors, letting this function handle +// the common cases, which are then propagated up the call stack into fn's +// parent. +async function deviceRun(dispatch, getState, device, fn) { + + const handleError = error => { + const { trezor: { waitingForPin, waitingForPassphrase } } = getState(); + console.log("Handle error no deviceRun"); + if (waitingForPin) dispatch({ error, type: TRZ_PIN_CANCELED }); + if (waitingForPassphrase) dispatch({ error, type: TRZ_PASSPHRASE_CANCELED }); + if (error instanceof Error) { + if (error.message.includes("Inconsistent state")) { + return "Device returned inconsistent state. Disconnect and reconnect the device."; + } + } + return error; + }; + + try { + return await device.run(async session => { + try { + return await fn(session); + } catch (err) { + // doesn't seem to be reachable by trezor interruptions, but might be + // caused by fn() failing in some other way (even though it's + // recommended not to do (non-trezor) lengthy operations inside fn()) + throw handleError(err); + } + }); + } catch (outerErr) { + throw handleError(outerErr); + } +} + +export const TRZ_CANCELOPERATION_SUCCESS = "TRZ_CANCELOPERATION_SUCCESS"; +export const TRZ_CANCELOPERATION_FAILED = "TRZ_CANCELOPERATION_FAILED"; + +// Note that calling this function while no pin/passphrase operation is running +// will attempt to steal the device, cancelling operations from apps *other +// than decrediton*. +export const cancelCurrentOperation = () => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + const { trezor: { pinCallBack, passPhraseCallBack } } = getState(); + + if (!device) return; + try { + if (pinCallBack) await pinCallBack("cancelled", null); + else if (passPhraseCallBack) await passPhraseCallBack("cancelled", null); + else await device.steal(); + + dispatch({ type: TRZ_CANCELOPERATION_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_CANCELOPERATION_FAILED }); + } +}; + +export const submitPin = (pin) => (dispatch, getState) => { + const { trezor: { pinCallBack } } = getState(); + dispatch({ type: TRZ_PIN_ENTERED }); + if (!pinCallBack) return; + pinCallBack(null, pin); +}; + +export const submitPassPhrase = (passPhrase) => (dispatch, getState) => { + const { trezor: { passPhraseCallBack } } = getState(); + dispatch({ type: TRZ_PASSPHRASE_ENTERED }); + if (!passPhraseCallBack) return; + passPhraseCallBack(null, passPhrase); +}; + +// checkTrezorIsDcrwallet verifies whether the wallet currently running on +// dcrwallet (presumably a watch only wallet created from a trezor provided +// xpub) is the same wallet as the one of the currently connected trezor. This +// function throws an error if they are not the same. +// This is useful for making sure, prior to performing some wallet related +// function such as transaction signing, that trezor will correctly perform the +// operation. +// Note that this might trigger pin/passphrase modals, depending on the current +// trezor configuration. +// The way the check is performed is by generating the first address from the +// trezor wallet and then validating this address agains dcrwallet, ensuring +// this is an owned address at the appropriate branch/index. +// This check is only valid for a single session (ie, a single execution of +// `deviceRun`) as the physical device might change between sessions. +const checkTrezorIsDcrwallet = (session) => async (dispatch, getState) => { + const { grpc: { walletService } } = getState(); + const chainParams = selectors.chainParams(getState()); + + const address_n = addressPath(0, 0, WALLET_ACCOUNT, chainParams.HDCoinType); + const resp = await session.getAddress(address_n, chainParams.trezorCoinName, false); + const addr = resp.message.address; + + const addrValidResp = await wallet.validateAddress(walletService, addr); + if (!addrValidResp.getIsValid()) throw "Trezor provided an invalid address " + addr; + + if (!addrValidResp.getIsMine()) throw "Trezor and dcrwallet not running from the same extended public key"; + + if (addrValidResp.getIndex() !== 0) throw "Wallet replied with wrong index."; }; export const signTransactionAttemptTrezor = (rawUnsigTx, constructTxResponse) => async (dispatch, getState) => { @@ -120,7 +242,10 @@ export const signTransactionAttemptTrezor = (rawUnsigTx, constructTxResponse) => const chainParams = selectors.chainParams(getState()); const device = selectors.trezorDevice(getState()); - // TODO: handle not having device + if (!device) { + dispatch({ error: "Device not connected", type: SIGNTX_FAILED }); + return; + } console.log("construct tx response", constructTxResponse); @@ -135,7 +260,9 @@ export const signTransactionAttemptTrezor = (rawUnsigTx, constructTxResponse) => const txInfo = await dispatch(walletTxToBtcjsTx(decodedUnsigTx, changeIndex, inputTxs)); - const signedRaw = await device.run(async session => { + const signedRaw = await deviceRun(dispatch, getState, device, async session => { + await dispatch(checkTrezorIsDcrwallet(session)); + const signedResp = await session.signTx(txInfo.inputs, txInfo.outputs, refTxs, chainParams.trezorCoinName, 0); return signedResp.message.serialized.serialized_tx; @@ -196,6 +323,7 @@ export const walletTxToBtcjsTx = (tx, changeIndex, inputTxs) => async (dispatch, sequence: inp.getSequence(), address_n: addressPath(addrIndex, addrBranch, WALLET_ACCOUNT, chainParams.HDCoinType), + decred_tree: inp.getTree() // FIXME: this needs to be supported on trezor.js. // decredTree: inp.getTree(), @@ -228,6 +356,7 @@ export const walletTxToBtcjsTx = (tx, changeIndex, inputTxs) => async (dispatch, script_type: "PAYTOADDRESS", // needs to change on OP_RETURNs address: addr, address_n: address_n, + decred_script_version: outp.getVersion(), }); } @@ -249,6 +378,7 @@ export function walletTxToRefTx(tx) { amount: inp.getAmountIn(), prev_hash: rawHashToHex(inp.getPreviousTransactionHash()), prev_index: inp.getPreviousTransactionIndex(), + decred_tree: inp.getTree() // TODO: this needs to be supported on trezor.js // decredTree: inp.getTree(), @@ -258,6 +388,7 @@ export function walletTxToRefTx(tx) { const bin_outputs = tx.getOutputsList().map(outp => ({ amount: outp.getValue(), script_pubkey: rawToHex(outp.getScript()), + decred_script_version: outp.getVersion(), })); const txInfo = { diff --git a/app/components/modals/trezor/Modals.js b/app/components/modals/trezor/Modals.js new file mode 100644 index 0000000000..a391fc9eda --- /dev/null +++ b/app/components/modals/trezor/Modals.js @@ -0,0 +1,36 @@ +import { trezor } from "connectors"; +import PinModal from "./PinModal"; +import PassPhraseModal from "./PassPhraseModal"; +import "style/Trezor.less"; + +@autobind +class TrezorModals extends React.Component { + constructor(props) { + super(props); + } + + onCancelModal() { + this.props.cancelCurrentOperation(); + } + + render() { + if (this.props.waitingForPin) { + return ; + } else if (this.props.waitingForPassPhrase) { + return ; + } else { + return null; + } + } +} + +const TrezorModalsOrNone = ({ isTrezor, ...props }) => + isTrezor ? : null; + +export default trezor(TrezorModalsOrNone); diff --git a/app/components/modals/trezor/PassPhraseModal.js b/app/components/modals/trezor/PassPhraseModal.js new file mode 100644 index 0000000000..8825143411 --- /dev/null +++ b/app/components/modals/trezor/PassPhraseModal.js @@ -0,0 +1,38 @@ +import { PassphraseModal } from "../PassphraseModal"; +import { FormattedMessage as T } from "react-intl"; + +@autobind +class TrezorPassphraseModal extends React.Component { + constructor(props) { + super(props); + } + + onSubmit(passPhrase) { + console.log("gonna submit", passPhrase); + this.props.submitPassPhrase(passPhrase); + } + + render() { + const { onCancelModal } = this.props; + const { onSubmit } = this; + + const trezorLabel = this.props.device ? this.props.device.features.label : ""; + + return ( + } + className="trezor-passphrase-modal" + modalDescription={ +

+ '{trezorLabel}' }} /> +

+ } + {...{ onCancelModal, onSubmit }} + /> + ); + } +} + +export default TrezorPassphraseModal; diff --git a/app/components/modals/trezor/PinModal.js b/app/components/modals/trezor/PinModal.js new file mode 100644 index 0000000000..eb33044f42 --- /dev/null +++ b/app/components/modals/trezor/PinModal.js @@ -0,0 +1,75 @@ +import Modal from "../Modal"; +import { FormattedMessage as T } from "react-intl"; +import { PasswordInput } from "inputs"; +import { ButtonsToolbar } from "../PassphraseModal"; +import { InvisibleButton } from "buttons"; + +const PinButton = ({ index, label, onClick }) => +
onClick(index)}>{label}
; + +@autobind +class PinModal extends React.Component { + constructor(props) { + super(props); + this.state = { currentPin: "" }; + } + + onPinButtonClick(index) { + this.setState({ currentPin: this.state.currentPin + index }); + } + + onCancelModal() { + this.setState({ currentPin: "" }); + this.props.onCancelModal(); + } + + onSubmit() { + this.props.submitPin(this.state.currentPin); + } + + onClearPin() { + this.setState({ currentPin: "" }); + } + + render() { + const { onCancelModal, onSubmit, onPinButtonClick, onClearPin } = this; + + const labels = "ABCDEFGHI"; + const currentPin = this.state.currentPin.split("").map(v => labels[parseInt(v)-1]).join(""); + + const Button = ({ index }) => + ; + + const trezorLabel = this.props.device ? this.props.device.features.label : ""; + + return ( + +

+

'{trezorLabel}' }} />

+
+
+
+ + + +
+
+ +
+ +
+ ); + } +} + +export default PinModal; diff --git a/app/components/modals/trezor/index.js b/app/components/modals/trezor/index.js new file mode 100644 index 0000000000..a4848bf877 --- /dev/null +++ b/app/components/modals/trezor/index.js @@ -0,0 +1 @@ +export { default as TrezorModals } from "./Modals"; diff --git a/app/connectors/index.js b/app/connectors/index.js index 82574fc9e4..f50380eb3e 100644 --- a/app/connectors/index.js +++ b/app/connectors/index.js @@ -54,3 +54,4 @@ export { default as proposals } from "./proposals"; export { default as newProposalCounts } from "./newProposalCounts"; export { default as network } from "./network"; export { default as treasuryInfo } from "./treasuryInfo"; +export { default as trezor } from "./trezor"; diff --git a/app/connectors/trezor.js b/app/connectors/trezor.js new file mode 100644 index 0000000000..52429e017e --- /dev/null +++ b/app/connectors/trezor.js @@ -0,0 +1,20 @@ +import { connect } from "react-redux"; +import { selectorMap } from "../fp"; +import { bindActionCreators } from "redux"; +import * as sel from "../selectors"; +import * as trza from "../actions/TrezorActions"; + +const mapStateToProps = selectorMap({ + isTrezor: sel.isTrezor, + waitingForPin: sel.trezorWaitingForPin, + waitingForPassPhrase: sel.trezorWaitingForPassPhrase, + device: sel.trezorDevice, +}); + +const mapDispatchToProps = dispatch => bindActionCreators({ + cancelCurrentOperation: trza.cancelCurrentOperation, + submitPin: trza.submitPin, + submitPassPhrase: trza.submitPassPhrase, +}, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps); diff --git a/app/containers/App.js b/app/containers/App.js index 1f4af06c6c..8102096240 100644 --- a/app/containers/App.js +++ b/app/containers/App.js @@ -11,12 +11,12 @@ import FatalErrorPage from "components/views/FatalErrorPage"; import Snackbar from "components/Snackbar"; import AboutModal from "../components/modals/AboutModal/AboutModal"; import { log } from "wallet"; +import { TrezorModals } from "components/modals/trezor"; import "style/Themes.less"; import "style/Layout.less"; import { ipcRenderer } from "electron"; const topLevelAnimation = { atEnter: { opacity: 0 }, atLeave: { opacity: 0 }, atActive: { opacity: 1 } }; - @autobind class App extends React.Component { static propTypes = { @@ -119,7 +119,7 @@ class App extends React.Component { - + ); diff --git a/app/index.js b/app/index.js index 98780f3971..b759afc915 100644 --- a/app/index.js +++ b/app/index.js @@ -391,6 +391,12 @@ var initialState = { deviceList: null, transportError: false, device: null, + waitingForPin: false, + waitingForPassPhrase: false, + pinCallBack: null, + passPhraseCallBack: null, + pinMessage: null, + passPhraseMessage: null, }, locales: locales }; diff --git a/app/reducers/trezor.js b/app/reducers/trezor.js index 5efc04a07b..3e5458569a 100644 --- a/app/reducers/trezor.js +++ b/app/reducers/trezor.js @@ -2,6 +2,9 @@ import { TRZ_LOADDEVICELIST_ATTEMPT, TRZ_LOADDEVICELIST_FAILED, TRZ_LOADDEVICELIST_SUCCESS, TRZ_DEVICELISTTRANSPORT_LOST, TRZ_SELECTEDDEVICE_CHANGED, + TRZ_PIN_REQUESTED, TRZ_PIN_ENTERED, TRZ_PIN_CANCELED, + TRZ_PASSPHRASE_REQUESTED, TRZ_PASSPHRASE_ENTERED, TRZ_PASSPHRASE_CANCELED, + TRZ_CANCELOPERATION_SUCCESS, } from "actions/TrezorActions"; export default function trezor(state = {}, action) { @@ -31,6 +34,38 @@ export default function trezor(state = {}, action) { return { ...state, device: action.device, }; + case TRZ_PIN_REQUESTED: + return { ...state, + waitingForPin: true, + pinCallBack: action.pinCallBack, + pinMessage: action.pinMessage, + }; + case TRZ_PIN_CANCELED: + case TRZ_PIN_ENTERED: + return { ...state, + waitingForPin: false, + pinCallBack: null, + pinMessage: null, + }; + case TRZ_PASSPHRASE_REQUESTED: + return { ...state, + waitingForPassPhrase: true, + passPhraseCallBack: action.passPhraseCallBack, + }; + case TRZ_PASSPHRASE_CANCELED: + case TRZ_PASSPHRASE_ENTERED: + return { ...state, + waitingForPassPhrase: false, + passPhraseCallBack: null, + }; + case TRZ_CANCELOPERATION_SUCCESS: + return { ...state, + waitingForPin: false, + pinCallBack: null, + pinMessage: null, + waitingForPassPhrase: false, + passPhraseCallBack: null, + }; default: return state; } diff --git a/app/selectors.js b/app/selectors.js index 4ba70e5450..3ebeec9de4 100644 --- a/app/selectors.js +++ b/app/selectors.js @@ -977,4 +977,6 @@ export const initialProposalLoading = createSelector( ( proposals, getVettedAttempt ) => (Object.keys(proposals).length === 0) && getVettedAttempt ); +export const trezorWaitingForPin = get([ "trezor", "waitingForPin" ]); +export const trezorWaitingForPassPhrase = get([ "trezor", "waitingForPassPhrase" ]); export const trezorDevice = get([ "trezor", "device" ]); diff --git a/app/style/Trezor.less b/app/style/Trezor.less new file mode 100644 index 0000000000..d1a0e319eb --- /dev/null +++ b/app/style/Trezor.less @@ -0,0 +1,27 @@ +@import (reference) "./main.less"; + +.trezor-pin-modal { + h1 { + margin: 0; + } + + .pin-pad { + display: grid; + grid-template-columns: 2em 2em 2em; + text-align: center; + font-size: 150%; + + .pin-pad-button { + padding: 0.5em 0em; + border: 1px solid transparent; + + &:hover { + border: 1px solid #6397ff; + } + } + } +} + +.trezor-label { + font-family: @font-family-monospaced; +} From 8ea37e66d636ba136f19e26f8a39454370aa8d53 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Tue, 10 Jul 2018 17:19:58 -0300 Subject: [PATCH 04/13] Trezor Setup Page --- app/actions/TrezorActions.js | 225 +++++++++++++++++- app/components/SideBar/MenuLinks/index.js | 5 + app/components/modals/trezor/Modals.js | 6 + app/components/modals/trezor/WordModal.js | 79 ++++++ .../views/TrezorPage/ChangeLabel.js | 57 +++++ .../views/TrezorPage/ConfigButtons.js | 45 ++++ .../views/TrezorPage/FirmwareUpdate.js | 54 +++++ app/components/views/TrezorPage/Header.js | 9 + .../views/TrezorPage/NoDevicePage.js | 11 + app/components/views/TrezorPage/Page.js | 23 ++ .../views/TrezorPage/RecoveryButtons.js | 48 ++++ app/components/views/TrezorPage/index.js | 82 +++++++ app/connectors/routing.js | 1 + app/connectors/trezor.js | 11 + app/containers/Wallet.js | 2 + app/helpers/trezor.js | 209 ++++++++++++++++ .../docs/en/Warnings/TrezorFirmwareUpdate.md | 8 + app/i18n/docs/en/Warnings/TrezorWipe.md | 8 + app/i18n/docs/en/index.js | 2 + app/index.js | 3 + app/reducers/snackbar.js | 68 ++++++ app/reducers/trezor.js | 66 +++++ app/selectors.js | 2 + app/style/Header.less | 1 + app/style/Icons.less | 3 + app/style/MiscComponents.less | 12 + app/style/Trezor.less | 32 +++ app/style/icons/trezor-active.png | Bin 0 -> 2335 bytes app/style/icons/trezor-default.png | Bin 0 -> 870 bytes app/style/icons/trezor-hover.png | Bin 0 -> 2330 bytes 30 files changed, 1071 insertions(+), 1 deletion(-) create mode 100644 app/components/modals/trezor/WordModal.js create mode 100644 app/components/views/TrezorPage/ChangeLabel.js create mode 100644 app/components/views/TrezorPage/ConfigButtons.js create mode 100644 app/components/views/TrezorPage/FirmwareUpdate.js create mode 100644 app/components/views/TrezorPage/Header.js create mode 100644 app/components/views/TrezorPage/NoDevicePage.js create mode 100644 app/components/views/TrezorPage/Page.js create mode 100644 app/components/views/TrezorPage/RecoveryButtons.js create mode 100644 app/components/views/TrezorPage/index.js create mode 100644 app/helpers/trezor.js create mode 100644 app/i18n/docs/en/Warnings/TrezorFirmwareUpdate.md create mode 100644 app/i18n/docs/en/Warnings/TrezorWipe.md create mode 100644 app/style/icons/trezor-active.png create mode 100644 app/style/icons/trezor-default.png create mode 100644 app/style/icons/trezor-hover.png diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index 6934f4f469..975b5a0aac 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -2,9 +2,11 @@ import * as trezorjs from "trezor.js"; import trezorTransports from "trezor-link"; import * as wallet from "wallet"; import * as selectors from "selectors"; +import fs from "fs"; import { sprintf } from "sprintf-js"; import { rawHashToHex, rawToHex, hexToRaw } from "helpers"; import { publishTransactionAttempt } from "./ControlActions"; +import { model1_decred_homescreen } from "helpers/trezor"; import { EXTERNALREQUEST_TREZOR_BRIDGE } from "main_dev/externalRequests"; import { SIGNTX_ATTEMPT, SIGNTX_FAILED, SIGNTX_SUCCESS } from "./ControlActions"; @@ -121,6 +123,9 @@ export const TRZ_PIN_CANCELED = "TRZ_PIN_CANCELED"; export const TRZ_PASSPHRASE_REQUESTED = "TRZ_PASSPHRASE_REQUESTED"; export const TRZ_PASSPHRASE_ENTERED = "TRZ_PASSPHRASE_ENTERED"; export const TRZ_PASSPHRASE_CANCELED = "TRZ_PASSPHRASE_CANCELED"; +export const TRZ_WORD_REQUESTED = "TRZ_WORD_REQUESTED"; +export const TRZ_WORD_ENTERED = "TRZ_WORD_ENTERED"; +export const TRZ_WORD_CANCELED = "TRZ_WORD_CANCELED"; function setDeviceListeners(device, dispatch) { device.on("pin", (pinMessage, pinCallBack) => { @@ -130,6 +135,10 @@ function setDeviceListeners(device, dispatch) { device.on("passphrase", (passPhraseCallBack) => { dispatch({ passPhraseCallBack, type: TRZ_PASSPHRASE_REQUESTED }); }); + + device.on("word", (wordCallBack) => { + dispatch({ wordCallBack, type: TRZ_WORD_REQUESTED }); + }); } // deviceRun is the main function for executing trezor operations. This handles @@ -177,12 +186,13 @@ export const TRZ_CANCELOPERATION_FAILED = "TRZ_CANCELOPERATION_FAILED"; // than decrediton*. export const cancelCurrentOperation = () => async (dispatch, getState) => { const device = selectors.trezorDevice(getState()); - const { trezor: { pinCallBack, passPhraseCallBack } } = getState(); + const { trezor: { pinCallBack, passPhraseCallBack, wordCallBack } } = getState(); if (!device) return; try { if (pinCallBack) await pinCallBack("cancelled", null); else if (passPhraseCallBack) await passPhraseCallBack("cancelled", null); + else if (wordCallBack) await wordCallBack("cancelled", null); else await device.steal(); dispatch({ type: TRZ_CANCELOPERATION_SUCCESS }); @@ -205,6 +215,13 @@ export const submitPassPhrase = (passPhrase) => (dispatch, getState) => { passPhraseCallBack(null, passPhrase); }; +export const submitWord = (word) => (dispatch, getState) => { + const { trezor: { wordCallBack } } = getState(); + dispatch({ type: TRZ_WORD_ENTERED }); + if (!wordCallBack) return; + wordCallBack(null, word); +}; + // checkTrezorIsDcrwallet verifies whether the wallet currently running on // dcrwallet (presumably a watch only wallet created from a trezor provided // xpub) is the same wallet as the one of the currently connected trezor. This @@ -402,3 +419,209 @@ export function walletTxToRefTx(tx) { return txInfo; } + +export const TRZ_TOGGLEPINPROTECTION_ATTEMPT = "TRZ_TOGGLEPINPROTECTION_ATTEMPT"; +export const TRZ_TOGGLEPINPROTECTION_FAILED = "TRZ_TOGGLEPINPROTECTION_FAILED"; +export const TRZ_TOGGLEPINPROTECTION_SUCCESS = "TRZ_TOGGLEPINPROTECTION_SUCCESS"; + +export const togglePinProtection = () => async (dispatch, getState) => { + + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_TOGGLEPINPROTECTION_FAILED }); + return; + } + + const clearProtection = !!device.features.pin_protection; + dispatch({ clearProtection, type: TRZ_TOGGLEPINPROTECTION_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + await session.changePin(clearProtection); + }); + dispatch({ clearProtection, deviceLabel: device.features.label, type: TRZ_TOGGLEPINPROTECTION_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_TOGGLEPINPROTECTION_FAILED }); + } +}; + +export const TRZ_TOGGLEPASSPHRASEPROTECTION_ATTEMPT = "TRZ_TOGGLEPASSPHRASEPROTECTION_ATTEMPT"; +export const TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED = "TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED"; +export const TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS = "TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS"; + +export const togglePassPhraseProtection = () => async (dispatch, getState) => { + + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED }); + return; + } + + const enableProtection = !device.features.passphrase_protection; + dispatch({ enableProtection, type: TRZ_TOGGLEPASSPHRASEPROTECTION_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + await session.togglePassphrase(enableProtection); + }); + dispatch({ enableProtection, deviceLabel: device.features.label, type: TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED }); + } +}; + +export const TRZ_CHANGEHOMESCREEN_ATTEMPT = "TRZ_CHANGEHOMESCREEN_ATTEMPT"; +export const TRZ_CHANGEHOMESCREEN_FAILED = "TRZ_CHANGEHOMESCREEN_FAILED"; +export const TRZ_CHANGEHOMESCREEN_SUCCESS = "TRZ_CHANGEHOMESCREEN_SUCCESS"; + +export const changeToDecredHomeScreen = () => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED }); + return; + } + + dispatch({ type: TRZ_CHANGEHOMESCREEN_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + await session.changeHomescreen(model1_decred_homescreen); + }); + dispatch({ type: TRZ_CHANGEHOMESCREEN_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_CHANGEHOMESCREEN_FAILED }); + } +}; + +export const TRZ_CHANGELABEL_ATTEMPT = "TRZ_CHANGELABEL_ATTEMPT"; +export const TRZ_CHANGELABEL_FAILED = "TRZ_CHANGELABEL_FAILED"; +export const TRZ_CHANGELABEL_SUCCESS = "TRZ_CHANGELABEL_SUCCESS"; + +export const changeLabel = (label) => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_CHANGELABEL_FAILED }); + return; + } + + dispatch({ type: TRZ_CHANGELABEL_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + await session.changeLabel(label); + }); + dispatch({ deviceLabel: label, type: TRZ_CHANGELABEL_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_CHANGELABEL_FAILED }); + } +}; + +export const TRZ_WIPEDEVICE_ATTEMPT = "TRZ_WIPEDEVICE_ATTEMPT"; +export const TRZ_WIPEDEVICE_FAILED = "TRZ_WIPEDEVICE_FAILED"; +export const TRZ_WIPEDEVICE_SUCCESS = "TRZ_WIPEDEVICE_SUCCESS"; + +export const wipeDevice = () => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_WIPEDEVICE_FAILED }); + return; + } + + dispatch({ type: TRZ_WIPEDEVICE_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + await session.wipeDevice(); + }); + dispatch({ type: TRZ_WIPEDEVICE_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_WIPEDEVICE_FAILED }); + } +}; + +export const TRZ_RECOVERDEVICE_ATTEMPT = "TRZ_RECOVERDEVICE_ATTEMPT"; +export const TRZ_RECOVERDEVICE_FAILED = "TRZ_RECOVERDEVICE_FAILED"; +export const TRZ_RECOVERDEVICE_SUCCESS = "TRZ_RECOVERDEVICE_SUCCESS"; + +export const recoverDevice = () => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_RECOVERDEVICE_FAILED }); + return; + } + + dispatch({ type: TRZ_RECOVERDEVICE_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + const settings = { + word_count: 24, // FIXED at 24 (256 bits) + passphrase_protection: false, + pin_protection: false, + label: "New DCR Trezor", + dry_run: false, + }; + + await session.recoverDevice(settings); + }); + dispatch({ type: TRZ_RECOVERDEVICE_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_RECOVERDEVICE_FAILED }); + } +}; + +export const TRZ_INITDEVICE_ATTEMPT = "TRZ_INITDEVICE_ATTEMPT"; +export const TRZ_INITDEVICE_FAILED = "TRZ_INITDEVICE_FAILED"; +export const TRZ_INITDEVICE_SUCCESS = "TRZ_INITDEVICE_SUCCESS"; + +export const initDevice = () => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_RECOVERDEVICE_FAILED }); + return; + } + + dispatch({ type: TRZ_INITDEVICE_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + const settings = { + strength: 256, // 24 words + passphrase_protection: false, + pin_protection: false, + label: "New DCR Trezor", + }; + + await session.resetDevice(settings); + }); + dispatch({ type: TRZ_INITDEVICE_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_INITDEVICE_FAILED }); + } +}; + +export const TRZ_UPDATEFIRMWARE_ATTEMPT = "TRZ_UPDATEFIRMWARE_ATTEMPT"; +export const TRZ_UPDATEFIRMWARE_FAILED = "TRZ_UPDATEFIRMWARE_FAILED"; +export const TRZ_UPDATEFIRMWARE_SUCCESS = "TRZ_UPDATEFIRMWARE_SUCCESS"; + +export const updateFirmware = (path) => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_UPDATEFIRMWARE_FAILED }); + return; + } + + dispatch({ type: TRZ_UPDATEFIRMWARE_ATTEMPT }); + + try { + const rawFirmware = fs.readFileSync(path); + const hexFirmware = rawToHex(rawFirmware); + + await deviceRun(dispatch, getState, device, async session => { + await session.updateFirmware(hexFirmware); + }); + dispatch({ type: TRZ_UPDATEFIRMWARE_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_UPDATEFIRMWARE_FAILED }); + } +}; diff --git a/app/components/SideBar/MenuLinks/index.js b/app/components/SideBar/MenuLinks/index.js index 8d369103f3..c6e2cf6e49 100644 --- a/app/components/SideBar/MenuLinks/index.js +++ b/app/components/SideBar/MenuLinks/index.js @@ -23,6 +23,11 @@ class MenuLinks extends React.Component { constructor (props) { super(props); + + this.links = [ ...linkList ]; + if (props.isTrezor) { + this.links.push({ path: "/trezor", link: , icon:"trezor" }); + } } componentDidMount() { diff --git a/app/components/modals/trezor/Modals.js b/app/components/modals/trezor/Modals.js index a391fc9eda..c38896eaea 100644 --- a/app/components/modals/trezor/Modals.js +++ b/app/components/modals/trezor/Modals.js @@ -1,6 +1,7 @@ import { trezor } from "connectors"; import PinModal from "./PinModal"; import PassPhraseModal from "./PassPhraseModal"; +import WordModal from "./WordModal"; import "style/Trezor.less"; @autobind @@ -24,6 +25,11 @@ class TrezorModals extends React.Component { {...this.props} onCancelModal={this.onCancelModal} />; + } else if (this.props.waitingForWord) { + return ; } else { return null; } diff --git a/app/components/modals/trezor/WordModal.js b/app/components/modals/trezor/WordModal.js new file mode 100644 index 0000000000..2fb0b34e18 --- /dev/null +++ b/app/components/modals/trezor/WordModal.js @@ -0,0 +1,79 @@ +import Modal from "../Modal"; +import { FormattedMessage as T } from "react-intl"; +import { ButtonsToolbar } from "../PassphraseModal"; +import Select from "react-select"; +import { word_list } from "helpers/trezor"; + +const input_options = word_list.map(w => ({ word: w })); + +@autobind +class WordModal extends React.Component { + constructor(props) { + super(props); + this.state = { word: "", value: null }; + } + + onCancelModal() { + this.setState({ word: "", value: null }); + this.props.onCancelModal(); + } + + onSubmit() { + if (!this.state.word) return; + this.props.submitWord(this.state.word); + this.setState({ word: "", value: null }); + } + + onWordChanged(value) { + this.setState({ word: value, value: { word: value } }); + } + + onSelectKeyDown(e) { + if (e.keyCode === 13 && this.state.word) { + this.onSubmit(); + } + } + + getSeedWords (input, callback) { + input = input.toLowerCase(); + const options = input_options + .filter(w => w.word.toLowerCase().substr(0, input.length) === input); + callback(null, { + options: options.slice(0, 5) + }); + } + + render() { + const { onCancelModal, onSubmit, onWordChanged, onSelectKeyDown, getSeedWords } = this; + + return ( + +

+

+ +
+ n && n.focus()} + autoFocus + simpleValue + multi={false} + clearable={false} + multi={false} + filterOptions={false} + valueKey="word" + labelKey="word" + loadOptions={getSeedWords} + onChange={onWordChanged} + value={this.state.value} + placeholder={} + onInputKeyDown={onSelectKeyDown} + /> +
+ + +
+ ); + } +} + +export default WordModal; diff --git a/app/components/views/TrezorPage/ChangeLabel.js b/app/components/views/TrezorPage/ChangeLabel.js new file mode 100644 index 0000000000..99c628f9ca --- /dev/null +++ b/app/components/views/TrezorPage/ChangeLabel.js @@ -0,0 +1,57 @@ +import { VerticalAccordion } from "shared"; +import { FormattedMessage as T } from "react-intl"; +import { TextInput } from "inputs"; +import { KeyBlueButton } from "buttons"; + +@autobind +class ChangeLabel extends React.Component { + constructor(props) { + super(props); + this.state = { newLabel: "" }; + } + + onChangeLabelClicked() { + this.props.onChangeLabel(this.state.newLabel); + } + + onNewLabelChanged(e) { + this.setState({ newLabel: e.target.value }); + } + + render() { + + const changeLabelHeader = ( + + + + ); + + const { loading } = this.props; + + return ( + + +
+
+ +
+
+ + + +
+
+
+ + ); + } +} + +export default ChangeLabel; diff --git a/app/components/views/TrezorPage/ConfigButtons.js b/app/components/views/TrezorPage/ConfigButtons.js new file mode 100644 index 0000000000..293979fb25 --- /dev/null +++ b/app/components/views/TrezorPage/ConfigButtons.js @@ -0,0 +1,45 @@ +import { VerticalAccordion } from "shared"; +import { FormattedMessage as T } from "react-intl"; +import { KeyBlueButton } from "buttons"; + +@autobind +class ConfigButtons extends React.Component { + constructor(props) { + super(props); + } + + render() { + + const ConfigButtonsHeader = ( + + + + ); + + const { loading, onTogglePinProtection, onTogglePassPhraseProtection, + onChangeHomeScreen } = this.props; + + return ( + + + + + + + + + + + + + + + ); + } +} + +export default ConfigButtons; diff --git a/app/components/views/TrezorPage/FirmwareUpdate.js b/app/components/views/TrezorPage/FirmwareUpdate.js new file mode 100644 index 0000000000..abd3f8a274 --- /dev/null +++ b/app/components/views/TrezorPage/FirmwareUpdate.js @@ -0,0 +1,54 @@ +import { VerticalAccordion } from "shared"; +import { FormattedMessage as T } from "react-intl"; +import { DangerButton } from "buttons"; +import { Documentation } from "shared"; +import { PathBrowseInput } from "inputs"; + +@autobind +class FirmwareUpdate extends React.Component { + constructor(props) { + super(props); + this.state = { path: "" }; + } + + onChangePath(path) { + this.setState({ path }); + } + + onUpdateFirmware() { + this.props.onUpdateFirmware(this.state.path); + } + + render() { + + const header = ( + + + + ); + + const { loading } = this.props; + + return ( + +
+ +
+ +

+
+ + + + +
+ + ); + } +} + +export default FirmwareUpdate; diff --git a/app/components/views/TrezorPage/Header.js b/app/components/views/TrezorPage/Header.js new file mode 100644 index 0000000000..2f271b7b0b --- /dev/null +++ b/app/components/views/TrezorPage/Header.js @@ -0,0 +1,9 @@ +import { FormattedMessage as T } from "react-intl"; +import { StandaloneHeader } from "layout"; + +export default () => + } + description={} + />; diff --git a/app/components/views/TrezorPage/NoDevicePage.js b/app/components/views/TrezorPage/NoDevicePage.js new file mode 100644 index 0000000000..10ff56cb58 --- /dev/null +++ b/app/components/views/TrezorPage/NoDevicePage.js @@ -0,0 +1,11 @@ +import { FormattedMessage as T } from "react-intl"; +import Header from "./Header"; +import { StandalonePage } from "layout"; + +export default () => ( + }> +
+ +
+
+); diff --git a/app/components/views/TrezorPage/Page.js b/app/components/views/TrezorPage/Page.js new file mode 100644 index 0000000000..7669fe68ef --- /dev/null +++ b/app/components/views/TrezorPage/Page.js @@ -0,0 +1,23 @@ +import { StandalonePage } from "layout"; +import Header from "./Header"; +import ChangeLabel from "./ChangeLabel"; +import ConfigButtons from "./ConfigButtons"; +import RecoveryButtons from "./RecoveryButtons"; +import FirmwareUpdate from "./FirmwareUpdate"; + +export default ({ + onTogglePinProtection, onTogglePassPhraseProtection, onChangeHomeScreen, + onChangeLabel, onWipeDevice, onRecoverDevice, onInitDevice, onUpdateFirmware, + loading, +}) => ( + }> + + + + + + + + +); diff --git a/app/components/views/TrezorPage/RecoveryButtons.js b/app/components/views/TrezorPage/RecoveryButtons.js new file mode 100644 index 0000000000..28696cee0a --- /dev/null +++ b/app/components/views/TrezorPage/RecoveryButtons.js @@ -0,0 +1,48 @@ +import { VerticalAccordion } from "shared"; +import { FormattedMessage as T } from "react-intl"; +import { DangerButton } from "buttons"; +import { Documentation } from "shared"; + +@autobind +class RecoveryButtons extends React.Component { + constructor(props) { + super(props); + } + + render() { + + const header = ( + + + + ); + + const { loading, onWipeDevice, onRecoverDevice, onInitDevice } = this.props; + + return ( + +
+ +
+ + + + + + + + + + + +
+ + ); + } +} + +export default RecoveryButtons; diff --git a/app/components/views/TrezorPage/index.js b/app/components/views/TrezorPage/index.js new file mode 100644 index 0000000000..3c27174e4f --- /dev/null +++ b/app/components/views/TrezorPage/index.js @@ -0,0 +1,82 @@ +import { trezor } from "connectors"; +import Page from "./Page"; +import NoDevicePage from "./NoDevicePage"; +import "style/Trezor.less"; + +@autobind +class TrezorPage extends React.Component { + + constructor(props) { + super(props); + } + + onTogglePinProtection() { + this.props.togglePinProtection(); + } + + onTogglePassPhraseProtection() { + this.props.togglePassPhraseProtection(); + } + + onChangeHomeScreen() { + this.props.changeToDecredHomeScreen(); + } + + onChangeLabel(newLabel) { + this.props.changeLabel(newLabel); + } + + onWipeDevice() { + this.props.wipeDevice(); + } + + onRecoverDevice() { + this.props.recoverDevice(); + } + + onUpdateFirmware(path) { + this.props.updateFirmware(path); + } + + onInitDevice() { + this.props.initDevice(); + } + + render() { + const { device } = this.props; + if (!device) return ; + + const loading = this.props.performingOperation; + + const { + onTogglePinProtection, + onTogglePassPhraseProtection, + onChangeHomeScreen, + onChangeLabel, + onWipeDevice, + onRecoverDevice, + onInitDevice, + onUpdateFirmware, + } = this; + + return ( + + ); + } +} + +export default trezor(TrezorPage); diff --git a/app/connectors/routing.js b/app/connectors/routing.js index 3fde906261..f9165df3d7 100644 --- a/app/connectors/routing.js +++ b/app/connectors/routing.js @@ -6,6 +6,7 @@ import * as ca from "../actions/ClientActions"; const mapStateToProps = selectorMap({ location: sel.location, + isTrezor: sel.isTrezor, }); const mapDispatchToProps = dispatch => bindActionCreators({ diff --git a/app/connectors/trezor.js b/app/connectors/trezor.js index 52429e017e..e4dca84497 100644 --- a/app/connectors/trezor.js +++ b/app/connectors/trezor.js @@ -8,13 +8,24 @@ const mapStateToProps = selectorMap({ isTrezor: sel.isTrezor, waitingForPin: sel.trezorWaitingForPin, waitingForPassPhrase: sel.trezorWaitingForPassPhrase, + waitingForWord: sel.trezorWaitingForWord, device: sel.trezorDevice, + performingOperation: sel.trezorPerformingOperation, }); const mapDispatchToProps = dispatch => bindActionCreators({ cancelCurrentOperation: trza.cancelCurrentOperation, submitPin: trza.submitPin, submitPassPhrase: trza.submitPassPhrase, + submitWord: trza.submitWord, + togglePinProtection: trza.togglePinProtection, + togglePassPhraseProtection: trza.togglePassPhraseProtection, + changeToDecredHomeScreen: trza.changeToDecredHomeScreen, + changeLabel: trza.changeLabel, + wipeDevice: trza.wipeDevice, + recoverDevice: trza.recoverDevice, + initDevice: trza.initDevice, + updateFirmware: trza.updateFirmware, }, dispatch); export default connect(mapStateToProps, mapDispatchToProps); diff --git a/app/containers/Wallet.js b/app/containers/Wallet.js index fc3640517b..43257d6199 100644 --- a/app/containers/Wallet.js +++ b/app/containers/Wallet.js @@ -14,6 +14,7 @@ import TransactionPage from "components/views/TransactionPage"; import TicketsPage from "components/views/TicketsPage"; import TutorialsPage from "components/views/TutorialsPage"; import GovernancePage from "components/views/GovernancePage"; +import TrezorPage from "components/views/TrezorPage"; import SideBar from "components/SideBar"; import { BlurableContainer } from "layout"; import { walletContainer, theming } from "connectors"; @@ -44,6 +45,7 @@ class Wallet extends React.Component { + diff --git a/app/helpers/trezor.js b/app/helpers/trezor.js new file mode 100644 index 0000000000..4d97efa979 --- /dev/null +++ b/app/helpers/trezor.js @@ -0,0 +1,209 @@ +export const model1_decred_homescreen = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007ffe003ff00000000000000000000007fffc007fe0000000000000000000001ffff800ffc0000000000000000000003ffff001ff8000000000000000000000ffffe003ff0000000000000000000001ffffc007fe0000000000000000000003ffff800ffc0000000000000000000003ffff001ff80000000000000000000007fffe003ff00000000000000000000007ff00007fe0000000000000000000000ffc0000ffc0000000000000000000000ff80001fff0000000000000000000001ff00003fffc000000000000000000001ff00007ffff000000000000000000001fe0000fffff800000000000000000001fe0001fffff800000000000000000001fe0003fffffc00000000000000000001fe0000003ffe00000000000000000001fe00000007fe00000000000000000001fe00000003ff00000000000000000001fe00000001ff00000000000000000001ff00000001ff00000000000000000001ff80000000ff00000000000000000000ffc0000000ff00000000000000000000ffe0000000ff800000000000000000007ffc000000ff800000000000000000007fffff8000ff800000000000000000003fffff0000ff000000000000000000001ffffe0000ff000000000000000000000ffffc0001ff0000000000000000000003fff80001ff0000000000000000000001fff00003ff00000000000000000000007fe00007fe0000000000000000000000ffc0001ffe0000000000000000000001ff800ffffc0000000000000000000003ff001ffff80000000000000000000007fe003ffff8000000000000000000000ffc007ffff0000000000000000000001ff800ffffe0000000000000000000003ff001ffff80000000000000000000007fe003ffff0000000000000000000000ffc007fff80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + +export const word_list = [ + "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", + "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", + "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", + "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", + "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", + "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", + "always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", + "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", + "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", + "area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", + "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", + "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", + "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", + "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", + "bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", + "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", + "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", + "best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", + "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", + "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", + "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", + "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", + "breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", + "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", + "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", + "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", + "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", + "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", + "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", + "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", + "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", + "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", + "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", + "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", + "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", + "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", + "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", + "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", + "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", + "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", + "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", + "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", + "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", + "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", + "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", + "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", + "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", + "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", + "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", + "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", + "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", + "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", + "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", + "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", + "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", + "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", + "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", + "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", + "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", + "endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", + "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", + "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", + "eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", + "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", + "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", + "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", + "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", + "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", + "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", + "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", + "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", + "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", + "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", + "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", + "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", + "fruit", "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", + "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", + "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", + "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", + "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", + "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", + "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", + "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", + "guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", + "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", + "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", + "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", + "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", + "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", + "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", + "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", + "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", + "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", + "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", + "intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", + "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", + "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", + "junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", + "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", + "knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", + "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", + "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", + "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", + "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", + "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", + "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", + "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", + "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", + "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", + "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", + "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", + "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", + "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", + "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", + "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", + "more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", + "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", + "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", + "near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", + "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", + "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", + "nuclear", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", + "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", + "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", + "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", + "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", + "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", + "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", + "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", + "pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", + "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", + "phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", + "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", + "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", + "pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", + "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", + "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", + "prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", + "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", + "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", + "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", + "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", + "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", + "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", + "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", + "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", + "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", + "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", + "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", + "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", + "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", + "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", + "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", + "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", + "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", + "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", + "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", + "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", + "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", + "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", + "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", + "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", + "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", + "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", + "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", + "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", + "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", + "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", + "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", + "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", + "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", + "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", + "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", + "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", + "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", + "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", + "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", + "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", + "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", + "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", + "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", + "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", + "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", + "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", + "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", + "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", + "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", + "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", + "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", + "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", + "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", + "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", + "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", + "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", + "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", + "video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", + "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", + "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", + "wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", + "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", + "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", + "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", + "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", + "worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", + "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo", +]; diff --git a/app/i18n/docs/en/Warnings/TrezorFirmwareUpdate.md b/app/i18n/docs/en/Warnings/TrezorFirmwareUpdate.md new file mode 100644 index 0000000000..f78fe840a8 --- /dev/null +++ b/app/i18n/docs/en/Warnings/TrezorFirmwareUpdate.md @@ -0,0 +1,8 @@ +**Warning!** Only use official firmware distributed by SatoshiLabs (Trezor +manufacturer) or other very trusted sources. Using a non standard firmware +or one obtained from an unreputable individual or company might result in **loss +or stealing of funds**. + +Also, ensure you have access to your seed, that it is valid and that it +corresponds to the funds in this wallet. Firmware updates might cause all data +in the device to be wiped, requiring a recover operation afterwards. diff --git a/app/i18n/docs/en/Warnings/TrezorWipe.md b/app/i18n/docs/en/Warnings/TrezorWipe.md new file mode 100644 index 0000000000..8b2766d4f8 --- /dev/null +++ b/app/i18n/docs/en/Warnings/TrezorWipe.md @@ -0,0 +1,8 @@ +**Warning!** Please ensure you have the seed for your trezor device stored in a +secure and accessible location, that it is valid, and that it is the correct key +for this wallet before performing a device wipe and recovery. + +Failure to do so will result in **loss of funds**. + +Please also note that Decrediton only supports recovering 24 word (256 bit) +seeds and that the words for trezor and dcrwallet imports are *different*. diff --git a/app/i18n/docs/en/index.js b/app/i18n/docs/en/index.js index 84245a1885..adf045f422 100644 --- a/app/i18n/docs/en/index.js +++ b/app/i18n/docs/en/index.js @@ -10,6 +10,8 @@ export { default as ScriptNotRedeemableInfo } from "./InfoModals/ScriptNotRedeem export { default as SeedCopyWarning } from "./Warnings/SeedCopy.md"; export { default as WalletCreationWarning } from "./Warnings/WalletCreation.md"; +export { default as TrezorWipeWarning } from "./Warnings/TrezorWipe.md"; +export { default as TrezorFirmwareUpdateWarning } from "./Warnings/TrezorFirmwareUpdate.md"; export { default as GetStartedTutorialPage01 } from "./GetStarted/TutorialPage01.md"; export { default as GetStartedTutorialPage02 } from "./GetStarted/TutorialPage02.md"; diff --git a/app/index.js b/app/index.js index b759afc915..c390cc0546 100644 --- a/app/index.js +++ b/app/index.js @@ -391,12 +391,15 @@ var initialState = { deviceList: null, transportError: false, device: null, + performingOperation: false, waitingForPin: false, waitingForPassPhrase: false, + waitingForWord: false, pinCallBack: null, passPhraseCallBack: null, pinMessage: null, passPhraseMessage: null, + wordCallBack: null, }, locales: locales }; diff --git a/app/reducers/snackbar.js b/app/reducers/snackbar.js index 67c0776e0d..17ad013885 100644 --- a/app/reducers/snackbar.js +++ b/app/reducers/snackbar.js @@ -45,6 +45,16 @@ import { GETWALLETSEEDSVC_FAILED, SPVSYNC_FAILED, } from "actions/WalletLoaderActions"; +import { + TRZ_TOGGLEPINPROTECTION_SUCCESS, TRZ_TOGGLEPINPROTECTION_FAILED, + TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS, TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED, + TRZ_CHANGEHOMESCREEN_SUCCESS, TRZ_CHANGEHOMESCREEN_FAILED, + TRZ_CHANGELABEL_SUCCESS, TRZ_CHANGELABEL_FAILED, + TRZ_WIPEDEVICE_SUCCESS, TRZ_WIPEDEVICE_FAILED, + TRZ_RECOVERDEVICE_SUCCESS, TRZ_RECOVERDEVICE_FAILED, + TRZ_INITDEVICE_SUCCESS, TRZ_INITDEVICE_FAILED, + TRZ_UPDATEFIRMWARE_SUCCESS, TRZ_UPDATEFIRMWARE_FAILED, +} from "actions/TrezorActions"; import { GETACTIVEVOTE_FAILED, GETVETTED_FAILED, GETPROPOSAL_FAILED, @@ -225,6 +235,46 @@ const messages = defineMessages({ WRONG_PASSPHRASE_MSG: { id: "errors.wrongPassphrase", defaultMessage: "Wrong private passphrase entered. Please verify you have typed the correct private passphrase for the wallet." + }, + TRZ_TOGGLEPINPROTECTION_SUCCESS_ENABLED: { + id: "trezor.pinProtectionSuccess.enabled", + defaultMessage: "Pin protection has been enabled in trezor '{label}'" + }, + TRZ_TOGGLEPINPROTECTION_SUCCESS_DISABLED: { + id: "trezor.pinProtectionSuccess.disabled", + defaultMessage: "Pin protection has been disabled in trezor '{label}'" + }, + TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS_ENABLED: { + id: "trezor.passphraseProtectionSuccess.enabled", + defaultMessage: "Passphrase protection has been enabled in trezor '{label}'" + }, + TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS_DISABLED: { + id: "trezor.passphraseProtectionSuccess.disabled", + defaultMessage: "Passphrase protection has been disabled in trezor '{label}'" + }, + TRZ_CHANGEHOMESCREEN_SUCCESS: { + id: "trezor.changeHomeScreen.success", + defaultMessage: "Trezor home screen successfully changed" + }, + TRZ_CHANGELABEL_SUCCESS: { + id: "trezor.changeLabel.success", + defaultMessage: "Changed label on selected trezor to '{label}'" + }, + TRZ_WIPEDEVICE_SUCCESS: { + id: "trezor.wipeDevice.success", + defaultMessage: "Trezor device wiped" + }, + TRZ_RECOVERDEVICE_SUCCESS: { + id: "trezor.recoverDevice.success", + defaultMessage: "Trezor device recovered" + }, + TRZ_INITDEVICE_SUCCESS: { + id: "trezor.initDevice.success", + defaultMessage: "Trezor device initialized with new seed" + }, + TRZ_UPDATEFIRMWARE_SUCCESS: { + id: "trezor.updateFirmware.success", + defaultMessage: "Firmware updated on trezor device" } }); @@ -280,6 +330,11 @@ export default function snackbar(state = {}, action) { case PURCHASETICKETS_SUCCESS: case ADDCUSTOMSTAKEPOOL_SUCCESS: case PUBLISHTX_SUCCESS: + case TRZ_CHANGEHOMESCREEN_SUCCESS: + case TRZ_WIPEDEVICE_SUCCESS: + case TRZ_RECOVERDEVICE_SUCCESS: + case TRZ_INITDEVICE_SUCCESS: + case TRZ_UPDATEFIRMWARE_SUCCESS: type = "Success"; message = messages[action.type] || messages.defaultSuccessMessage; @@ -294,6 +349,11 @@ export default function snackbar(state = {}, action) { case PUBLISHTX_SUCCESS: values = { hash: action.hash }; break; + case TRZ_TOGGLEPINPROTECTION_SUCCESS: + case TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS: + case TRZ_CHANGELABEL_SUCCESS: + values = { label: action.deviceLabel }; + break; } break; @@ -330,6 +390,14 @@ export default function snackbar(state = {}, action) { case UPDATEVOTECHOICE_FAILED: case GETACCOUNTEXTENDEDKEY_FAILED: case STARTTICKETBUYERV2_FAILED: + case TRZ_TOGGLEPINPROTECTION_FAILED: + case TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED: + case TRZ_CHANGEHOMESCREEN_FAILED: + case TRZ_CHANGELABEL_FAILED: + case TRZ_WIPEDEVICE_FAILED: + case TRZ_RECOVERDEVICE_FAILED: + case TRZ_INITDEVICE_FAILED: + case TRZ_UPDATEFIRMWARE_FAILED: if (action.error && String(action.error).indexOf("wallet.Unlock: invalid passphrase:: secretkey.DeriveKey") > -1) { // intercepting all wrong passphrase errors, independently of which error // state was triggered. Not terribly pretty. diff --git a/app/reducers/trezor.js b/app/reducers/trezor.js index 3e5458569a..f3ab5005fb 100644 --- a/app/reducers/trezor.js +++ b/app/reducers/trezor.js @@ -4,8 +4,20 @@ import { TRZ_SELECTEDDEVICE_CHANGED, TRZ_PIN_REQUESTED, TRZ_PIN_ENTERED, TRZ_PIN_CANCELED, TRZ_PASSPHRASE_REQUESTED, TRZ_PASSPHRASE_ENTERED, TRZ_PASSPHRASE_CANCELED, + TRZ_WORD_REQUESTED, TRZ_WORD_ENTERED, TRZ_WORD_CANCELED, TRZ_CANCELOPERATION_SUCCESS, + TRZ_TOGGLEPINPROTECTION_ATTEMPT, TRZ_TOGGLEPINPROTECTION_FAILED, TRZ_TOGGLEPINPROTECTION_SUCCESS, + TRZ_TOGGLEPASSPHRASEPROTECTION_ATTEMPT, TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED, TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS, + TRZ_CHANGEHOMESCREEN_ATTEMPT, TRZ_CHANGEHOMESCREEN_FAILED, TRZ_CHANGEHOMESCREEN_SUCCESS, + TRZ_CHANGELABEL_ATTEMPT, TRZ_CHANGELABEL_FAILED, TRZ_CHANGELABEL_SUCCESS, + TRZ_WIPEDEVICE_ATTEMPT, TRZ_WIPEDEVICE_FAILED, TRZ_WIPEDEVICE_SUCCESS, + TRZ_RECOVERDEVICE_ATTEMPT, TRZ_RECOVERDEVICE_FAILED, TRZ_RECOVERDEVICE_SUCCESS, + TRZ_INITDEVICE_ATTEMPT, TRZ_INITDEVICE_FAILED, TRZ_INITDEVICE_SUCCESS, + TRZ_UPDATEFIRMWARE_ATTEMPT, TRZ_UPDATEFIRMWARE_FAILED, TRZ_UPDATEFIRMWARE_SUCCESS, } from "actions/TrezorActions"; +import { + SIGNTX_ATTEMPT, SIGNTX_FAILED, SIGNTX_SUCCESS +} from "actions/ControlActions"; export default function trezor(state = {}, action) { switch (action.type) { @@ -29,6 +41,7 @@ export default function trezor(state = {}, action) { deviceList: null, transportError: action.error, device: null, + performingOperation: false, }; case TRZ_SELECTEDDEVICE_CHANGED: return { ...state, @@ -39,6 +52,7 @@ export default function trezor(state = {}, action) { waitingForPin: true, pinCallBack: action.pinCallBack, pinMessage: action.pinMessage, + performingOperation: true, }; case TRZ_PIN_CANCELED: case TRZ_PIN_ENTERED: @@ -46,25 +60,77 @@ export default function trezor(state = {}, action) { waitingForPin: false, pinCallBack: null, pinMessage: null, + performingOperation: false, }; case TRZ_PASSPHRASE_REQUESTED: return { ...state, waitingForPassPhrase: true, passPhraseCallBack: action.passPhraseCallBack, + performingOperation: true, }; case TRZ_PASSPHRASE_CANCELED: case TRZ_PASSPHRASE_ENTERED: return { ...state, waitingForPassPhrase: false, passPhraseCallBack: null, + performingOperation: false, + }; + case TRZ_WORD_REQUESTED: + return { ...state, + waitingForWord: true, + wordCallBack: action.wordCallBack, + performingOperation: true, + }; + case TRZ_WORD_CANCELED: + case TRZ_WORD_ENTERED: + return { ...state, + waitingForWord: false, + wordCallBack: null, + performingOperation: false, }; case TRZ_CANCELOPERATION_SUCCESS: return { ...state, waitingForPin: false, pinCallBack: null, pinMessage: null, + wordCallBack: null, waitingForPassPhrase: false, passPhraseCallBack: null, + performingOperation: false, + waitingForWord: false, + }; + case SIGNTX_ATTEMPT: + case TRZ_TOGGLEPINPROTECTION_ATTEMPT: + case TRZ_TOGGLEPASSPHRASEPROTECTION_ATTEMPT: + case TRZ_CHANGEHOMESCREEN_ATTEMPT: + case TRZ_CHANGELABEL_ATTEMPT: + case TRZ_WIPEDEVICE_ATTEMPT: + case TRZ_RECOVERDEVICE_ATTEMPT: + case TRZ_INITDEVICE_ATTEMPT: + case TRZ_UPDATEFIRMWARE_ATTEMPT: + return { ...state, + performingOperation: true, + }; + case SIGNTX_FAILED: + case SIGNTX_SUCCESS: + case TRZ_TOGGLEPINPROTECTION_FAILED: + case TRZ_TOGGLEPINPROTECTION_SUCCESS: + case TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED: + case TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS: + case TRZ_CHANGEHOMESCREEN_FAILED: + case TRZ_CHANGEHOMESCREEN_SUCCESS: + case TRZ_CHANGELABEL_FAILED: + case TRZ_CHANGELABEL_SUCCESS: + case TRZ_WIPEDEVICE_FAILED: + case TRZ_WIPEDEVICE_SUCCESS: + case TRZ_RECOVERDEVICE_FAILED: + case TRZ_RECOVERDEVICE_SUCCESS: + case TRZ_INITDEVICE_FAILED: + case TRZ_INITDEVICE_SUCCESS: + case TRZ_UPDATEFIRMWARE_FAILED: + case TRZ_UPDATEFIRMWARE_SUCCESS: + return { ...state, + performingOperation: false, }; default: return state; diff --git a/app/selectors.js b/app/selectors.js index 3ebeec9de4..d30064e5f7 100644 --- a/app/selectors.js +++ b/app/selectors.js @@ -979,4 +979,6 @@ export const initialProposalLoading = createSelector( export const trezorWaitingForPin = get([ "trezor", "waitingForPin" ]); export const trezorWaitingForPassPhrase = get([ "trezor", "waitingForPassPhrase" ]); +export const trezorWaitingForWord = get([ "trezor", "waitingForWord" ]); +export const trezorPerformingOperation = get([ "trezor", "performingOperation" ]); export const trezorDevice = get([ "trezor", "device" ]); diff --git a/app/style/Header.less b/app/style/Header.less index 32d1d7fecb..e3cb132ff4 100644 --- a/app/style/Header.less +++ b/app/style/Header.less @@ -70,6 +70,7 @@ &.tickets { background-image: @header-tickets; } &.transactions { background-image: @header-transactions; } &.governance { background-image: @header-governance; } + &.trezor { background-image: @menu-trezor-active; } &.tx-detail-icon-ticket { background-image: @ticket-live-icon; } &.tx-detail-icon-vote { background-image: @ticket-voted-icon; } diff --git a/app/style/Icons.less b/app/style/Icons.less index 284535fc0c..1c998288fd 100644 --- a/app/style/Icons.less +++ b/app/style/Icons.less @@ -132,6 +132,9 @@ @menu-settings-default: url('@{icon-root}/settings-default.png'); @menu-transactions-active: url('@{icon-root}/transactions-active.png'); @menu-transactions-default: url('@{icon-root}/transactions-default.png'); +@menu-trezor-active: url('@{icon-root}/trezor-active.png'); +@menu-trezor-default: url('@{icon-root}/trezor-default.png'); +@menu-trezor-hover: url('@{icon-root}/trezor-hover.png'); @menu-tickets-active: url('@{icon-root}/tickets-active.png'); @menu-tickets-default: url('@{icon-root}/tickets-default.png'); @menu-logo: url('@{icon-root}/menu-logo.svg'); diff --git a/app/style/MiscComponents.less b/app/style/MiscComponents.less index 0c445f7c48..ba77dcf246 100644 --- a/app/style/MiscComponents.less +++ b/app/style/MiscComponents.less @@ -368,6 +368,18 @@ background-image: @menu-animation-governance; } +.menu-link-active.menu-link.trezorIcon { + background-image: @menu-trezor-active; +} + +.menu-link.trezorIcon:not(.menu-link-active):hover { + background-image: @menu-trezor-hover; +} + +.menu-link.trezorIcon { + background-image: @menu-trezor-default; +} + .menu-caret { position: absolute; height: 52px; diff --git a/app/style/Trezor.less b/app/style/Trezor.less index d1a0e319eb..bf0890d25b 100644 --- a/app/style/Trezor.less +++ b/app/style/Trezor.less @@ -22,6 +22,38 @@ } } +.trezor-word-modal { + h1 { + margin: 0; + } +} + + .trezor-label { font-family: @font-family-monospaced; } + +.trezor-config-regular-buttons .button { + margin-right: 1em; + margin-top: 1em; +} + +.trezor-config-accordion { + border-bottom: 1px solid #dadfe2; + border-radius: 4px; + padding: 1.5em 1em 1em; + background-color: #fff; + + .vertical-accordion-header { + font-size: 22px; + margin-bottom: 0.5em; + } + + .input-and-unit { + margin-bottom: 0.5em; + } +} + +.trezor-word-select { + min-height: 10em; +} diff --git a/app/style/icons/trezor-active.png b/app/style/icons/trezor-active.png new file mode 100644 index 0000000000000000000000000000000000000000..0cf0512a80d34850811678ede4c19bca1bd05d1c GIT binary patch literal 2335 zcma)8c{CLI7oRbju}v~5ldTCU$!mB#O3YN28Nyhj#?F(am@FeyMiN5C8d+Lwm1RQq zWiW`$=<&!p#`YxJ$TEcHH=TFR@4SBJ{qZ~B@BQBUz4v@SpZh)Mo_lT#&eRYpBq;;{ z0H9cmo&`_8{~-ck-dSQ>n!{6I7h^*`0Qbjw)>xR#^9cH3?EC=$;e$T}2*}KW^PG?X ztcgBk3L+$a0QQ-z_Za{XD8}ktunHbo8h5;IPXtm|eJ?*M!r37uksad8HfI9yGG6Z8 zhmY!UDD#7T1Fg36vRNv|_7K+5_+faru4$0Jk8CdDxmMbB)AP`~*=XaI6*)u^ITH z#_k5om%2il0Q&O6U(7WV)gs>lhlI`p;{IB%^r!lg()$&%HZ75fFZADnhxixymp`w4 zI4jy9ARQ?AJ3?Q;6~b2VGj4HUO0!{pNR!s%C>2oG&hC`6lVd`E5$9kxkVTP?CCvmT z5NZIHuVybdHsvZ#2BP#qF&bXG0>KfpWx9u2RGs?%prLKl5(q3eLBsfVgJej*0zYcJ z{SNh#Uz^r%eB5H3G+-fIQ;I$qgqm!Jh6o&vpH^Z?Q12-R7lTQ@Q0QY3xJee$8QJkVDZABVZqzuH{g=8+nmMO3{=QT=?itb!n@sVesq#Sg~p^aTcNRjl5I9D zjbF(GH`#LH6A}>YVgmkCktHKD_r6jM<)tX0k~B>vs|$Mgt{?SiiCoN0wo_Me(q`irW%o4L zT9glBv_Iqx;D+J3#eXJwZpsFM%#tV4t4FA|<7symg7=pzb!su(EtBn)x9_JaP8wxK zHd5h!ik;>&=4O3wd_7Ei{ROrxdTxhiMjCs<6vK6feYb{WyKJ~wF4lft8UkUl43+b) zX8N)w$$si{oN6*7C}YU7H1tcr^uv)j#4ty?YL;zP+evP0PNouX!Le0Z?XOEim7Q&W&D`#^7t3|IM=n+KkT{YXvn5WH#B79V^i4C&d&*wPKGKQ}W1?Tr zZDJnpe2`lXpc~L78PX$ECE_|b|@-=Tb8ip+vkgPfjiMo!c zBUeA#osu)->iQro=trX65J@BqSng*-M6V9}pt*iWXiv549od0da(v4*z1kpTjGP+0 z;t4aA*k(af5C^zLcOK+pPpVj zXdd(K6UAe}@b8WrXd(&F1aZPIE*Z-oPc zfM$R_5mOOOI=g~lLcTi--tInHFIS;0Y_} zQ&{Bd2%r00TzY-pS(=a6!O z%d!ejm@NMwUuQ6}@W!Fac^^$KJ~MJBTyvKubAh=M-IrdUYbfwCjaWwHk3qe_8zoa9 zTk!0a{FM2t-O||$DA(bQXOHf_NI<+bOj*96!!aN8R_dn;rv%s6l2TA9a!hz#=>4UG zQO*ec7=G5mI>$`7hF1vf`;VKn4*0}uL1wDQzmEj@qxON6ERibb>Z4u9$@yPL>Cimc z=W!LcMwh(bjWx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv`=>|7!Oj5+$OW(*8WpFLe1Ln>~)y}L25*+9f0Q23*5 zPEL)Tw6wj;^t2T&SG-E+r0*|&w#w_k_5LL)x~xD`AmBl-TKj_DzZUCf?az-Ef9|Ky zB+%o~qA1eX;iSVP=qA|Z&~njJ!bz~h!bM3Sk;7HtkwB-z5k;ZK4rdi6!S(1ACSEJ`AYsFGWFT_u<#wWdVD zRZO|>EZFAIQY74Ez|y2}i2Hxz%G}sxZ{}Z=2-JVRWp``(a#5hP5>F!sP^VzG0WdKf zDi`@(7^l2<<)hEnW{5>H3w9a&dp+@~%dhAz10Mllkh(5|m?X=edcM8_!AF+?-PLhO zb=qm}#2E@|ASK7lK%)Mgv_KSS+#fIF6 nSquX~e*2>f(Ev-f3<&}8o)a67yWUuR6(r{A>gTe~DWM4fmueJa literal 0 HcmV?d00001 diff --git a/app/style/icons/trezor-hover.png b/app/style/icons/trezor-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..40f98a9d66438a453d71864eb8ea6573e9fd1bbd GIT binary patch literal 2330 zcma)82{0S#7S19`5mig=B@fk>*48LhTkXV}atW@jY70fRwKRQ6Xwj;rw4te`ih7|` zxfR5c*1m_QwAQ$`SHx0cFE?i1o4I|<%$xr||2cE!%y+(j{(sInkL+yB_;@6E0001= zg}E`BquY<<7?g9CI+x{e6x17KW(+tyTCeJh(m5XPP;*Q;0KhABBtQT;ON`@$-L|kc zf&C8S5fyZPmuBh($@~7xX};IO_0r9QQTGb-WIIl=Z*^< zvLo?>Ronq^bE91bnUQFbhddczs6irp;6?`AKc@TO?CJL{bq-#LJaw@}F8&Lr8^%6n zyDi1Sq6A6KGIy2hbH2{pstLL4+i=Kl=`k{%{bFx&t4Fgk9AS|r|EDc%mrHvkmgHL^ zvn^OI{RQwAsK5(0$;x?y4_IA!B?IihiiN46Oq4gTRn_7JT&z^Z9w}-bi71L6H4hz$Gw^K#SfF zQwFF2+p^=t&BZ@`;nv|EbE!zt1lPf(dIx4+n{^pH)@zIu)3ReC7kmvbb4lGXR!e=| ztCmdiRQ!-h8RoAasjsG+nvWPf1T<7b8U7E+!WdJV1w~Qw14b_&rmt-qkzw5T$BEMcxZhH z&-Se)O>ONMdSz>vBlvb1(B#5`<6E3WShq;6%Tbm_4xz?rVWnQ7b?PZy5#(nVnZ=$Y zHQV>nDj2C??aA@X&ck2dP=+d#5o_6*whUSb&<5C8xc)2+E|z;JseSkaOXve_0VbEd z7{ScV^VSO9dl-z!HPk4*wq<+NJ(7ZGB~l!^Iomj8bWh&nNBCpMCW@neIk{I($H>-1x@%x2 z^5ZD=Q7=n9ugGnyb%Po2Z<@M)@AxQhWXY%LcIt}oYCXs_t<>iI0`@v{Sf7!oH5XY= zSbwnRP4;a;wzzOx!ElL{J39vARqT`6{8hZOCA^s8yIho^owi|6ZbXnuzd{P5Mohd@ zGu!*~%Kmf?AtbEa%IU>=;*&luT;dzzVBavfei=TyPhRln+#nH(){o$d9`|^@W;2W% z#Hlg5_-CBf`3g+W{3EZ7KkMw;n?nUA7C&LIFntkNmARO@d+d&Cn^HLo&$m$<3q{N1*_$dS{GyS& zTLR#xvVMp(qB&GnJi+HDL;XYm(em*2*6H2rX;{$xx>3c|HsuiBjasD)@6k%+Mwf@G2LI6s*?R*t-!h;D?yy_xVLq3%W0hN`KV;$(efzngbw-vKyf2QiHj; z?-HS^sPy;bcr~siSk*~!Q%4YHE@-cq1a>7{Nb+4ouHr}vet{yb8g+ej{`3llv&I@RgOTDy3x7>;~ zQeCQJU%JUIS2&E8jXWz3)Pa3Vf~W}0d+As`$_Ui8&`h=SCCJfH>lq0pzd2wL^?W`F z(i2ursYW*cWRlwxPf`%#^oPbJ>f(lD zF#r6ulzO^(558$ASP|2qQowe+lQN&T^lBZF3&cZFlC6JX`eRm{N`evLh7`7dNkQ`1ONK(v-G?Qu1vh!VW zj0U{ZiVv084j?JaFVqt6t4XyePO@AKWt@vk_gYQYnAMzmR)PG=2M6`_V}9*ViVx$e z)FT#I;C2!y(jssZq#lRLG?8*x?awFTVU3kq_9V@W$d_HJ})cmX4D*9%=F+V zZzO2PJdn2cm064;e0N8^0J#@@qP~`1p-G^wUm7=u)exHnI!~Y?X!jkcFSp2Db&yO> zBv6F&$L`T~JgulNuf#N8em&MMgn8XCK4u`RPbu1CZBE?0i69GgE`4~GGEftzwtz1E g^Hd;DIeq|pqgT00-(7L#Ofdip6C2|)L(iB$0J};$`~Uy| literal 0 HcmV?d00001 From 5097192d96d0f6ff2af3c226a76378ca39373067 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Wed, 11 Jul 2018 10:46:05 -0300 Subject: [PATCH 05/13] Support signing messages on trezor --- app/actions/TrezorActions.js | 45 +++++++++++++++++++- app/components/buttons/SignMessageButton.js | 46 +++++++++++++++------ app/connectors/signMessagePage.js | 3 ++ app/helpers/byteActions.js | 9 ++++ 4 files changed, 88 insertions(+), 15 deletions(-) diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index 975b5a0aac..e3f8f4e5dc 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -4,12 +4,15 @@ import * as wallet from "wallet"; import * as selectors from "selectors"; import fs from "fs"; import { sprintf } from "sprintf-js"; -import { rawHashToHex, rawToHex, hexToRaw } from "helpers"; +import { rawHashToHex, rawToHex, hexToRaw, str2utf8hex, hex2b64 } from "helpers"; import { publishTransactionAttempt } from "./ControlActions"; import { model1_decred_homescreen } from "helpers/trezor"; import { EXTERNALREQUEST_TREZOR_BRIDGE } from "main_dev/externalRequests"; -import { SIGNTX_ATTEMPT, SIGNTX_FAILED, SIGNTX_SUCCESS } from "./ControlActions"; +import { + SIGNTX_ATTEMPT, SIGNTX_FAILED, SIGNTX_SUCCESS, + SIGNMESSAGE_ATTEMPT, SIGNMESSAGE_FAILED, SIGNMESSAGE_SUCCESS +} from "./ControlActions"; const hardeningConstant = 0x80000000; @@ -293,6 +296,44 @@ export const signTransactionAttemptTrezor = (rawUnsigTx, constructTxResponse) => } }; +export const signMessageAttemptTrezor = (address, message) => async (dispatch, getState) => { + + dispatch({ type: SIGNMESSAGE_ATTEMPT }); + + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: SIGNMESSAGE_FAILED }); + return; + } + + const chainParams = selectors.chainParams(getState()); + const { grpc: { walletService } } = getState(); + + try { + const addrValidResp = await wallet.validateAddress(walletService, address); + if (!addrValidResp.getIsValid()) throw "Input has an invalid address " + address; + if (!addrValidResp.getIsMine()) throw "Trezor only supports signing with wallet addresses"; + const addrIndex = addrValidResp.getIndex(); + const addrBranch = addrValidResp.getIsInternal() ? 1 : 0; + const address_n = addressPath(addrIndex, addrBranch, WALLET_ACCOUNT, + chainParams.HDCoinType); + + const signedMsg = await deviceRun(dispatch, getState, device, async session => { + await dispatch(checkTrezorIsDcrwallet(session)); + + return await session.signMessage(address_n, str2utf8hex(message), + chainParams.trezorCoinName, false); + }); + + const signature = hex2b64(signedMsg.message.signature); + dispatch({ getSignMessageSignature: signature, type: SIGNMESSAGE_SUCCESS }); + + } catch (error) { + dispatch({ error, type: SIGNMESSAGE_FAILED }); + } + +}; + // walletTxToBtcjsTx converts a tx decoded by the decred wallet (ie, // returned from the decodeRawTransaction call) into a bitcoinjs-compatible // transaction (to be used in trezor) diff --git a/app/components/buttons/SignMessageButton.js b/app/components/buttons/SignMessageButton.js index 62c9c96dda..7c672577f4 100644 --- a/app/components/buttons/SignMessageButton.js +++ b/app/components/buttons/SignMessageButton.js @@ -1,5 +1,5 @@ import { signMessagePage } from "connectors"; -import { PassphraseModalButton } from "./index"; +import { PassphraseModalButton, KeyBlueButton } from "./index"; import { FormattedMessage as T } from "react-intl"; @autobind @@ -16,19 +16,39 @@ class SignMessageButton extends React.Component { onSubmit && onSubmit(); } + async onAttemptSignMessageTrezor() { + const { address, message, disabled, signMessageAttemptTrezor, onSubmit } = this.props; + if (disabled || !signMessageAttemptTrezor) return; + await signMessageAttemptTrezor(address, message); + onSubmit && onSubmit(); + } + render() { - const { disabled, isSigningMessage, className } = this.props; - - return ( - } - className={className} - disabled={disabled} - onSubmit={this.onAttemptSignMessage} - loading={isSigningMessage} - buttonLabel={} - /> - ); + const { disabled, isSigningMessage, className, isTrezor } = this.props; + + if (isTrezor) { + return ( + + + + ); + } else { + return ( + } + className={className} + disabled={disabled} + onSubmit={this.onAttemptSignMessage} + loading={isSigningMessage} + buttonLabel={} + /> + ); + } } } diff --git a/app/connectors/signMessagePage.js b/app/connectors/signMessagePage.js index 83d8a8eda0..fc07f59c46 100644 --- a/app/connectors/signMessagePage.js +++ b/app/connectors/signMessagePage.js @@ -3,6 +3,7 @@ import { bindActionCreators } from "redux"; import { selectorMap } from "../fp"; import * as sel from "../selectors"; import * as ca from "../actions/ControlActions"; +import * as trza from "../actions/TrezorActions"; const mapStateToProps = selectorMap({ signMessageError: sel.signMessageError, @@ -10,12 +11,14 @@ const mapStateToProps = selectorMap({ isSigningMessage: sel.isSigningMessage, walletService: sel.walletService, isSignMessageDisabled: sel.isSignMessageDisabled, + isTrezor: sel.isTrezor, }); const mapDispatchToProps = dispatch => bindActionCreators({ signMessageAttempt: ca.signMessageAttempt, validateAddress: ca.validateAddress, signMessageCleanStore: ca.signMessageCleanStore, + signMessageAttemptTrezor: trza.signMessageAttemptTrezor, }, dispatch); export default connect(mapStateToProps, mapDispatchToProps); diff --git a/app/helpers/byteActions.js b/app/helpers/byteActions.js index 2e10938bde..cb7a369894 100644 --- a/app/helpers/byteActions.js +++ b/app/helpers/byteActions.js @@ -77,3 +77,12 @@ export function readFileBackward(path, maxSize, end) { } ); } + +// str2utf8hex converts a (js, utf-16) string into (utf-8 encoded) hex. +export function str2utf8hex(str) { + return Buffer.from(str).toString("hex"); +} + +export function hex2b64(hex) { + return new Buffer(hex, "hex").toString("base64"); +} From 69fe85f288bfdf75a7d0d35ad3f939b4d38de0ba Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Tue, 24 Jul 2018 11:17:03 -0300 Subject: [PATCH 06/13] Add trezor wallet creation --- app/actions/ClientActions.js | 2 - app/actions/DaemonActions.js | 2 + app/actions/TrezorActions.js | 70 +++++++++++++- .../modals/PassphraseModal/Modal.js | 3 +- app/components/modals/trezor/Modals.js | 30 +++--- .../modals/trezor/PassPhraseModal.js | 10 +- app/components/modals/trezor/PinModal.js | 7 +- .../trezor/WalletCreationPassPhraseModal.js | 96 +++++++++++++++++++ app/components/modals/trezor/WordModal.js | 8 +- .../views/GetStartedPage/TrezorConfig/Form.js | 27 ++++++ .../views/GetStartedPage/TrezorConfig/Page.js | 33 +++++++ .../GetStartedPage/TrezorConfig/index.js | 94 ++++++++++++++++++ .../WalletSelection/CreateWalletForm.js | 12 +++ .../GetStartedPage/WalletSelection/index.js | 37 +++++-- app/components/views/GetStartedPage/index.js | 20 +++- app/connectors/createWallet.js | 7 ++ app/connectors/trezor.js | 5 + .../TrezorWalletCreationPassPhrase.md | 6 ++ app/i18n/docs/en/index.js | 1 + app/index.js | 6 +- app/main_dev/ipc.js | 3 +- app/reducers/snackbar.js | 12 +++ app/reducers/trezor.js | 18 ++++ app/selectors.js | 2 + app/style/GetStarted.less | 4 + app/style/Trezor.less | 6 ++ app/wallet/daemon.js | 2 +- 27 files changed, 488 insertions(+), 35 deletions(-) create mode 100644 app/components/modals/trezor/WalletCreationPassPhraseModal.js create mode 100644 app/components/views/GetStartedPage/TrezorConfig/Form.js create mode 100644 app/components/views/GetStartedPage/TrezorConfig/Page.js create mode 100644 app/components/views/GetStartedPage/TrezorConfig/index.js create mode 100644 app/i18n/docs/en/Warnings/TrezorWalletCreationPassPhrase.md diff --git a/app/actions/ClientActions.js b/app/actions/ClientActions.js index 0c490b446a..ed423cec31 100644 --- a/app/actions/ClientActions.js +++ b/app/actions/ClientActions.js @@ -17,7 +17,6 @@ import { getVettedProposals } from "./GovernanceActions"; import { rawHashToHex } from "../helpers/byteActions"; import * as da from "../middleware/dcrdataapi"; import { EXTERNALREQUEST_DCRDATA, EXTERNALREQUEST_POLITEIA } from "main_dev/externalRequests"; -import * as trezorActions from "./TrezorActions"; export const goToTransactionHistory = () => (dispatch) => { dispatch(pushHistory("/transactions/history")); @@ -61,7 +60,6 @@ const startWalletServicesTrigger = () => (dispatch, getState) => new Promise((re await dispatch(getStartupWalletInfo()); await dispatch(transactionNtfnsStart()); await dispatch(accountNtfnsStart()); - await dispatch(trezorActions.loadDeviceList()); await dispatch(pushHistory("/home")); resolve(); diff --git a/app/actions/DaemonActions.js b/app/actions/DaemonActions.js index aa703f6d1d..367afcbcfe 100644 --- a/app/actions/DaemonActions.js +++ b/app/actions/DaemonActions.js @@ -12,6 +12,7 @@ import { isTestNet } from "selectors"; import axios from "axios"; import { STANDARD_EXTERNAL_REQUESTS } from "main_dev/externalRequests"; import { DIFF_CONNECTION_ERROR } from "main_dev/constants"; +import { enableTrezor } from "./TrezorActions"; export const DECREDITON_VERSION = "DECREDITON_VERSION"; export const SELECT_LANGUAGE = "SELECT_LANGUAGE"; @@ -294,6 +295,7 @@ export const startWallet = (selectedWallet) => (dispatch, getState) => { dispatch({ type: WALLET_SETTINGS, currencyDisplay, gapLimit }); dispatch({ type: WALLET_STAKEPOOL_SETTINGS, activeStakePoolConfig, selectedStakePool, currentStakePoolConfig }); dispatch({ type: WALLET_LOADER_SETTINGS, discoverAccountsComplete }); + selectedWallet.value.isTrezor && dispatch(enableTrezor()); setTimeout(()=>dispatch(versionCheckAction()), 2000); }) .catch((err) => { diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index e3f8f4e5dc..165a2a83d3 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -7,11 +7,13 @@ import { sprintf } from "sprintf-js"; import { rawHashToHex, rawToHex, hexToRaw, str2utf8hex, hex2b64 } from "helpers"; import { publishTransactionAttempt } from "./ControlActions"; import { model1_decred_homescreen } from "helpers/trezor"; +import { getWalletCfg } from "../config"; import { EXTERNALREQUEST_TREZOR_BRIDGE } from "main_dev/externalRequests"; import { SIGNTX_ATTEMPT, SIGNTX_FAILED, SIGNTX_SUCCESS, - SIGNMESSAGE_ATTEMPT, SIGNMESSAGE_FAILED, SIGNMESSAGE_SUCCESS + SIGNMESSAGE_ATTEMPT, SIGNMESSAGE_FAILED, SIGNMESSAGE_SUCCESS, + VALIDATEMASTERPUBKEY_SUCCESS, } from "./ControlActions"; const hardeningConstant = 0x80000000; @@ -31,15 +33,44 @@ function addressPath(index, branch, account, coinType) { ]; } +function accountPath(account, coinType) { + return [ + (44 | hardeningConstant) >>> 0, // purpose + ((coinType || 0)| hardeningConstant) >>> 0, // coin type + ((account || 0) | hardeningConstant) >>> 0 // account + ]; +} + +export const TRZ_TREZOR_ENABLED = "TRZ_TREZOR_ENABLED"; + +export const enableTrezor = () => (dispatch, getState) => { + const walletName = selectors.getWalletName(getState()); + + if (walletName) { + const config = getWalletCfg(selectors.isTestNet(getState()), walletName); + config.set("trezor", true); + } + + dispatch({ type: TRZ_TREZOR_ENABLED }); + + const { trezor: { deviceList, getDeviceListAttempt } } = getState(); + if (!deviceList && !getDeviceListAttempt) { + dispatch(loadDeviceList()); + } +}; + export const TRZ_LOADDEVICELIST_ATTEMPT = "TRZ_LOADDEVICELIST_ATTEMPT"; export const TRZ_LOADDEVICELIST_FAILED = "TRZ_LOADDEVICELIST_FAILED"; export const TRZ_LOADDEVICELIST_SUCCESS = "TRZ_LOADDEVICELIST_SUCCESS"; export const TRZ_DEVICELISTTRANSPORT_LOST = "TRZ_DEVICELISTTRANSPORT_LOST"; export const TRZ_SELECTEDDEVICE_CHANGED = "TRZ_SELECTEDDEVICE_CHANGED"; +export const TRZ_NOCONNECTEDDEVICE = "TRZ_NOCONNECTEDDEVICE"; export const loadDeviceList = () => (dispatch, getState) => { return new Promise((resolve, reject) => { - if (!getState().trezor.enabled) return; + const { trezor: { getDeviceListAttempt } } = getState(); + if (getDeviceListAttempt) return; + wallet.allowExternalRequest(EXTERNALREQUEST_TREZOR_BRIDGE); dispatch({ type: TRZ_LOADDEVICELIST_ATTEMPT }); @@ -120,6 +151,10 @@ export const selectDevice = (path) => async (dispatch, getState) => { setDeviceListeners(devList.devices[path], dispatch); }; +export const alertNoConnectedDevice = () => dispatch => { + dispatch({ type: TRZ_NOCONNECTEDDEVICE }); +}; + export const TRZ_PIN_REQUESTED = "TRZ_PIN_REQUESTED"; export const TRZ_PIN_ENTERED = "TRZ_PIN_ENTERED"; export const TRZ_PIN_CANCELED = "TRZ_PIN_CANCELED"; @@ -666,3 +701,34 @@ export const updateFirmware = (path) => async (dispatch, getState) => { dispatch({ error, type: TRZ_UPDATEFIRMWARE_FAILED }); } }; + +export const TRZ_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT = "TRZ_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT"; +export const TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED = "TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED"; +export const TRZ_GETWALLETCREATIONMASTERPUBKEY_SUCCESS = "TRZ_GETWALLETCREATIONMASTERPUBKEY_SUCCESS"; + +export const getWalletCreationMasterPubKey = () => async (dispatch, getState) => { + dispatch({ type: TRZ_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT }); + + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED }); + return; + } + + const chainParams = selectors.chainParams(getState()); + + try { + const path = accountPath(WALLET_ACCOUNT, chainParams.HDCoinType); + + const masterPubKey = await deviceRun(dispatch, getState, device, async session => { + const res = await session.getPublicKey(path, chainParams.trezorCoinName, false); + return res.message.xpub; + }); + + dispatch({ type: VALIDATEMASTERPUBKEY_SUCCESS, isWatchOnly: true, masterPubKey }); + dispatch({ type: TRZ_GETWALLETCREATIONMASTERPUBKEY_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED }); + throw error; + } +}; diff --git a/app/components/modals/PassphraseModal/Modal.js b/app/components/modals/PassphraseModal/Modal.js index 6e3cc0c8f5..7cb0416132 100644 --- a/app/components/modals/PassphraseModal/Modal.js +++ b/app/components/modals/PassphraseModal/Modal.js @@ -12,6 +12,7 @@ const propTypes = { const StandardPassphraseModal = (props) => { const { + modalClassName, show, modalDescription, modalTitle, @@ -29,7 +30,7 @@ const StandardPassphraseModal = (props) => { />; return ( - +
{modalTitle ? modalTitle : diff --git a/app/components/modals/trezor/Modals.js b/app/components/modals/trezor/Modals.js index c38896eaea..67bef119bf 100644 --- a/app/components/modals/trezor/Modals.js +++ b/app/components/modals/trezor/Modals.js @@ -1,6 +1,7 @@ import { trezor } from "connectors"; import PinModal from "./PinModal"; import PassPhraseModal from "./PassPhraseModal"; +import WalletCreationPassPhraseModal from "./WalletCreationPassPhraseModal"; import WordModal from "./WordModal"; import "style/Trezor.less"; @@ -15,24 +16,27 @@ class TrezorModals extends React.Component { } render() { + let Component = null; + if (this.props.waitingForPin) { - return ; + Component = PinModal; } else if (this.props.waitingForPassPhrase) { - return ; + if (this.props.walletCreationMasterPubkeyAttempt) { + Component = WalletCreationPassPhraseModal; + } else { + Component = PassPhraseModal; + } } else if (this.props.waitingForWord) { - return ; - } else { - return null; - } + /> + ); } } diff --git a/app/components/modals/trezor/PassPhraseModal.js b/app/components/modals/trezor/PassPhraseModal.js index 8825143411..37e8d4e808 100644 --- a/app/components/modals/trezor/PassPhraseModal.js +++ b/app/components/modals/trezor/PassPhraseModal.js @@ -8,7 +8,6 @@ class TrezorPassphraseModal extends React.Component { } onSubmit(passPhrase) { - console.log("gonna submit", passPhrase); this.props.submitPassPhrase(passPhrase); } @@ -18,11 +17,16 @@ class TrezorPassphraseModal extends React.Component { const trezorLabel = this.props.device ? this.props.device.features.label : ""; + const className = [ + "trezor-passphrase-modal", + this.props.isGetStarted ? "get-started" : "" + ].join(" "); + return ( } - className="trezor-passphrase-modal" + modalTitle={} + modalClassName={className} modalDescription={

; const trezorLabel = this.props.device ? this.props.device.features.label : ""; + const className = [ + "passphrase-modal", + "trezor-pin-modal", + this.props.isGetStarted ? "get-started" : "" + ].join(" "); return ( - +

'{trezorLabel}' }} />

diff --git a/app/components/modals/trezor/WalletCreationPassPhraseModal.js b/app/components/modals/trezor/WalletCreationPassPhraseModal.js new file mode 100644 index 0000000000..ae1ab9f205 --- /dev/null +++ b/app/components/modals/trezor/WalletCreationPassPhraseModal.js @@ -0,0 +1,96 @@ +import Modal from "../Modal"; +import { FormattedMessage as T } from "react-intl"; +import { Documentation } from "shared"; +import { PasswordInput, PassphraseModalField } from "inputs"; +import { ButtonsToolbar } from "../PassphraseModal"; + +@autobind +class TrezorWalletCreationPassphraseModal extends React.Component { + constructor(props) { + super(props); + this.state = { passphraseValue: "", passphraseConfirmValue: "", + submitAttempted: false, mismatchedValues: false }; + } + + componentWillUnmount() { + this.setState = { passphraseValue: "", passphraseConfirmValue: "" }; + } + + onSubmit() { + const { passphraseValue, passphraseConfirmValue } = this.state; + if (passphraseValue != passphraseConfirmValue) { + this.setState({ submitAttempted: true, mismatchedValues: true }); + return; + } + + this.props.submitPassPhrase(passphraseValue); + this.setState({ passphraseValue: "", passphraseConfirmValue: "" }); + } + + onChangePassphraseValue(passphraseValue) { + this.setState({ passphraseValue, submitAttempted: false, + mismatchedValues: false }); + } + + onChangePassphraseConfirmValue(passphraseConfirmValue) { + this.setState({ passphraseConfirmValue, submitAttempted: false, + mismatchedValues: false }); + } + + render() { + const { onCancelModal } = this.props; + const { onSubmit, onChangePassphraseValue, onChangePassphraseConfirmValue } = this; + const { submitAttempted, passphraseValue, passphraseConfirmValue, + mismatchedValues } = this.state; + + const trezorLabel = this.props.device ? this.props.device.features.label : ""; + + const className = [ + "trezor-passphrase-modal", + this.props.isGetStarted ? "get-started" : "" + ].join(" "); + + return ( + +

+

+ '{trezorLabel}' }} /> +

+ + + } + > + onChangePassphraseValue(e.target.value)} + onKeyDownSubmit={onSubmit} + showErrors={submitAttempted} + /> + + + } + > + onChangePassphraseConfirmValue(e.target.value)} + onKeyDownSubmit={onSubmit} + showErrors={submitAttempted} + invalid={mismatchedValues} + invalidMessage={} + /> + + + +
+ ); + } +} + +export default TrezorWalletCreationPassphraseModal; diff --git a/app/components/modals/trezor/WordModal.js b/app/components/modals/trezor/WordModal.js index 2fb0b34e18..b9ce098d09 100644 --- a/app/components/modals/trezor/WordModal.js +++ b/app/components/modals/trezor/WordModal.js @@ -46,8 +46,14 @@ class WordModal extends React.Component { render() { const { onCancelModal, onSubmit, onWordChanged, onSelectKeyDown, getSeedWords } = this; + const className = [ + "passphrase-modal", + "trezor-word-modal", + this.props.isGetStarted ? "get-started" : "" + ].join(" "); + return ( - +

diff --git a/app/components/views/GetStartedPage/TrezorConfig/Form.js b/app/components/views/GetStartedPage/TrezorConfig/Form.js new file mode 100644 index 0000000000..638c9a95c3 --- /dev/null +++ b/app/components/views/GetStartedPage/TrezorConfig/Form.js @@ -0,0 +1,27 @@ +import ChangeLabel from "views/TrezorPage/ChangeLabel"; +import ConfigButtons from "views/TrezorPage/ConfigButtons"; +import RecoveryButtons from "views/TrezorPage/RecoveryButtons"; +import FirmwareUpdate from "views/TrezorPage/FirmwareUpdate"; + +export default ({ + onTogglePinProtection, + onTogglePassPhraseProtection, + onChangeHomeScreen, + onChangeLabel, + onWipeDevice, + onRecoverDevice, + onInitDevice, + onUpdateFirmware, + loading, +}) => ( + + + + + + + + + +); diff --git a/app/components/views/GetStartedPage/TrezorConfig/Page.js b/app/components/views/GetStartedPage/TrezorConfig/Page.js new file mode 100644 index 0000000000..7e21eb9dc5 --- /dev/null +++ b/app/components/views/GetStartedPage/TrezorConfig/Page.js @@ -0,0 +1,33 @@ +import { Tooltip } from "shared"; +import { FormattedMessage as T } from "react-intl"; +import { LoaderBarBottom } from "indicators"; +import { AboutModalButtonInvisible } from "buttons"; + +export default ({ + onHideTrezorConfig, + getCurrentBlockCount, + getNeededBlocks, + getEstimatedTimeLeft, + appVersion, + updateAvailable, + children, +}) => ( +
+
+
+
+ }/> +
+
+ }>
+
+
+ +
+ {children} +
+ + +
+
+); diff --git a/app/components/views/GetStartedPage/TrezorConfig/index.js b/app/components/views/GetStartedPage/TrezorConfig/index.js new file mode 100644 index 0000000000..c9af646956 --- /dev/null +++ b/app/components/views/GetStartedPage/TrezorConfig/index.js @@ -0,0 +1,94 @@ +import { trezor } from "connectors"; +import { FormattedMessage as T } from "react-intl"; +import Form from "./Form"; +import Page from "./Page"; +import "style/Trezor.less"; + +@autobind +class TrezorConfig extends React.Component { + + constructor(props) { + super(props); + props.enableTrezor(); + } + + onTogglePinProtection() { + this.props.togglePinProtection(); + } + + onTogglePassPhraseProtection() { + this.props.togglePassPhraseProtection(); + } + + onChangeHomeScreen() { + this.props.changeToDecredHomeScreen(); + } + + onChangeLabel(newLabel) { + this.props.changeLabel(newLabel); + } + + onWipeDevice() { + this.props.wipeDevice(); + } + + onRecoverDevice() { + this.props.recoverDevice(); + } + + onInitDevice() { + this.props.initDevice(); + } + + onUpdateFirmware(path) { + this.props.updateFirmware(path); + } + + render() { + const { device } = this.props; + let children; + + if (!device) { + children = (
); + } else { + const loading = this.props.performingOperation; + + const { + onTogglePinProtection, + onTogglePassPhraseProtection, + onChangeHomeScreen, + onChangeLabel, + onWipeDevice, + onRecoverDevice, + onInitDevice, + onUpdateFirmware, + } = this; + + children = ( +
+ ); + } + + return ( + + {children} + + ); + } +} + +export default trezor(TrezorConfig); diff --git a/app/components/views/GetStartedPage/WalletSelection/CreateWalletForm.js b/app/components/views/GetStartedPage/WalletSelection/CreateWalletForm.js index c752179619..9973301ef5 100644 --- a/app/components/views/GetStartedPage/WalletSelection/CreateWalletForm.js +++ b/app/components/views/GetStartedPage/WalletSelection/CreateWalletForm.js @@ -37,6 +37,9 @@ const CreateWalletForm = ({ toggleWatchOnly, onChangeCreateWalletMasterPubKey, masterPubKeyError, + isTrezor, + toggleTrezor, + onShowTrezorConfig, }) => { return ( @@ -80,6 +83,15 @@ const CreateWalletForm = ({
+
+
+ +
+
+ + +
+
{isWatchingOnly &&
diff --git a/app/components/views/GetStartedPage/WalletSelection/index.js b/app/components/views/GetStartedPage/WalletSelection/index.js index 44a3de10f1..b4578b673f 100644 --- a/app/components/views/GetStartedPage/WalletSelection/index.js +++ b/app/components/views/GetStartedPage/WalletSelection/index.js @@ -1,6 +1,5 @@ import { WalletSelectionFormBody } from "./Form"; import { createWallet } from "connectors"; - @autobind class WalletSelectionBody extends React.Component { constructor(props) { @@ -20,6 +19,7 @@ class WalletSelectionBody extends React.Component { walletMasterPubKey: "", masterPubKeyError: false, walletNameError: null, + isTrezor: false, }; } componentDidUpdate(prevProps) { @@ -55,6 +55,7 @@ class WalletSelectionBody extends React.Component { onCloseEditWallets, toggleWatchOnly, onChangeCreateWalletMasterPubKey, + toggleTrezor, } = this; const { selectedWallet, @@ -101,6 +102,7 @@ class WalletSelectionBody extends React.Component { walletNameError, maxWalletCount, isSPV, + toggleTrezor, ...this.props, ...this.state, }} @@ -139,7 +141,8 @@ class WalletSelectionBody extends React.Component { } createWallet() { const { newWalletName, createNewWallet, - isWatchingOnly, masterPubKeyError, walletMasterPubKey, walletNameError } = this.state; + isWatchingOnly, masterPubKeyError, walletMasterPubKey, walletNameError, + isTrezor } = this.state; if (newWalletName == "" || walletNameError) { this.setState({ hasFailedAttemptName: true }); return; @@ -150,13 +153,35 @@ class WalletSelectionBody extends React.Component { return; } } - this.props.onCreateWallet( - createNewWallet, - { label: newWalletName, value: { wallet: newWalletName, watchingOnly: isWatchingOnly } }); + if (isTrezor && !this.props.trezorDevice) { + this.props.trezorAlertNoConnectedDevice(); + return; + } + if (isTrezor) { + this.props.trezorGetWalletCreationMasterPubKey() + .then(() => { + this.props.onCreateWallet( + createNewWallet, + { label: newWalletName, value: { wallet: newWalletName, + watchingOnly: true, isTrezor } } ); + }); + } else { + this.props.onCreateWallet( + createNewWallet, + { label: newWalletName, value: { wallet: newWalletName, + watchingOnly: isWatchingOnly, isTrezor } } ); + } } toggleWatchOnly() { const { isWatchingOnly } = this.state; - this.setState({ isWatchingOnly : !isWatchingOnly }); + this.setState({ isWatchingOnly : !isWatchingOnly, isTrezor: false }); + } + toggleTrezor() { + const isTrezor = !this.state.isTrezor; + this.setState({ isTrezor, isWatchingOnly: false }); + if (isTrezor) { + this.props.trezorEnable(); + } } async onChangeCreateWalletMasterPubKey(walletMasterPubKey) { if (walletMasterPubKey === "") { diff --git a/app/components/views/GetStartedPage/index.js b/app/components/views/GetStartedPage/index.js index 937b327f15..ac19ef74c4 100644 --- a/app/components/views/GetStartedPage/index.js +++ b/app/components/views/GetStartedPage/index.js @@ -7,6 +7,7 @@ import ReleaseNotes from "./ReleaseNotes"; import WalletSelectionBody from "./WalletSelection"; import StartRPCBody from "./StartRPC"; import SpvSync from "./SpvSync"; +import TrezorConfig from "./TrezorConfig"; import { AdvancedStartupBody, RemoteAppdataError } from "./AdvancedStartup"; import { RescanWalletBody } from "./RescanWallet/index"; import StakePoolsBody from "./StakePools"; @@ -19,7 +20,7 @@ class GetStartedPage extends React.Component { constructor(props) { super(props); this.state = { showSettings: false, showLogs: false, showReleaseNotes: false, - walletPrivatePassphrase: "" }; + walletPrivatePassphrase: "", showTrezorConfig: false }; } componentDidMount() { @@ -78,6 +79,14 @@ class GetStartedPage extends React.Component { this.setState({ showLogs: false }); } + onShowTrezorConfig() { + this.setState({ showTrezorConfig: true }); + } + + onHideTrezorConfig() { + this.setState({ showTrezorConfig: false }); + } + onSetWalletPrivatePassphrase(walletPrivatePassphrase) { this.setState({ walletPrivatePassphrase }); } @@ -107,6 +116,7 @@ class GetStartedPage extends React.Component { showSettings, showLogs, showReleaseNotes, + showTrezorConfig, ...state } = this.state; @@ -117,7 +127,9 @@ class GetStartedPage extends React.Component { onHideSettings, onShowLogs, onHideLogs, - onSetWalletPrivatePassphrase + onSetWalletPrivatePassphrase, + onShowTrezorConfig, + onHideTrezorConfig, } = this; const blockChainLoading = "blockchain-syncing"; @@ -135,6 +147,8 @@ class GetStartedPage extends React.Component { return ; } else if (showReleaseNotes) { return ; + } else if (showTrezorConfig) { + return ; } else if (isAdvancedDaemon && openForm && !remoteAppdataError && !isPrepared && !getWalletReady && !isSPV) { Form = AdvancedStartupBody; } else if (remoteAppdataError && !isPrepared && !getWalletReady && !isSPV) { @@ -235,6 +249,8 @@ class GetStartedPage extends React.Component { onShowLogs, onHideLogs, onSetWalletPrivatePassphrase, + onShowTrezorConfig, + onHideTrezorConfig, appVersion, updateAvailable, isSPV, diff --git a/app/connectors/createWallet.js b/app/connectors/createWallet.js index e341998e82..280a089a23 100644 --- a/app/connectors/createWallet.js +++ b/app/connectors/createWallet.js @@ -4,6 +4,7 @@ import { selectorMap } from "fp"; import * as sel from "selectors"; import * as wla from "actions/WalletLoaderActions"; import * as ca from "actions/ClientActions"; +import * as trza from "actions/TrezorActions"; const mapStateToProps = selectorMap({ createWalletExisting: sel.createWalletExisting, @@ -14,6 +15,8 @@ const mapStateToProps = selectorMap({ isCreatingWatchingOnly: sel.isWatchingOnly, masterPubKey: sel.masterPubKey, maxWalletCount: sel.maxWalletCount, + trezorDeviceList: sel.trezorDeviceList, + trezorDevice: sel.trezorDevice, }); const mapDispatchToProps = dispatch => bindActionCreators({ @@ -24,6 +27,10 @@ const mapDispatchToProps = dispatch => bindActionCreators({ createWatchOnlyWalletRequest: wla.createWatchOnlyWalletRequest, generateSeed: wla.generateSeed, decodeSeed: wla.decodeSeed, + trezorLoadDeviceList: trza.loadDeviceList, + trezorEnable: trza.enableTrezor, + trezorAlertNoConnectedDevice: trza.alertNoConnectedDevice, + trezorGetWalletCreationMasterPubKey: trza.getWalletCreationMasterPubKey }, dispatch); export default connect(mapStateToProps, mapDispatchToProps); diff --git a/app/connectors/trezor.js b/app/connectors/trezor.js index e4dca84497..d7b6e0adba 100644 --- a/app/connectors/trezor.js +++ b/app/connectors/trezor.js @@ -11,9 +11,13 @@ const mapStateToProps = selectorMap({ waitingForWord: sel.trezorWaitingForWord, device: sel.trezorDevice, performingOperation: sel.trezorPerformingOperation, + isGetStarted: sel.isGetStarted, + deviceList: sel.trezorDeviceList, + walletCreationMasterPubkeyAttempt: sel.trezorWalletCreationMasterPubkeyAttempt, }); const mapDispatchToProps = dispatch => bindActionCreators({ + loadDeviceList: trza.loadDeviceList, cancelCurrentOperation: trza.cancelCurrentOperation, submitPin: trza.submitPin, submitPassPhrase: trza.submitPassPhrase, @@ -26,6 +30,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({ recoverDevice: trza.recoverDevice, initDevice: trza.initDevice, updateFirmware: trza.updateFirmware, + enableTrezor: trza.enableTrezor, }, dispatch); export default connect(mapStateToProps, mapDispatchToProps); diff --git a/app/i18n/docs/en/Warnings/TrezorWalletCreationPassPhrase.md b/app/i18n/docs/en/Warnings/TrezorWalletCreationPassPhrase.md new file mode 100644 index 0000000000..72aec96232 --- /dev/null +++ b/app/i18n/docs/en/Warnings/TrezorWalletCreationPassPhrase.md @@ -0,0 +1,6 @@ +**Warning!** Please note that the trezor wallet passphrase **cannot** be changed +as it is used to derive the addresses to control funds. If you lose or type the +wrong passphrase you may **lose your funds**. + +Please read the [Trezor Manual](https://doc.satoshilabs.com) for further +information about passphrase protected trezor devices. diff --git a/app/i18n/docs/en/index.js b/app/i18n/docs/en/index.js index adf045f422..ad251df2a8 100644 --- a/app/i18n/docs/en/index.js +++ b/app/i18n/docs/en/index.js @@ -12,6 +12,7 @@ export { default as SeedCopyWarning } from "./Warnings/SeedCopy.md"; export { default as WalletCreationWarning } from "./Warnings/WalletCreation.md"; export { default as TrezorWipeWarning } from "./Warnings/TrezorWipe.md"; export { default as TrezorFirmwareUpdateWarning } from "./Warnings/TrezorFirmwareUpdate.md"; +export { default as TrezorWalletCreationPassPhraseWarning } from "./Warnings/TrezorWalletCreationPassPhrase.md"; export { default as GetStartedTutorialPage01 } from "./GetStarted/TutorialPage01.md"; export { default as GetStartedTutorialPage02 } from "./GetStarted/TutorialPage02.md"; diff --git a/app/index.js b/app/index.js index c390cc0546..5241259bf8 100644 --- a/app/index.js +++ b/app/index.js @@ -386,9 +386,10 @@ var initialState = { lastVettedFetchTime: new Date(0), // time when vetted proposals were requested }, trezor: { - enabled: true, - debug: true, + enabled: false, + debug: globalCfg.get("trezor_debug"), deviceList: null, + getDeviceListAttempt: false, transportError: false, device: null, performingOperation: false, @@ -400,6 +401,7 @@ var initialState = { pinMessage: null, passPhraseMessage: null, wordCallBack: null, + walletCreationMasterPubkeyAttempt: false, }, locales: locales }; diff --git a/app/main_dev/ipc.js b/app/main_dev/ipc.js index 167dbe261e..54d6876aa4 100644 --- a/app/main_dev/ipc.js +++ b/app/main_dev/ipc.js @@ -25,9 +25,10 @@ export const getAvailableWallets = (network) => { const cfg = getWalletCfg(isTestNet, wallet); const lastAccess = cfg.get("lastaccess"); const watchingOnly = cfg.get("iswatchonly"); + const isTrezor = cfg.get("trezor"); const walletDbFilePath = getWalletDBPathFromWallets(isTestNet, wallet); const finished = fs.pathExistsSync(walletDbFilePath); - availableWallets.push({ network, wallet, finished, lastAccess, watchingOnly }); + availableWallets.push({ network, wallet, finished, lastAccess, watchingOnly, isTrezor }); }); return availableWallets; diff --git a/app/reducers/snackbar.js b/app/reducers/snackbar.js index 17ad013885..a94009300c 100644 --- a/app/reducers/snackbar.js +++ b/app/reducers/snackbar.js @@ -54,6 +54,8 @@ import { TRZ_RECOVERDEVICE_SUCCESS, TRZ_RECOVERDEVICE_FAILED, TRZ_INITDEVICE_SUCCESS, TRZ_INITDEVICE_FAILED, TRZ_UPDATEFIRMWARE_SUCCESS, TRZ_UPDATEFIRMWARE_FAILED, + TRZ_NOCONNECTEDDEVICE, + TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED, } from "actions/TrezorActions"; import { @@ -275,6 +277,14 @@ const messages = defineMessages({ TRZ_UPDATEFIRMWARE_SUCCESS: { id: "trezor.updateFirmware.success", defaultMessage: "Firmware updated on trezor device" + }, + TRZ_NOCONNECTEDDEVICE: { + id: "trezor.noConnectedDevice", + defaultMessage: "No trezor device connected. Check the device connection and trezor bridge." + }, + TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED: { + id: "trezor.getWalletCreationMasterPubKey.failed", + defaultMessage: "Failed to obtain master extended pubkey from trezor device: {originalError}" } }); @@ -398,6 +408,8 @@ export default function snackbar(state = {}, action) { case TRZ_RECOVERDEVICE_FAILED: case TRZ_INITDEVICE_FAILED: case TRZ_UPDATEFIRMWARE_FAILED: + case TRZ_NOCONNECTEDDEVICE: + case TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED: if (action.error && String(action.error).indexOf("wallet.Unlock: invalid passphrase:: secretkey.DeriveKey") > -1) { // intercepting all wrong passphrase errors, independently of which error // state was triggered. Not terribly pretty. diff --git a/app/reducers/trezor.js b/app/reducers/trezor.js index f3ab5005fb..bed30cb0b0 100644 --- a/app/reducers/trezor.js +++ b/app/reducers/trezor.js @@ -1,4 +1,5 @@ import { + TRZ_TREZOR_ENABLED, TRZ_LOADDEVICELIST_ATTEMPT, TRZ_LOADDEVICELIST_FAILED, TRZ_LOADDEVICELIST_SUCCESS, TRZ_DEVICELISTTRANSPORT_LOST, TRZ_SELECTEDDEVICE_CHANGED, @@ -14,6 +15,7 @@ import { TRZ_RECOVERDEVICE_ATTEMPT, TRZ_RECOVERDEVICE_FAILED, TRZ_RECOVERDEVICE_SUCCESS, TRZ_INITDEVICE_ATTEMPT, TRZ_INITDEVICE_FAILED, TRZ_INITDEVICE_SUCCESS, TRZ_UPDATEFIRMWARE_ATTEMPT, TRZ_UPDATEFIRMWARE_FAILED, TRZ_UPDATEFIRMWARE_SUCCESS, + TRZ_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT, TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED, TRZ_GETWALLETCREATIONMASTERPUBKEY_SUCCESS, } from "actions/TrezorActions"; import { SIGNTX_ATTEMPT, SIGNTX_FAILED, SIGNTX_SUCCESS @@ -21,20 +23,27 @@ import { export default function trezor(state = {}, action) { switch (action.type) { + case TRZ_TREZOR_ENABLED: + return { ...state, + enabled: true, + }; case TRZ_LOADDEVICELIST_ATTEMPT: return { ...state, deviceList: null, transportError: false, device: null, + getDeviceListAttempt: true, }; case TRZ_LOADDEVICELIST_SUCCESS: return { ...state, deviceList: action.deviceList, transportError: false, + getDeviceListAttempt: false, }; case TRZ_LOADDEVICELIST_FAILED: return { ...state, transportError: action.error, + getDeviceListAttempt: false, }; case TRZ_DEVICELISTTRANSPORT_LOST: return { ...state, @@ -99,6 +108,15 @@ export default function trezor(state = {}, action) { performingOperation: false, waitingForWord: false, }; + case TRZ_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT: + return { ...state, + walletCreationMasterPubkeyAttempt: true + }; + case TRZ_GETWALLETCREATIONMASTERPUBKEY_SUCCESS: + case TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED: + return { ...state, + walletCreationMasterPubkeyAttempt: false + }; case SIGNTX_ATTEMPT: case TRZ_TOGGLEPINPROTECTION_ATTEMPT: case TRZ_TOGGLEPASSPHRASEPROTECTION_ATTEMPT: diff --git a/app/selectors.js b/app/selectors.js index d30064e5f7..6996a80cc7 100644 --- a/app/selectors.js +++ b/app/selectors.js @@ -982,3 +982,5 @@ export const trezorWaitingForPassPhrase = get([ "trezor", "waitingForPassPhrase" export const trezorWaitingForWord = get([ "trezor", "waitingForWord" ]); export const trezorPerformingOperation = get([ "trezor", "performingOperation" ]); export const trezorDevice = get([ "trezor", "device" ]); +export const trezorDeviceList = get([ "trezor", "deviceList" ]); +export const trezorWalletCreationMasterPubkeyAttempt = get([ "trezor", "walletCreationMasterPubkeyAttempt" ]); diff --git a/app/style/GetStarted.less b/app/style/GetStarted.less index 220ab0db60..a4a525929c 100644 --- a/app/style/GetStarted.less +++ b/app/style/GetStarted.less @@ -790,3 +790,7 @@ display: none; } } + +.getstarted-trezor-config-sections { + margin-top: 14em; +} diff --git a/app/style/Trezor.less b/app/style/Trezor.less index bf0890d25b..fc51cd6b25 100644 --- a/app/style/Trezor.less +++ b/app/style/Trezor.less @@ -28,6 +28,12 @@ } } +.trezor-pin-modal.get-started, +.trezor-word-modal.get-started, +.trezor-passphrase-modal.get-started { + margin: 10%; + padding: 2em; +} .trezor-label { font-family: @font-family-monospaced; diff --git a/app/wallet/daemon.js b/app/wallet/daemon.js index 4bd8e83d45..616c300b5f 100644 --- a/app/wallet/daemon.js +++ b/app/wallet/daemon.js @@ -139,7 +139,7 @@ export const reloadAllowedExternalRequests = log(() => Promise export const allowExternalRequest = log(requestType => Promise .resolve(ipcRenderer.sendSync("allow-external-request", requestType)) - , "Allow External Request"); +, "Allow External Request"); export const allowStakePoolHost = log(host => Promise .resolve(ipcRenderer.sendSync("allow-stakepool-host", host)) From 8b3df4a7f1b5d9acee60b3dffb5c1868416293e6 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Wed, 10 Oct 2018 10:26:13 -0300 Subject: [PATCH 07/13] Clear device session on close wallet --- app/actions/TrezorActions.js | 20 ++++++++++++++++++++ app/actions/WalletLoaderActions.js | 2 ++ 2 files changed, 22 insertions(+) diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index 165a2a83d3..a7ff2ca2a9 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -236,6 +236,26 @@ export const cancelCurrentOperation = () => async (dispatch, getState) => { dispatch({ type: TRZ_CANCELOPERATION_SUCCESS }); } catch (error) { dispatch({ error, type: TRZ_CANCELOPERATION_FAILED }); + throw error; + } +}; + +export const TRZ_CLEARDEVICESESSION_SUCCESS = "TRZ_CLEARDEVICESESSION_SUCCESS"; +export const TRZ_CLEARDEVICESESSION_FAILED = "TRZ_CLEARDEVICESESSION_FAILED"; + +// Closes a device session, locking the device (if it requires a pin). +export const clearDeviceSession = () => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) return; + + await dispatch(cancelCurrentOperation()); + try { + await deviceRun(dispatch, getState, device, async session => { + await session.clearSession(); + }); + dispatch({ type: TRZ_CLEARDEVICESESSION_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_CLEARDEVICESESSION_FAILED }); } }; diff --git a/app/actions/WalletLoaderActions.js b/app/actions/WalletLoaderActions.js index 16be0abe7c..e47c0b7331 100644 --- a/app/actions/WalletLoaderActions.js +++ b/app/actions/WalletLoaderActions.js @@ -14,6 +14,7 @@ import { isTestNet } from "selectors"; import { SpvSyncRequest, SyncNotificationType, RpcSyncRequest } from "../middleware/walletrpc/api_pb"; import { push as pushHistory } from "react-router-redux"; import { stopNotifcations } from "./NotificationActions"; +import { clearDeviceSession as trezorClearDeviceSession } from "./TrezorActions"; const MAX_RPC_RETRIES = 5; const RPC_RETRY_DELAY = 5000; @@ -182,6 +183,7 @@ export const closeWalletRequest = () => async(dispatch, getState) => { await dispatch(stopNotifcations()); await dispatch(syncCancel()); await dispatch(rescanCancel()); + await dispatch(trezorClearDeviceSession()); await closeWallet(getState().walletLoader.loader); await wallet.stopWallet(); dispatch({ type: CLOSEWALLET_SUCCESS }); From d57249e3f416d19736c0b427d8c6513c8c7fa896 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Tue, 23 Oct 2018 11:17:23 -0300 Subject: [PATCH 08/13] Fixes after rebase --- app/components/views/GetStartedPage/TrezorConfig/Page.js | 7 +++---- app/reducers/snackbar.js | 7 +++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/components/views/GetStartedPage/TrezorConfig/Page.js b/app/components/views/GetStartedPage/TrezorConfig/Page.js index 7e21eb9dc5..3ae488613f 100644 --- a/app/components/views/GetStartedPage/TrezorConfig/Page.js +++ b/app/components/views/GetStartedPage/TrezorConfig/Page.js @@ -1,7 +1,6 @@ import { Tooltip } from "shared"; -import { FormattedMessage as T } from "react-intl"; import { LoaderBarBottom } from "indicators"; -import { AboutModalButtonInvisible } from "buttons"; +import { AboutModalButton, GoBackMsg } from "../messages"; export default ({ onHideTrezorConfig, @@ -16,10 +15,10 @@ export default ({
- }/> +
- }>
+ }>
diff --git a/app/reducers/snackbar.js b/app/reducers/snackbar.js index a94009300c..51ae356c14 100644 --- a/app/reducers/snackbar.js +++ b/app/reducers/snackbar.js @@ -345,6 +345,7 @@ export default function snackbar(state = {}, action) { case TRZ_RECOVERDEVICE_SUCCESS: case TRZ_INITDEVICE_SUCCESS: case TRZ_UPDATEFIRMWARE_SUCCESS: + case TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS: type = "Success"; message = messages[action.type] || messages.defaultSuccessMessage; @@ -435,6 +436,12 @@ export default function snackbar(state = {}, action) { } break; + + case TRZ_TOGGLEPINPROTECTION_SUCCESS: + type = "Success"; + message = messages["TRZ_TOGGLEPINPROTECTION_SUCCESS_" + (action.clearProtection ? "DISABLED" : "ENABLED")]; + values = { label: action.deviceLabel }; + break; } if (!message || !type) { From 2d906b823b55edf9d8786139a0145396ad2adcad Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Tue, 30 Oct 2018 14:22:31 -0300 Subject: [PATCH 09/13] Startup improvements --- app/actions/TrezorActions.js | 9 +- app/components/modals/trezor/PinModal.js | 22 +++-- .../views/GetStartedPage/TrezorConfig/Form.js | 27 ------ .../GetStartedPage/TrezorConfig/index.js | 74 +++-------------- .../views/TrezorPage/ConfigButtons.js | 6 +- .../views/TrezorPage/ConfigSections.js | 83 +++++++++++++++++++ .../views/TrezorPage/NoDevicePage.js | 8 +- app/components/views/TrezorPage/Page.js | 20 +---- app/components/views/TrezorPage/index.js | 80 ++---------------- app/connectors/trezor.js | 2 + app/reducers/trezor.js | 8 ++ 11 files changed, 151 insertions(+), 188 deletions(-) delete mode 100644 app/components/views/GetStartedPage/TrezorConfig/Form.js create mode 100644 app/components/views/TrezorPage/ConfigSections.js diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index a7ff2ca2a9..24a9926039 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -59,6 +59,13 @@ export const enableTrezor = () => (dispatch, getState) => { } }; +export const TRZ_CLEAR_DEVICELIST = "TRZ_CLEAR_DEVICELIST"; + +export const reloadTrezorDeviceList = () => (dispatch) => { + dispatch({ type: TRZ_CLEAR_DEVICELIST }); + dispatch(loadDeviceList()); +}; + export const TRZ_LOADDEVICELIST_ATTEMPT = "TRZ_LOADDEVICELIST_ATTEMPT"; export const TRZ_LOADDEVICELIST_FAILED = "TRZ_LOADDEVICELIST_FAILED"; export const TRZ_LOADDEVICELIST_SUCCESS = "TRZ_LOADDEVICELIST_SUCCESS"; @@ -189,7 +196,7 @@ async function deviceRun(dispatch, getState, device, fn) { const handleError = error => { const { trezor: { waitingForPin, waitingForPassphrase } } = getState(); - console.log("Handle error no deviceRun"); + console.log("Handle error no deviceRun", error); if (waitingForPin) dispatch({ error, type: TRZ_PIN_CANCELED }); if (waitingForPassphrase) dispatch({ error, type: TRZ_PASSPHRASE_CANCELED }); if (error instanceof Error) { diff --git a/app/components/modals/trezor/PinModal.js b/app/components/modals/trezor/PinModal.js index 562496cb7a..cc239ebea3 100644 --- a/app/components/modals/trezor/PinModal.js +++ b/app/components/modals/trezor/PinModal.js @@ -4,6 +4,8 @@ import { PasswordInput } from "inputs"; import { ButtonsToolbar } from "../PassphraseModal"; import { InvisibleButton } from "buttons"; +const PIN_LABELS = "ABCDEFGHI"; + const PinButton = ({ index, label, onClick }) =>
onClick(index)}>{label}
; @@ -31,14 +33,24 @@ class PinModal extends React.Component { this.setState({ currentPin: "" }); } + onChangeCurrentPin(e) { + const txt = (e.target.value || "").toUpperCase().trim(); + let pin = ""; + for (let i = 0; i < txt.length; i++) { + const idx = PIN_LABELS.indexOf(txt[i]); + if (idx > -1) pin = pin + "" + (idx+1); + } + this.setState({ currentPin: pin }); + } + render() { - const { onCancelModal, onSubmit, onPinButtonClick, onClearPin } = this; + const { onCancelModal, onSubmit, onPinButtonClick, onClearPin, + onChangeCurrentPin } = this; - const labels = "ABCDEFGHI"; - const currentPin = this.state.currentPin.split("").map(v => labels[parseInt(v)-1]).join(""); + const currentPin = this.state.currentPin.split("").map(v => PIN_LABELS[parseInt(v)-1]).join(""); const Button = ({ index }) => - ; + ; const trezorLabel = this.props.device ? this.props.device.features.label : ""; const className = [ @@ -69,7 +81,7 @@ class PinModal extends React.Component {
- +
diff --git a/app/components/views/GetStartedPage/TrezorConfig/Form.js b/app/components/views/GetStartedPage/TrezorConfig/Form.js deleted file mode 100644 index 638c9a95c3..0000000000 --- a/app/components/views/GetStartedPage/TrezorConfig/Form.js +++ /dev/null @@ -1,27 +0,0 @@ -import ChangeLabel from "views/TrezorPage/ChangeLabel"; -import ConfigButtons from "views/TrezorPage/ConfigButtons"; -import RecoveryButtons from "views/TrezorPage/RecoveryButtons"; -import FirmwareUpdate from "views/TrezorPage/FirmwareUpdate"; - -export default ({ - onTogglePinProtection, - onTogglePassPhraseProtection, - onChangeHomeScreen, - onChangeLabel, - onWipeDevice, - onRecoverDevice, - onInitDevice, - onUpdateFirmware, - loading, -}) => ( - - - - - - - - - -); diff --git a/app/components/views/GetStartedPage/TrezorConfig/index.js b/app/components/views/GetStartedPage/TrezorConfig/index.js index c9af646956..19f5801826 100644 --- a/app/components/views/GetStartedPage/TrezorConfig/index.js +++ b/app/components/views/GetStartedPage/TrezorConfig/index.js @@ -1,7 +1,8 @@ import { trezor } from "connectors"; import { FormattedMessage as T } from "react-intl"; -import Form from "./Form"; +import ConfigSections from "views/TrezorPage/ConfigSections"; import Page from "./Page"; +import { InvisibleButton } from "buttons"; import "style/Trezor.less"; @autobind @@ -12,36 +13,15 @@ class TrezorConfig extends React.Component { props.enableTrezor(); } - onTogglePinProtection() { - this.props.togglePinProtection(); - } - - onTogglePassPhraseProtection() { - this.props.togglePassPhraseProtection(); - } - - onChangeHomeScreen() { - this.props.changeToDecredHomeScreen(); - } - - onChangeLabel(newLabel) { - this.props.changeLabel(newLabel); - } - - onWipeDevice() { - this.props.wipeDevice(); - } - - onRecoverDevice() { - this.props.recoverDevice(); - } - - onInitDevice() { - this.props.initDevice(); - } - - onUpdateFirmware(path) { - this.props.updateFirmware(path); + renderNoDevice() { + return <> +
+
+ + + +
+ ; } render() { @@ -49,38 +29,10 @@ class TrezorConfig extends React.Component { let children; if (!device) { - children = (
); + children = this.renderNoDevice(); } else { const loading = this.props.performingOperation; - - const { - onTogglePinProtection, - onTogglePassPhraseProtection, - onChangeHomeScreen, - onChangeLabel, - onWipeDevice, - onRecoverDevice, - onInitDevice, - onUpdateFirmware, - } = this; - - children = ( - - ); + children = ; } return ( diff --git a/app/components/views/TrezorPage/ConfigButtons.js b/app/components/views/TrezorPage/ConfigButtons.js index 293979fb25..6661c1d81d 100644 --- a/app/components/views/TrezorPage/ConfigButtons.js +++ b/app/components/views/TrezorPage/ConfigButtons.js @@ -17,7 +17,7 @@ class ConfigButtons extends React.Component { ); const { loading, onTogglePinProtection, onTogglePassPhraseProtection, - onChangeHomeScreen } = this.props; + onChangeHomeScreen, onClearDeviceSession } = this.props; return ( + + + + ); diff --git a/app/components/views/TrezorPage/ConfigSections.js b/app/components/views/TrezorPage/ConfigSections.js new file mode 100644 index 0000000000..f456e9362f --- /dev/null +++ b/app/components/views/TrezorPage/ConfigSections.js @@ -0,0 +1,83 @@ +import ChangeLabel from "./ChangeLabel"; +import ConfigButtons from "./ConfigButtons"; +import RecoveryButtons from "./RecoveryButtons"; +import FirmwareUpdate from "./FirmwareUpdate"; + +@autobind +class TrezorConfigSections extends React.Component { + + constructor(props) { + super(props); + } + + onTogglePinProtection() { + this.props.togglePinProtection(); + } + + onTogglePassPhraseProtection() { + this.props.togglePassPhraseProtection(); + } + + onChangeHomeScreen() { + this.props.changeToDecredHomeScreen(); + } + + onChangeLabel(newLabel) { + this.props.changeLabel(newLabel); + } + + onWipeDevice() { + this.props.wipeDevice(); + } + + onRecoverDevice() { + this.props.recoverDevice(); + } + + onUpdateFirmware(path) { + this.props.updateFirmware(path); + } + + onInitDevice() { + this.props.initDevice(); + } + + onReloadDeviceList() { + this.props.reloadDeviceList(); + } + + onClearDeviceSession() { + this.props.clearDeviceSession(); + } + + render() { + const { + onTogglePinProtection, + onTogglePassPhraseProtection, + onChangeHomeScreen, + onChangeLabel, + onWipeDevice, + onRecoverDevice, + onInitDevice, + onUpdateFirmware, + onClearDeviceSession, + loading, + } = this; + + return ( + <> + + + + + + + + + ); + } + +} + +export default TrezorConfigSections; diff --git a/app/components/views/TrezorPage/NoDevicePage.js b/app/components/views/TrezorPage/NoDevicePage.js index 10ff56cb58..ca4e20bf28 100644 --- a/app/components/views/TrezorPage/NoDevicePage.js +++ b/app/components/views/TrezorPage/NoDevicePage.js @@ -1,11 +1,17 @@ import { FormattedMessage as T } from "react-intl"; import Header from "./Header"; import { StandalonePage } from "layout"; +import { InvisibleButton } from "buttons"; -export default () => ( +export default ({ onReloadDeviceList }) => ( }>
+
+ + + +
); diff --git a/app/components/views/TrezorPage/Page.js b/app/components/views/TrezorPage/Page.js index 7669fe68ef..85362fdf0b 100644 --- a/app/components/views/TrezorPage/Page.js +++ b/app/components/views/TrezorPage/Page.js @@ -1,23 +1,9 @@ import { StandalonePage } from "layout"; +import ConfigSections from "./ConfigSections"; import Header from "./Header"; -import ChangeLabel from "./ChangeLabel"; -import ConfigButtons from "./ConfigButtons"; -import RecoveryButtons from "./RecoveryButtons"; -import FirmwareUpdate from "./FirmwareUpdate"; -export default ({ - onTogglePinProtection, onTogglePassPhraseProtection, onChangeHomeScreen, - onChangeLabel, onWipeDevice, onRecoverDevice, onInitDevice, onUpdateFirmware, - loading, -}) => ( +export default props => ( }> - - - - - - - + ); diff --git a/app/components/views/TrezorPage/index.js b/app/components/views/TrezorPage/index.js index 3c27174e4f..02fca7729a 100644 --- a/app/components/views/TrezorPage/index.js +++ b/app/components/views/TrezorPage/index.js @@ -3,80 +3,10 @@ import Page from "./Page"; import NoDevicePage from "./NoDevicePage"; import "style/Trezor.less"; -@autobind -class TrezorPage extends React.Component { - - constructor(props) { - super(props); - } - - onTogglePinProtection() { - this.props.togglePinProtection(); - } - - onTogglePassPhraseProtection() { - this.props.togglePassPhraseProtection(); - } - - onChangeHomeScreen() { - this.props.changeToDecredHomeScreen(); - } - - onChangeLabel(newLabel) { - this.props.changeLabel(newLabel); - } - - onWipeDevice() { - this.props.wipeDevice(); - } - - onRecoverDevice() { - this.props.recoverDevice(); - } - - onUpdateFirmware(path) { - this.props.updateFirmware(path); - } - - onInitDevice() { - this.props.initDevice(); - } - - render() { - const { device } = this.props; - if (!device) return ; - - const loading = this.props.performingOperation; - - const { - onTogglePinProtection, - onTogglePassPhraseProtection, - onChangeHomeScreen, - onChangeLabel, - onWipeDevice, - onRecoverDevice, - onInitDevice, - onUpdateFirmware, - } = this; - - return ( - - ); - } -} +const TrezorPage = ({ device, reloadDeviceList, ...props }) => ( + !device + ? + : +); export default trezor(TrezorPage); diff --git a/app/connectors/trezor.js b/app/connectors/trezor.js index d7b6e0adba..98ab0b86f1 100644 --- a/app/connectors/trezor.js +++ b/app/connectors/trezor.js @@ -31,6 +31,8 @@ const mapDispatchToProps = dispatch => bindActionCreators({ initDevice: trza.initDevice, updateFirmware: trza.updateFirmware, enableTrezor: trza.enableTrezor, + reloadDeviceList: trza.reloadTrezorDeviceList, + clearDeviceSession: trza.clearDeviceSession, }, dispatch); export default connect(mapStateToProps, mapDispatchToProps); diff --git a/app/reducers/trezor.js b/app/reducers/trezor.js index bed30cb0b0..07dd3b566f 100644 --- a/app/reducers/trezor.js +++ b/app/reducers/trezor.js @@ -16,6 +16,7 @@ import { TRZ_INITDEVICE_ATTEMPT, TRZ_INITDEVICE_FAILED, TRZ_INITDEVICE_SUCCESS, TRZ_UPDATEFIRMWARE_ATTEMPT, TRZ_UPDATEFIRMWARE_FAILED, TRZ_UPDATEFIRMWARE_SUCCESS, TRZ_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT, TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED, TRZ_GETWALLETCREATIONMASTERPUBKEY_SUCCESS, + TRZ_CLEAR_DEVICELIST } from "actions/TrezorActions"; import { SIGNTX_ATTEMPT, SIGNTX_FAILED, SIGNTX_SUCCESS @@ -27,6 +28,13 @@ export default function trezor(state = {}, action) { return { ...state, enabled: true, }; + case TRZ_CLEAR_DEVICELIST: + return { ...state, + deviceList: null, + transportError: false, + device: null, + getDeviceListAttempt: true, + }; case TRZ_LOADDEVICELIST_ATTEMPT: return { ...state, deviceList: null, From 1ed6ec4c6ce50ea5012a697bab4b2716377eb084 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Tue, 30 Oct 2018 16:34:42 -0300 Subject: [PATCH 10/13] Only show console.log with debug on --- app/actions/TrezorActions.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index 24a9926039..b656eda5ba 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -93,7 +93,7 @@ export const loadDeviceList = () => (dispatch, getState) => { let resolvedTransport = false; devList.on("transport", t => { - console.log("transport", t); + debug && console.log("transport", t); if (resolvedTransport) return; resolvedTransport = true; // resolved with success dispatch({ deviceList: devList, type: TRZ_LOADDEVICELIST_SUCCESS }); @@ -101,7 +101,7 @@ export const loadDeviceList = () => (dispatch, getState) => { }); devList.on("error", err => { - console.log("error", err); + debug && console.log("error", err); if (!resolvedTransport && err.message.includes("ECONNREFUSED")) { resolvedTransport = true; // resolved with failure dispatch({ error: err.message, type: TRZ_LOADDEVICELIST_FAILED }); @@ -114,7 +114,7 @@ export const loadDeviceList = () => (dispatch, getState) => { }); devList.on("connect", device => { - console.log("connect", Object.keys(devList.devices), device); + debug && console.log("connect", Object.keys(devList.devices), device); const currentDevice = getState().trezor.device; if (!currentDevice) { // first device connected. Use it. @@ -124,7 +124,7 @@ export const loadDeviceList = () => (dispatch, getState) => { }); devList.on("disconnect", device => { - console.log("disconnect", Object.keys(devList.devices), device); + debug && console.log("disconnect", Object.keys(devList.devices), device); const currentDevice = getState().trezor.device; if (currentDevice && device.originalDescriptor.path === currentDevice.originalDescriptor.path ) { const devicePaths = Object.keys(devList.devices); @@ -141,11 +141,11 @@ export const loadDeviceList = () => (dispatch, getState) => { }); devList.on("connectUnacquired", device => { - console.log("connect unacquired", device); + debug && console.log("connect unacquired", device); }); devList.on("disconnectUnacquired", device => { - console.log("d.catch(error => dispatch({ error, type: SIGNTX_FAILED }));isconnect unacquired", device); + debug && console.log("d.catch(error => dispatch({ error, type: SIGNTX_FAILED }));isconnect unacquired", device); }); }); @@ -195,8 +195,8 @@ function setDeviceListeners(device, dispatch) { async function deviceRun(dispatch, getState, device, fn) { const handleError = error => { - const { trezor: { waitingForPin, waitingForPassphrase } } = getState(); - console.log("Handle error no deviceRun", error); + const { trezor: { waitingForPin, waitingForPassphrase, debug } } = getState(); + debug && console.log("Handle error no deviceRun", error); if (waitingForPin) dispatch({ error, type: TRZ_PIN_CANCELED }); if (waitingForPassphrase) dispatch({ error, type: TRZ_PASSPHRASE_CANCELED }); if (error instanceof Error) { @@ -320,7 +320,7 @@ const checkTrezorIsDcrwallet = (session) => async (dispatch, getState) => { export const signTransactionAttemptTrezor = (rawUnsigTx, constructTxResponse) => async (dispatch, getState) => { dispatch({ type: SIGNTX_ATTEMPT }); - const { grpc: { decodeMessageService, walletService } } = getState(); + const { grpc: { decodeMessageService, walletService }, trezor: { debug } } = getState(); const chainParams = selectors.chainParams(getState()); const device = selectors.trezorDevice(getState()); @@ -329,7 +329,7 @@ export const signTransactionAttemptTrezor = (rawUnsigTx, constructTxResponse) => return; } - console.log("construct tx response", constructTxResponse); + debug && console.log("construct tx response", constructTxResponse); try { const decodedUnsigTxResp = await wallet.decodeTransaction(decodeMessageService, rawUnsigTx); From 0100e88eade3f281c8d01dd964aa5f2fa513d346 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Tue, 30 Oct 2018 17:25:39 -0300 Subject: [PATCH 11/13] style nit --- app/style/Trezor.less | 1 + 1 file changed, 1 insertion(+) diff --git a/app/style/Trezor.less b/app/style/Trezor.less index fc51cd6b25..d2c2161516 100644 --- a/app/style/Trezor.less +++ b/app/style/Trezor.less @@ -14,6 +14,7 @@ .pin-pad-button { padding: 0.5em 0em; border: 1px solid transparent; + cursor: pointer; &:hover { border: 1px solid #6397ff; From 6b1413edd572b9734aaa4856603837c8ad7787e7 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Fri, 30 Nov 2018 10:13:28 -0200 Subject: [PATCH 12/13] Fix verticalAccordion componentDidUpdate --- app/components/shared/VerticalAccordion.js | 27 ++++------------------ 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/app/components/shared/VerticalAccordion.js b/app/components/shared/VerticalAccordion.js index b57d6cc64c..1e3e639cb0 100644 --- a/app/components/shared/VerticalAccordion.js +++ b/app/components/shared/VerticalAccordion.js @@ -11,32 +11,15 @@ class VerticalAccordion extends React.Component { } componentDidUpdate(prevProps) { - let oldChildren, newChildren, newKeys; - let needUpdate = false; - if(prevProps.children) { - oldChildren = prevProps.children; - } - if (this.props.children) { - newChildren = this.props.children; - newKeys = Object.keys(newChildren.props); - } - if (oldChildren && newChildren) { - newKeys.forEach( key => { - if (typeof(newChildren.props[key]) !== "object" && - typeof(newChildren.props[key]) !== "function") { - if (oldChildren.props[key] !== newChildren.props[key]) { - needUpdate = true; - return; - } - } - }); - } + const needUpdate = + (prevProps.show !== this.props.show) || + (prevProps.children !== this.props.children) || + (prevProps.height !== this.props.height); - if (prevProps.show !== this.props.show || needUpdate) { + if (needUpdate) { this.setState({ shownStyles: this.chosenStyles(this.props, this.props.show), }); - this.chosenStyles(this.props, this.props.show); } } From f5cd803a758e78503f26417f5327b9a643d33539 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Fri, 30 Nov 2018 10:20:39 -0200 Subject: [PATCH 13/13] Fix vertical accordion activation --- app/components/SideBar/MenuLinks/index.js | 2 +- .../views/TrezorPage/ChangeLabel.js | 34 +++++++++++-------- .../views/TrezorPage/ConfigButtons.js | 9 ++++- .../views/TrezorPage/FirmwareUpdate.js | 8 ++++- .../views/TrezorPage/RecoveryButtons.js | 7 ++++ 5 files changed, 42 insertions(+), 18 deletions(-) diff --git a/app/components/SideBar/MenuLinks/index.js b/app/components/SideBar/MenuLinks/index.js index c6e2cf6e49..bee0f83a9d 100644 --- a/app/components/SideBar/MenuLinks/index.js +++ b/app/components/SideBar/MenuLinks/index.js @@ -92,7 +92,7 @@ class MenuLinks extends React.Component { return ( - {linkList.map(link => this.getMenuLink(link))} + {this.links.map(link => this.getMenuLink(link))} {caret} ); diff --git a/app/components/views/TrezorPage/ChangeLabel.js b/app/components/views/TrezorPage/ChangeLabel.js index 99c628f9ca..711458e4aa 100644 --- a/app/components/views/TrezorPage/ChangeLabel.js +++ b/app/components/views/TrezorPage/ChangeLabel.js @@ -7,7 +7,7 @@ import { KeyBlueButton } from "buttons"; class ChangeLabel extends React.Component { constructor(props) { super(props); - this.state = { newLabel: "" }; + this.state = { newLabel: "", show: false }; } onChangeLabelClicked() { @@ -18,6 +18,10 @@ class ChangeLabel extends React.Component { this.setState({ newLabel: e.target.value }); } + onToggleAccordion() { + this.setState({ show: !this.state.show }); + } + render() { const changeLabelHeader = ( @@ -32,22 +36,22 @@ class ChangeLabel extends React.Component { - -
-
- -
-
- - - -
-
+
+
+ +
+
+ + + +
); diff --git a/app/components/views/TrezorPage/ConfigButtons.js b/app/components/views/TrezorPage/ConfigButtons.js index 6661c1d81d..ad630f380c 100644 --- a/app/components/views/TrezorPage/ConfigButtons.js +++ b/app/components/views/TrezorPage/ConfigButtons.js @@ -6,6 +6,11 @@ import { KeyBlueButton } from "buttons"; class ConfigButtons extends React.Component { constructor(props) { super(props); + this.state = { show: false }; + } + + onToggleAccordion() { + this.setState({ show: !this.state.show }); } render() { @@ -21,8 +26,10 @@ class ConfigButtons extends React.Component { return ( diff --git a/app/components/views/TrezorPage/FirmwareUpdate.js b/app/components/views/TrezorPage/FirmwareUpdate.js index abd3f8a274..7828c0d4a2 100644 --- a/app/components/views/TrezorPage/FirmwareUpdate.js +++ b/app/components/views/TrezorPage/FirmwareUpdate.js @@ -8,7 +8,7 @@ import { PathBrowseInput } from "inputs"; class FirmwareUpdate extends React.Component { constructor(props) { super(props); - this.state = { path: "" }; + this.state = { path: "", show: false }; } onChangePath(path) { @@ -19,6 +19,10 @@ class FirmwareUpdate extends React.Component { this.props.onUpdateFirmware(this.state.path); } + onToggleAccordion() { + this.setState({ show: !this.state.show }); + } + render() { const header = ( @@ -33,6 +37,8 @@ class FirmwareUpdate extends React.Component {
diff --git a/app/components/views/TrezorPage/RecoveryButtons.js b/app/components/views/TrezorPage/RecoveryButtons.js index 28696cee0a..c95a93bf6d 100644 --- a/app/components/views/TrezorPage/RecoveryButtons.js +++ b/app/components/views/TrezorPage/RecoveryButtons.js @@ -7,6 +7,11 @@ import { Documentation } from "shared"; class RecoveryButtons extends React.Component { constructor(props) { super(props); + this.state = { show: false }; + } + + onToggleAccordion() { + this.setState({ show: !this.state.show }); } render() { @@ -23,6 +28,8 @@ class RecoveryButtons extends React.Component {