diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index 5cf61fd430..8015ebf271 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -1,6 +1,5 @@ import * as wallet from "wallet"; import * as selectors from "selectors"; -import fs from "fs"; import { hexToBytes, str2utf8hex } from "helpers"; import { walletTxToBtcjsTx, @@ -22,6 +21,7 @@ import { SIGNMESSAGE_SUCCESS } from "./ControlActions"; import { getAmountFromTxInputs, getTxFromInputs } from "./TransactionActions"; +import { ipcRenderer } from "electron"; const session = require("connect").default; const { TRANSPORT_EVENT, UI, UI_EVENT, DEVICE_EVENT } = require("connect"); @@ -32,6 +32,7 @@ const AQUIRED = "acquired"; const NOBACKUP = "no-backup"; const TRANSPORT_ERROR = "transport-error"; const TRANSPORT_START = "transport-start"; +const BOOTLOADER_MODE = "bootloader"; let setListeners = false; @@ -56,7 +57,7 @@ export const enableTrezor = () => (dispatch, getState) => { connect()(dispatch, getState); }; -async function initTransport(dispatch, debug) { +export const initTransport = async (session, debug) => { await session.init({ connectSrc: "https://localhost:8088/", env: "web", @@ -71,7 +72,7 @@ async function initTransport(dispatch, debug) { .catch(err => { throw err; }); -} +}; export const TRZ_CONNECT_ATTEMPT = "TRZ_CONNECT_ATTEMPT"; export const TRZ_CONNECT_FAILED = "TRZ_CONNECT_FAILED"; @@ -87,7 +88,7 @@ export const connect = () => async (dispatch, getState) => { wallet.allowExternalRequest(EXTERNALREQUEST_TREZOR_BRIDGE); const debug = getState().trezor.debug; - await initTransport(dispatch, debug) + await initTransport(session, debug) .catch(error => { dispatch({ error, type: TRZ_CONNECT_FAILED }); return; @@ -113,9 +114,12 @@ export const TRZ_NOCONNECTEDDEVICE = "TRZ_NOCONNECTEDDEVICE"; function onChange(dispatch, getState, features) { if (features == null) throw "no features on change"; const currentDevice = selectors.trezorDevice(getState()); - const device = features.id; // No current device handle by connect. if (!currentDevice) return; + let device = features.id; + if (features.mode == BOOTLOADER_MODE) { + device = BOOTLOADER_MODE; + } if (device == currentDevice) return; const deviceLabel = features.label; dispatch({ deviceLabel, device, type: TRZ_SELECTEDDEVICE_CHANGED }); @@ -123,14 +127,17 @@ function onChange(dispatch, getState, features) { function onConnect(dispatch, getState, features) { if (features == null) throw "no features on connect"; - const device = features.id; + let device = features.id; const deviceLabel = features.label; + if (features.mode == BOOTLOADER_MODE) { + device = BOOTLOADER_MODE; + } dispatch({ deviceLabel, device, type: TRZ_LOADDEVICE }); return device; }; function onDisconnect(dispatch, getState, features) { - if (features == null) throw "no features on change"; + if (features == null) throw "no features on disconnect"; const currentDevice = selectors.trezorDevice(getState()); const device = features.id; // If this is not the device we presume is current, ignore. @@ -175,11 +182,12 @@ function setDeviceListeners(dispatch, getState) { break; }; }); + session.on(DEVICE_EVENT, (event) => { const type = event.type; switch (type) { case CHANGE: - if (event.payload.type == AQUIRED) { + if (event.payload && event.payload.type == AQUIRED) { onChange(dispatch, getState, event.payload); } break; @@ -191,6 +199,7 @@ function setDeviceListeners(dispatch, getState) { break; }; }); + // TODO: Trezor needs some time to start listening for the responses to its // requests. Find a better way than static sleeps to accomplish this. session.on(UI_EVENT, async (event) => { @@ -274,9 +283,8 @@ async function deviceRun(dispatch, getState, fn) { if (noDevice(getState)) throw "no trezor device"; const handleError = (error) => { const { - trezor: { waitingForPin, waitingForPassphrase, debug } + trezor: { waitingForPin, waitingForPassphrase } } = 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 }); @@ -289,21 +297,11 @@ async function deviceRun(dispatch, getState, fn) { }; try { - const waitFor = async () => { - try { - return await fn(); - } 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); - } - }; - const res = await waitFor(); + const res = await fn(); if (res && res.error) throw handleError(res.error); return res; - } catch (outerErr) { - throw handleError(outerErr); + } catch (error) { + throw handleError(error); } }; @@ -776,13 +774,22 @@ export const TRZ_UPDATEFIRMWARE_ATTEMPT = "TRZ_UPDATEFIRMWARE_ATTEMPT"; export const TRZ_UPDATEFIRMWARE_FAILED = "TRZ_UPDATEFIRMWARE_FAILED"; export const TRZ_UPDATEFIRMWARE_SUCCESS = "TRZ_UPDATEFIRMWARE_SUCCESS"; +// updateFirmware attempts to update the device's firmware. For some reason, +// possibly the size of the firmware, this action will not complete if called +// from here. We send the firmware to a higher place in the electron hiearchy +// to send it for us. export const updateFirmware = (path) => async (dispatch, getState) => { + // Attempting to update the firmware while already updating will cause the + // trezor to lock up. + const { + trezor: { performingUpdate, device } + } = getState(); + if (performingUpdate) { + console.log("already updating firmware"); + return; + } + dispatch({ type: TRZ_UPDATEFIRMWARE_ATTEMPT }); - // TODO: Allow firmware installation. - dispatch({ error: "firware install currently disabled", type: TRZ_UPDATEFIRMWARE_FAILED }); - // Strange var to fool linter. - const abort = true; - if (abort) return; if (noDevice(getState)) { dispatch({ @@ -793,14 +800,13 @@ export const updateFirmware = (path) => async (dispatch, getState) => { } try { - const rawFirmware = fs.readFileSync(path); - - await deviceRun(dispatch, getState, async () => { - const res = await session.firmwareUpdate({ - binary: rawFirmware.buffer - }); - return res.payload; - }); + if (device != BOOTLOADER_MODE) throw "device must be in bootloader mode"; + // Ask main.development.js to send the firmware for us. + const { error, started } = await ipcRenderer.invoke("upload-firmware", path); + // If the updated started, the device must be disconnected before further + // use. + if (started) alertNoConnectedDevice()(dispatch); + if (error) throw error; dispatch({ type: TRZ_UPDATEFIRMWARE_SUCCESS }); } catch (error) { dispatch({ error, type: TRZ_UPDATEFIRMWARE_FAILED }); diff --git a/app/components/views/TrezorPage/ConfigSections.js b/app/components/views/TrezorPage/ConfigSections.js index 1cc9830b8b..5e5b8e2a4a 100644 --- a/app/components/views/TrezorPage/ConfigSections.js +++ b/app/components/views/TrezorPage/ConfigSections.js @@ -46,6 +46,10 @@ class TrezorConfigSections extends React.Component { this.props.backupDevice(); } + isUpdating() { + return this.props.isPerformingUpdate; + } + render() { const { onTogglePinProtection, @@ -57,6 +61,7 @@ class TrezorConfigSections extends React.Component { onInitDevice, onBackupDevice, onUpdateFirmware, + isUpdating, loading } = this; @@ -77,7 +82,7 @@ class TrezorConfigSections extends React.Component { {...{ onWipeDevice, onRecoverDevice, onInitDevice, onBackupDevice, loading }} /> - + ); } diff --git a/app/components/views/TrezorPage/FirmwareUpdate.js b/app/components/views/TrezorPage/FirmwareUpdate.js index b3569b28fc..c4656cb792 100644 --- a/app/components/views/TrezorPage/FirmwareUpdate.js +++ b/app/components/views/TrezorPage/FirmwareUpdate.js @@ -30,7 +30,7 @@ class FirmwareUpdate extends React.Component { ); - const { loading } = this.props; + const { loading, isUpdating } = this.props; return ( + loading={loading || isUpdating()} + disabled={loading || isUpdating()}> diff --git a/app/connectors/trezor.js b/app/connectors/trezor.js index cc4e0aad68..876c34ad56 100644 --- a/app/connectors/trezor.js +++ b/app/connectors/trezor.js @@ -6,6 +6,7 @@ import * as trza from "../actions/TrezorActions"; const mapStateToProps = selectorMap({ isTrezor: sel.isTrezor, + isPerformingUpdate: sel.isPerformingTrezorUpdate, waitingForPin: sel.trezorWaitingForPin, waitingForPassPhrase: sel.trezorWaitingForPassPhrase, waitingForWord: sel.trezorWaitingForWord, diff --git a/app/main.development.js b/app/main.development.js index 8000fbad2b..1a9ce95847 100644 --- a/app/main.development.js +++ b/app/main.development.js @@ -58,7 +58,8 @@ import { startDcrlnd, stopDcrlnd, removeDcrlnd, - lnScbInfo + lnScbInfo, + updateTrezorFirmware } from "./main_dev/ipc"; import { initTemplate, @@ -298,6 +299,11 @@ ipcMain.on("remove-wallet", (event, walletPath, testnet) => { event.returnValue = removeWallet(testnet, walletPath); }); +ipcMain.handle("upload-firmware", async (event, firmware) => { + const res = await updateTrezorFirmware(firmware); + return res; +}); + ipcMain.on("stop-daemon", (event) => { event.returnValue = stopDaemon(); }); diff --git a/app/main_dev/ipc.js b/app/main_dev/ipc.js index e274f684a4..88ebeb06c6 100644 --- a/app/main_dev/ipc.js +++ b/app/main_dev/ipc.js @@ -17,6 +17,10 @@ import { setDcrdRpcCredentials } from "./launch"; import { MAINNET } from "constants"; +import { initTransport } from "actions/TrezorActions.js"; +import * as connect from "connect"; +import { rawToHex } from "helpers"; + const logger = createLogger(); let watchingOnlyWallet; @@ -141,6 +145,48 @@ export const removeWallet = (testnet, walletPath) => { } }; +// updateTrezorFirmware attempts to make a temporary connection to a trezor +// device and update it with the firmware at path. It returns an error string +// in case of error and whether the update process was started at all. +export const updateTrezorFirmware = async ( firmwarePath ) => { + let started = false; + let completed = false; + const rawFirmware = fs.readFileSync(firmwarePath); + const hexFirmware = rawToHex(rawFirmware); + let session = connect.default; + try { + await initTransport(session, false); + session.on(connect.UI_EVENT, (event) => { + if (event.type == connect.UI.FIRMWARE_PROGRESS) { + logger.log("info", "Trezor update progress: " + event.payload.progress+"%"); + // Ignore disconnect errors if completed. + if (event.payload.progress == 100) { + completed = true; + } + } + }); + started = true; + const res = await session.firmwareUpdate({ + binary: hexFirmware + }); + if (res.payload) { + if (res.payload.error) { + throw res.payload.error; + } + if (!res.payload.success) { + throw res.payload.code; + } + } + return { error: null, started }; + } catch (e) { + if (completed) return { error: null, started }; + logger.log("error", "error uploading trezor firmware: " + e); + return { error: e.toString(), started }; + } finally { + session = null; + } +}; + export const startWallet = ( mainWindow, daemonIsAdvanced, diff --git a/app/reducers/trezor.js b/app/reducers/trezor.js index 9b9cd39007..932ed0bd99 100644 --- a/app/reducers/trezor.js +++ b/app/reducers/trezor.js @@ -180,8 +180,13 @@ export default function trezor(state = {}, action) { case TRZ_WIPEDEVICE_ATTEMPT: case TRZ_RECOVERDEVICE_ATTEMPT: case TRZ_INITDEVICE_ATTEMPT: - case TRZ_UPDATEFIRMWARE_ATTEMPT: return { ...state, performingOperation: true }; + case TRZ_UPDATEFIRMWARE_ATTEMPT: + return { + ...state, + performingOperation: true, + performingUpdate: true + }; case TRZ_CHANGELABEL_SUCCESS: return { ...state, @@ -203,11 +208,16 @@ export default function trezor(state = {}, action) { case TRZ_RECOVERDEVICE_SUCCESS: case TRZ_INITDEVICE_FAILED: case TRZ_INITDEVICE_SUCCESS: - case TRZ_UPDATEFIRMWARE_FAILED: - case TRZ_UPDATEFIRMWARE_SUCCESS: case TRZ_BACKUPDEVICE_FAILED: case TRZ_BACKUPDEVICE_SUCCESS: return { ...state, performingOperation: false }; + case TRZ_UPDATEFIRMWARE_FAILED: + case TRZ_UPDATEFIRMWARE_SUCCESS: + return { + ...state, + performingOperation: false, + performingUpdate: false + }; case CLOSEWALLET_SUCCESS: return { ...state, enabled: false }; default: diff --git a/app/selectors.js b/app/selectors.js index 8e73d52588..bce659a805 100644 --- a/app/selectors.js +++ b/app/selectors.js @@ -1457,6 +1457,7 @@ export const autobuyerRunningModalVisible = get([ ]); export const isTrezor = get(["trezor", "enabled"]); +export const isPerformingTrezorUpdate = get(["trezor", "performingUpdate"]); export const isSignMessageDisabled = and(isWatchingOnly, not(isTrezor)); export const isChangePassPhraseDisabled = isWatchingOnly;