From cb88f4efdc29012dd6680570c80ea400947471cd Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Fri, 25 May 2018 18:16:49 +0200 Subject: [PATCH] WIP: Manager Workflow + refacto using commands --- src/commands/getDeviceInfo.js | 24 ++ src/commands/getFirmwareInfo.js | 19 ++ src/commands/getIsGenuine.js | 15 ++ src/commands/getLatestFirmwareForDevice.js | 19 ++ src/commands/installApp.js | 25 ++ src/commands/listApps.js | 15 ++ src/components/ManagerPage/AppsList.js | 41 +-- src/components/ManagerPage/EnsureDashboard.js | 93 +++++++ src/components/ManagerPage/EnsureDevice.js | 37 +++ src/components/ManagerPage/EnsureGenuine.js | 87 +++++++ .../ManagerPage/FinalFirmwareUpdate.js | 78 ++++++ src/components/ManagerPage/FirmwareUpdate.js | 111 ++------ src/components/ManagerPage/FlashMcu.js | 0 src/components/ManagerPage/index.js | 106 ++++---- src/helpers/apps/installApp.js | 16 ++ src/helpers/apps/listApps.js | 14 ++ src/helpers/apps/uninstallApp.js | 12 + src/helpers/common.js | 236 ++++++++++++++++++ src/helpers/constants.js | 13 + src/helpers/devices/getDeviceInfo.js | 26 ++ src/helpers/devices/getFirmwareInfo.js | 31 +++ src/helpers/devices/getIsGenuine.js | 5 + .../devices/getLatestFirmwareForDevice.js | 43 ++++ .../firmware}/installFinalFirmware.js | 2 +- .../firmware}/installMcu.js | 0 src/helpers/firmware/installOsuFirmware.js | 27 ++ src/internals/devices/index.js | 15 +- src/internals/manager/getFirmwareInfo.js | 26 -- .../manager/getLatestFirmwareForDevice.js | 44 ---- src/internals/manager/helpers.js | 62 ++--- src/internals/manager/index.js | 12 +- src/internals/manager/installApp.js | 12 - src/internals/manager/installOsuFirmware.js | 24 -- src/internals/manager/listApps.js | 14 -- src/internals/manager/uninstallApp.js | 12 - 35 files changed, 969 insertions(+), 347 deletions(-) create mode 100644 src/commands/getDeviceInfo.js create mode 100644 src/commands/getFirmwareInfo.js create mode 100644 src/commands/getIsGenuine.js create mode 100644 src/commands/getLatestFirmwareForDevice.js create mode 100644 src/commands/installApp.js create mode 100644 src/commands/listApps.js create mode 100644 src/components/ManagerPage/EnsureDashboard.js create mode 100644 src/components/ManagerPage/EnsureDevice.js create mode 100644 src/components/ManagerPage/EnsureGenuine.js create mode 100644 src/components/ManagerPage/FinalFirmwareUpdate.js create mode 100644 src/components/ManagerPage/FlashMcu.js create mode 100644 src/helpers/apps/installApp.js create mode 100644 src/helpers/apps/listApps.js create mode 100644 src/helpers/apps/uninstallApp.js create mode 100644 src/helpers/common.js create mode 100644 src/helpers/constants.js create mode 100644 src/helpers/devices/getDeviceInfo.js create mode 100644 src/helpers/devices/getFirmwareInfo.js create mode 100644 src/helpers/devices/getIsGenuine.js create mode 100644 src/helpers/devices/getLatestFirmwareForDevice.js rename src/{internals/manager => helpers/firmware}/installFinalFirmware.js (89%) rename src/{internals/manager => helpers/firmware}/installMcu.js (100%) create mode 100644 src/helpers/firmware/installOsuFirmware.js delete mode 100644 src/internals/manager/getFirmwareInfo.js delete mode 100644 src/internals/manager/getLatestFirmwareForDevice.js delete mode 100644 src/internals/manager/installApp.js delete mode 100644 src/internals/manager/installOsuFirmware.js delete mode 100644 src/internals/manager/listApps.js delete mode 100644 src/internals/manager/uninstallApp.js diff --git a/src/commands/getDeviceInfo.js b/src/commands/getDeviceInfo.js new file mode 100644 index 00000000..c2fc4bd2 --- /dev/null +++ b/src/commands/getDeviceInfo.js @@ -0,0 +1,24 @@ +// @flow + +import { createCommand, Command } from 'helpers/ipc' +import { fromPromise } from 'rxjs/observable/fromPromise' +import { withDevice } from 'helpers/deviceAccess' + +import getDeviceInfo from 'helpers/devices/getDeviceInfo' + +type Input = { + devicePath: string, +} + +type Result = { + targetId: number | string, + version: string, + final: boolean, + mcu: boolean, +} + +const cmd: Command = createCommand('devices', 'getDeviceInfo', ({ devicePath }) => + fromPromise(withDevice(devicePath)(transport => getDeviceInfo(transport))), +) + +export default cmd diff --git a/src/commands/getFirmwareInfo.js b/src/commands/getFirmwareInfo.js new file mode 100644 index 00000000..d20e7a12 --- /dev/null +++ b/src/commands/getFirmwareInfo.js @@ -0,0 +1,19 @@ +// @flow + +import { createCommand, Command } from 'helpers/ipc' +import { fromPromise } from 'rxjs/observable/fromPromise' + +import getFirmwareInfo from 'helpers/devices/getFirmwareInfo' + +type Input = { + targetId: string | number, + version: string, +} + +type Result = * + +const cmd: Command = createCommand('devices', 'getFirmwareInfo', data => + fromPromise(getFirmwareInfo(data)), +) + +export default cmd diff --git a/src/commands/getIsGenuine.js b/src/commands/getIsGenuine.js new file mode 100644 index 00000000..9c5a0dbe --- /dev/null +++ b/src/commands/getIsGenuine.js @@ -0,0 +1,15 @@ +// @flow + +import { createCommand, Command } from 'helpers/ipc' +import { fromPromise } from 'rxjs/observable/fromPromise' + +import getIsGenuine from 'helpers/devices/getIsGenuine' + +type Input = * +type Result = boolean + +const cmd: Command = createCommand('devices', 'getIsGenuine', () => + fromPromise(getIsGenuine()), +) + +export default cmd diff --git a/src/commands/getLatestFirmwareForDevice.js b/src/commands/getLatestFirmwareForDevice.js new file mode 100644 index 00000000..7c8bc414 --- /dev/null +++ b/src/commands/getLatestFirmwareForDevice.js @@ -0,0 +1,19 @@ +// @flow + +import { createCommand, Command } from 'helpers/ipc' +import { fromPromise } from 'rxjs/observable/fromPromise' + +import getLatestFirmwareForDevice from '../helpers/devices/getLatestFirmwareForDevice' + +type Input = { + targetId: string | number, + version: string, +} + +type Result = * + +const cmd: Command = createCommand('devices', 'getLatestFirmwareForDevice', data => + fromPromise(getLatestFirmwareForDevice(data)), +) + +export default cmd diff --git a/src/commands/installApp.js b/src/commands/installApp.js new file mode 100644 index 00000000..d9f6d530 --- /dev/null +++ b/src/commands/installApp.js @@ -0,0 +1,25 @@ +// @flow + +import { createCommand, Command } from 'helpers/ipc' +import { fromPromise } from 'rxjs/observable/fromPromise' +import { withDevice } from 'helpers/deviceAccess' + +import installApp from 'helpers/apps/installApp' + +import type { LedgerScriptParams } from 'helpers/common' + +type Input = { + appParams: LedgerScriptParams, + devicePath: string, +} + +type Result = * + +const cmd: Command = createCommand( + 'devices', + 'installApp', + ({ devicePath, ...rest }) => + fromPromise(withDevice(devicePath)(transport => installApp(transport, rest))), +) + +export default cmd diff --git a/src/commands/listApps.js b/src/commands/listApps.js new file mode 100644 index 00000000..a6659cb4 --- /dev/null +++ b/src/commands/listApps.js @@ -0,0 +1,15 @@ +// @flow + +import { createCommand, Command } from 'helpers/ipc' +import { fromPromise } from 'rxjs/observable/fromPromise' + +import listApps from 'helpers/apps/listApps' + +type Input = * +type Result = * + +const cmd: Command = createCommand('manager', 'listApps', () => + fromPromise(listApps()), +) + +export default cmd diff --git a/src/components/ManagerPage/AppsList.js b/src/components/ManagerPage/AppsList.js index e58248e6..19581ff0 100644 --- a/src/components/ManagerPage/AppsList.js +++ b/src/components/ManagerPage/AppsList.js @@ -4,7 +4,8 @@ import React, { PureComponent } from 'react' import styled from 'styled-components' import { translate } from 'react-i18next' -import runJob from 'renderer/runJob' +import listApps from 'commands/listApps' +import installApp from 'commands/installApp' import Box from 'components/base/Box' import Modal, { ModalBody } from 'components/base/Modal' @@ -28,12 +29,6 @@ const ICONS_FALLBACK = { type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error' -type jobHandlerOptions = { - job: string, - successResponse: string, - errorResponse: string, -} - type LedgerApp = { name: string, version: string, @@ -74,47 +69,31 @@ class AppsList extends PureComponent { _unmounted = false async fetchAppList() { - const appsList = - CACHED_APPS || - (await runJob({ - channel: 'manager', - job: 'listApps', - successResponse: 'manager.listAppsSuccess', - errorResponse: 'manager.listAppsError', - })) + const appsList = CACHED_APPS || (await listApps.send().toPromise()) CACHED_APPS = appsList if (!this._unmounted) { this.setState({ appsList, status: 'idle' }) } } - createDeviceJobHandler = (options: jobHandlerOptions) => (args: { app: any }) => async () => { + handleInstallApp = (args: { app: any }) => async () => { const appParams = args.app this.setState({ status: 'busy' }) try { - const { job, successResponse, errorResponse } = options const { device: { path: devicePath }, } = this.props const data = { appParams, devicePath } - await runJob({ channel: 'manager', job, successResponse, errorResponse, data }) + await installApp.send(data).toPromise() this.setState({ status: 'success' }) } catch (err) { this.setState({ status: 'error', error: err.message }) } } - handleInstallApp = this.createDeviceJobHandler({ - job: 'installApp', - successResponse: 'manager.appInstalled', - errorResponse: 'manager.appInstallError', - }) - - handleUninstallApp = this.createDeviceJobHandler({ - job: 'uninstallApp', - successResponse: 'manager.appUninstalled', - errorResponse: 'manager.appUninstallError', - }) + handleUninstallApp = (/* args: { app: any } */) => () => { + /* TODO */ + } handleCloseModal = () => this.setState({ status: 'idle' }) @@ -128,8 +107,8 @@ class AppsList extends PureComponent { name={c.name} version={`Version ${c.version}`} icon={ICONS_FALLBACK[c.icon] || c.icon} - onInstall={this.handleInstallApp(c)} - onUninstall={this.handleUninstallApp(c)} + onInstall={() => {}} + onUninstall={() => {}} /> ))} { + static defaultProps = { + children: null, + device: null, + } + + state = { + deviceInfo: null, + error: null, + } + + componentDidMount() { + this.checkForDashboard() + } + + componentDidUpdate() { + this.checkForDashboard() + } + + componentWillUnmount() { + this._unmounting = true + } + + _checking = false + _unmounting = false + + async checkForDashboard() { + const { device } = this.props + if (device && !this._checking) { + this._checking = true + try { + const deviceInfo = await getDeviceInfo.send({ devicePath: device.path }).toPromise() + if (!isEqual(this.state.deviceInfo, deviceInfo) || this.state.error) { + !this._unmounting && this.setState({ deviceInfo, error: null }) + } + } catch (err) { + if (!isEqual(err, this.state.error)) { + !this._unmounting && this.setState({ error: err, deviceInfo: null }) + } + } + this._checking = false + } + } + + render() { + const { deviceInfo, error } = this.state + const { children } = this.props + + if (deviceInfo) { + return children(deviceInfo) + } + + return error ? ( + + {error.message} + Please make sure your device is on the dashboard screen + + ) : null + } +} + +export default translate()(EnsureDashboard) diff --git a/src/components/ManagerPage/EnsureDevice.js b/src/components/ManagerPage/EnsureDevice.js new file mode 100644 index 00000000..ffffde02 --- /dev/null +++ b/src/components/ManagerPage/EnsureDevice.js @@ -0,0 +1,37 @@ +// @flow +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import { translate } from 'react-i18next' +import { compose } from 'redux' + +// import type { Device, T } from 'types/common' +import type { Device } from 'types/common' + +import { getCurrentDevice, getDevices } from 'reducers/devices' + +const mapStateToProps = state => ({ + device: getCurrentDevice(state), + nbDevices: getDevices(state).length, +}) + +type Props = { + // t: T, + device: ?Device, + nbDevices: number, + children: Function, +} + +type State = {} + +class EnsureDevice extends PureComponent { + static defaultProps = { + device: null, + } + + render() { + const { device, nbDevices, children } = this.props + return device ? children(device, nbDevices) : Please connect your device + } +} + +export default compose(translate(), connect(mapStateToProps))(EnsureDevice) diff --git a/src/components/ManagerPage/EnsureGenuine.js b/src/components/ManagerPage/EnsureGenuine.js new file mode 100644 index 00000000..c4665a0d --- /dev/null +++ b/src/components/ManagerPage/EnsureGenuine.js @@ -0,0 +1,87 @@ +// @flow +import React, { PureComponent, Fragment } from 'react' +import { translate } from 'react-i18next' +import isEqual from 'lodash/isEqual' + +import type { Node } from 'react' +// import type { Device, T } from 'types/common' +import type { Device } from 'types/common' + +import getIsGenuine from 'commands/getIsGenuine' + +type Props = { + // t: T, + device: Device, + children: Node, +} + +type State = { + genuine: boolean, + error: ?{ + message: string, + stack: string, + }, +} + +class EnsureGenuine extends PureComponent { + static defaultProps = { + children: null, + firmwareInfo: null, + } + + state = { + error: null, + genuine: false, + } + + componentDidMount() { + this.checkIsGenuine() + } + + componentDidUpdate() { + this.checkIsGenuine() + } + + componentWillUnmount() { + this._unmounting = true + } + + _checking = false + _unmounting = false + + async checkIsGenuine() { + const { device } = this.props + if (device && !this._checking) { + this._checking = true + try { + const isGenuine = await getIsGenuine.send().toPromise() + if (!this.state.genuine || this.state.error) { + !this._unmounting && this.setState({ genuine: isGenuine, error: null }) + } + } catch (err) { + if (!isEqual(this.state.error, err)) { + !this._unmounting && this.setState({ genuine: false, error: err }) + } + } + this._checking = false + } + } + + render() { + const { error, genuine } = this.state + const { children } = this.props + + if (genuine) { + return children + } + + return error ? ( + + {error.message} + You did not approve request on your device or your device is not genuine + + ) : null + } +} + +export default translate()(EnsureGenuine) diff --git a/src/components/ManagerPage/FinalFirmwareUpdate.js b/src/components/ManagerPage/FinalFirmwareUpdate.js new file mode 100644 index 00000000..86c128c8 --- /dev/null +++ b/src/components/ManagerPage/FinalFirmwareUpdate.js @@ -0,0 +1,78 @@ +// @flow + +import React, { PureComponent } from 'react' +import { translate } from 'react-i18next' +import type { Device, T } from 'types/common' + +// import runJob from 'renderer/runJob' + +import Box, { Card } from 'components/base/Box' +// import Button from 'components/base/Button' + +type DeviceInfos = { + targetId: number, + version: string, +} + +type Props = { + t: T, + device: Device, + infos: DeviceInfos, +} + +type State = { + // latestFirmware: ?FirmwareInfos, +} + +class FirmwareUpdate extends PureComponent { + state = { + // latestFirmware: null, + } + + componentDidMount() {} + + componentWillUnmount() { + this._unmounting = true + } + + _unmounting = false + + // handleInstallFinalFirmware = async () => { + // try { + // const { latestFirmware } = this.state + // this.setState(state => ({ ...state, installing: true })) + // const { + // device: { path: devicePath }, + // } = this.props + // await runJob({ + // channel: 'manager', + // job: 'installFinalFirmware', + // successResponse: 'device.finalFirmwareInstallSuccess', + // errorResponse: 'device.finalFirmwareInstallError', + // data: { + // devicePath, + // firmware: latestFirmware, + // }, + // }) + // } catch (err) { + // console.log(err) + // } + // } + + render() { + const { t, ...props } = this.props + + return ( + + + {t('manager:firmwareUpdate')} + + + + + + ) + } +} + +export default translate()(FirmwareUpdate) diff --git a/src/components/ManagerPage/FirmwareUpdate.js b/src/components/ManagerPage/FirmwareUpdate.js index 23956556..84d95ac2 100644 --- a/src/components/ManagerPage/FirmwareUpdate.js +++ b/src/components/ManagerPage/FirmwareUpdate.js @@ -2,15 +2,17 @@ import React, { PureComponent } from 'react' import isEqual from 'lodash/isEqual' +import isEmpty from 'lodash/isEmpty' import type { Device, T } from 'types/common' import runJob from 'renderer/runJob' +import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice' import Box, { Card } from 'components/base/Box' import Button from 'components/base/Button' -const CACHED_LATEST_FIRMWARE = null +// let CACHED_LATEST_FIRMWARE = null type FirmwareInfos = { name: string, @@ -18,8 +20,6 @@ type FirmwareInfos = { } type DeviceInfos = { - final: boolean, - mcu: boolean, targetId: number, version: string, } @@ -27,77 +27,52 @@ type DeviceInfos = { type Props = { t: T, device: Device, + infos: DeviceInfos, } type State = { latestFirmware: ?FirmwareInfos, - deviceInfos: ?DeviceInfos, - installing: boolean, } class FirmwareUpdate extends PureComponent { state = { latestFirmware: null, - deviceInfos: null, - installing: false, } componentDidMount() { - this.fetchLatestFirmware(true) + this.fetchLatestFirmware() } - componentDidUpdate(prevProps: Props, prevState: State) { - if (!isEqual(prevState.deviceInfos, this.state.deviceInfos)) { - this.fetchDeviceInfos() - } else if (this.state.installing) { - this.installFirmware() + componentDidUpdate() { + if (/* !CACHED_LATEST_FIRMWARE || */ isEmpty(this.state.latestFirmware)) { + this.fetchLatestFirmware() } } componentWillUnmount() { - this._unmounted = true + this._unmounting = true } - _unmounted = false + _unmounting = false - fetchLatestFirmware = async (checkDeviceInfos: boolean = false) => { - const { device } = this.props + fetchLatestFirmware = async () => { + const { infos } = this.props const latestFirmware = - CACHED_LATEST_FIRMWARE || - (await runJob({ - channel: 'manager', - job: 'getLatestFirmwareForDevice', - data: { devicePath: device.path }, - successResponse: 'manager.getLatestFirmwareForDeviceSuccess', - errorResponse: 'manager.getLatestFirmwareForDeviceError', - })) - - if (checkDeviceInfos) { - await this.fetchDeviceInfos() - } - - // CACHED_LATEST_FIRMWARE = latestFirmware - if (!this._unmounted) { + // CACHED_LATEST_FIRMWARE || + await getLatestFirmwareForDevice + .send({ targetId: infos.targetId, version: infos.version }) + .toPromise() + if ( + !isEmpty(latestFirmware) && + !isEqual(this.state.latestFirmware, latestFirmware) && + !this._unmounting + ) { + // CACHED_LATEST_FIRMWARE = latestFirmware this.setState({ latestFirmware }) } } - fetchDeviceInfos = async () => { - const { device } = this.props - const deviceInfos = await runJob({ - channel: 'manager', - job: 'getFirmwareInfo', // TODO: RENAME THIS PROCESS DIFFERENTLY (EG: getInstallStep) - data: { devicePath: device.path }, - successResponse: 'device.getFirmwareInfoSuccess', - errorResponse: 'device.getFirmwareInfoError', - }) - - if (!this._unmounted) { - this.setState(state => ({ ...state, deviceInfos })) - } - } - - handleIntallOsuFirmware = async () => { + installFirmware = async () => { try { const { latestFirmware } = this.state const { @@ -118,44 +93,6 @@ class FirmwareUpdate extends PureComponent { } } - handleIntallFinalFirmware = async () => { - try { - const { latestFirmware } = this.state - this.setState(state => ({ ...state, installing: true })) - const { - device: { path: devicePath }, - } = this.props - await runJob({ - channel: 'manager', - job: 'installFinalFirmware', - successResponse: 'device.finalFirmwareInstallSuccess', - errorResponse: 'device.finalFirmwareInstallError', - data: { - devicePath, - firmware: latestFirmware, - }, - }) - } catch (err) { - console.log(err) - } - } - - handleIntallMcu = async () => {} - - installFirmware = async () => { - const { deviceInfos } = this.state - if (deviceInfos) { - const { mcu, final } = deviceInfos - if (mcu) { - this.handleIntallMcu() - } else if (final) { - this.handleIntallFinalFirmware() - } else { - this.handleIntallOsuFirmware() - } - } - } - render() { const { t, ...props } = this.props const { latestFirmware } = this.state @@ -172,7 +109,7 @@ class FirmwareUpdate extends PureComponent { {`Latest firmware: ${latestFirmware.name}`} - diff --git a/src/components/ManagerPage/FlashMcu.js b/src/components/ManagerPage/FlashMcu.js new file mode 100644 index 00000000..e69de29b diff --git a/src/components/ManagerPage/index.js b/src/components/ManagerPage/index.js index dc8fbf75..68b45ba5 100644 --- a/src/components/ManagerPage/index.js +++ b/src/components/ManagerPage/index.js @@ -1,56 +1,47 @@ // @flow -import React, { PureComponent, Fragment } from 'react' -import { connect } from 'react-redux' +import React, { Component, Fragment } from 'react' import { translate } from 'react-i18next' -import { compose } from 'redux' -import type { Device, T } from 'types/common' +import type { T } from 'types/common' -import { getCurrentDevice, getDevices } from 'reducers/devices' - -import Pills from 'components/base/Pills' +// import Pills from 'components/base/Pills' import AppsList from './AppsList' -import DeviceInfos from './DeviceInfos' -import FirmwareUpdate from './FirmwareUpdate' - -const mapStateToProps = state => ({ - device: getCurrentDevice(state), - nbDevices: getDevices(state).length, -}) +// import DeviceInfos from './DeviceInfos' +// import FirmwareUpdate from './FirmwareUpdate' +import EnsureDevice from './EnsureDevice' +import EnsureDashboard from './EnsureDashboard' +import EnsureGenuine from './EnsureGenuine' const TABS = [{ key: 'apps', value: 'apps' }, { key: 'device', value: 'device' }] type Props = { t: T, - device: Device, - nbDevices: number, } type State = { - currentTab: 'apps' | 'device', + // currentTab: 'apps' | 'device', } -class ManagerPage extends PureComponent { - state = { - currentTab: 'apps', - } +class ManagerPage extends Component { + // state = { + // currentTab: 'apps', + // } - componentWillReceiveProps(nextProps) { - const { device } = this.props - const { currentTab } = this.state - if (device && !nextProps.device && currentTab === 'device') { - this.setState({ currentTab: 'apps' }) - } - } + // componentWillReceiveProps(nextProps) { + // const { device } = this.props + // const { currentTab } = this.state + // if (device && !nextProps.device && currentTab === 'device') { + // this.setState({ currentTab: 'apps' }) + // } + // } - handleTabChange = t => this.setState({ currentTab: t.value }) + // handleTabChange = t => this.setState({ currentTab: t.value }) - render() { - const { device, t, nbDevices } = this.props - const { currentTab } = this.state - const tabs = TABS.map(i => { + createTabs = (device, nbDevices) => { + const { t } = this.props + return TABS.map(i => { let label = t(`manager:tabs.${i.key}`) if (i.key === 'device') { if (!device) { @@ -60,19 +51,50 @@ class ManagerPage extends PureComponent { } return { ...i, label } }).filter(Boolean) + } + + render() { + const { t } = this.props + // const { currentTab } = this.state + return ( - - {currentTab === 'apps' && ( - - - - - )} - {currentTab === 'device' && } + + {device => ( + + {deviceInfo => ( + + {/* */} + {deviceInfo.mcu && bootloader mode} + {deviceInfo.final && osu mode} + + {!deviceInfo.mcu && + !deviceInfo.final && ( + + {/* */} + + + )} + + )} + + )} + ) } } -export default compose(translate(), connect(mapStateToProps))(ManagerPage) +export default translate()(ManagerPage) diff --git a/src/helpers/apps/installApp.js b/src/helpers/apps/installApp.js new file mode 100644 index 00000000..465dc18f --- /dev/null +++ b/src/helpers/apps/installApp.js @@ -0,0 +1,16 @@ +// @flow + +import type Transport from '@ledgerhq/hw-transport' + +import { createSocketDialog } from 'helpers/common' +import type { LedgerScriptParams } from 'helpers/common' + +/** + * Install an app on the device + */ +export default async function installApp( + transport: Transport<*>, + { appParams }: { appParams: LedgerScriptParams }, +): Promise { + return createSocketDialog(transport, '/update/install', appParams) +} diff --git a/src/helpers/apps/listApps.js b/src/helpers/apps/listApps.js new file mode 100644 index 00000000..dce2b9e3 --- /dev/null +++ b/src/helpers/apps/listApps.js @@ -0,0 +1,14 @@ +// @flow + +import axios from 'axios' + +export default async () => { + try { + const { data } = await axios.get('https://api.ledgerwallet.com/update/applications') + return data['nanos-1.4'] + } catch (err) { + const error = Error(err.message) + error.stack = err.stack + throw err + } +} diff --git a/src/helpers/apps/uninstallApp.js b/src/helpers/apps/uninstallApp.js new file mode 100644 index 00000000..56090f4b --- /dev/null +++ b/src/helpers/apps/uninstallApp.js @@ -0,0 +1,12 @@ +// @flow + +import type { IPCSend } from 'types/electron' + +// import { createTransportHandler, uninstallApp } from 'helpers/common' + +// export default (send: IPCSend, data: any) => +// createTransportHandler(send, { +// action: uninstallApp, +// successResponse: 'manager.appUninstalled', +// errorResponse: 'manager.appUninstallError', +// })(data) diff --git a/src/helpers/common.js b/src/helpers/common.js new file mode 100644 index 00000000..52ebbe06 --- /dev/null +++ b/src/helpers/common.js @@ -0,0 +1,236 @@ +// @flow + +import chalk from 'chalk' +import Websocket from 'ws' +import qs from 'qs' +import type Transport from '@ledgerhq/hw-transport' + +import { BASE_SOCKET_URL, APDUS } from './constants' + +type WebsocketType = { + send: (string, any) => void, + on: (string, Function) => void, +} + +type Message = { + nonce: number, + query?: string, + response?: string, + data: any, +} + +export type LedgerScriptParams = { + firmware?: string, + firmwareKey?: string, + delete?: string, + deleteKey?: string, +} + +type FirmwareUpdateType = 'osu' | 'final' + +// /** +// * Install an app on the device +// */ +// export async function installApp( +// transport: Transport<*>, +// { appParams }: { appParams: LedgerScriptParams }, +// ): Promise { +// return createSocketDialog(transport, '/update/install', appParams) +// } + +/** + * Uninstall an app on the device + */ +export async function uninstallApp( + transport: Transport<*>, + { appParams }: { appParams: LedgerScriptParams }, +): Promise { + return createSocketDialog(transport, '/update/install', { + ...appParams, + firmware: appParams.delete, + firmwareKey: appParams.deleteKey, + }) +} + +export async function getMemInfos(transport: Transport<*>): Promise { + const { targetId } = await getFirmwareInfo(transport) + // Dont ask me about this `perso_11`: I don't know. But we need it. + return createSocketDialog(transport, '/get-mem-infos', { targetId, perso: 'perso_11' }) +} + +/** + * Send data through ws + */ +function socketSend(ws: WebsocketType, msg: Message) { + logWS('SEND', msg) + const strMsg = JSON.stringify(msg) + ws.send(strMsg) +} + +/** + * Exchange data on transport + */ +export async function exchange( + ws: WebsocketType, + transport: Transport<*>, + msg: Message, +): Promise { + const { data, nonce } = msg + const r: Buffer = await transport.exchange(Buffer.from(data, 'hex')) + const status = r.slice(r.length - 2) + const buffer = r.slice(0, r.length - 2) + const strStatus = status.toString('hex') + socketSend(ws, { + nonce, + response: strStatus === '9000' ? 'success' : 'error', + data: buffer.toString('hex'), + }) +} + +/** + * Bulk update on transport + */ +export async function bulk(ws: WebsocketType, transport: Transport<*>, msg: Message) { + const { data, nonce } = msg + + // Execute all apdus and collect last status + let lastStatus = null + for (const apdu of data) { + const r: Buffer = await transport.exchange(Buffer.from(apdu, 'hex')) + lastStatus = r.slice(r.length - 2) + } + if (!lastStatus) { + throw new Error('No status collected from bulk') + } + + const strStatus = lastStatus.toString('hex') + socketSend(ws, { + nonce, + response: strStatus === '9000' ? 'success' : 'error', + data: strStatus === '9000' ? '' : strStatus, + }) +} + +/** + * Open socket connection with firmware api, and init a dialog + * with the device + */ +export async function createSocketDialog( + transport: Transport<*>, + endpoint: string, + params: LedgerScriptParams, +) { + return new Promise(async (resolve, reject) => { + try { + let lastData + const url = `${BASE_SOCKET_URL}${endpoint}?${qs.stringify(params)}` + + log('WS CONNECTING', url) + const ws: WebsocketType = new Websocket(url) + + ws.on('open', () => log('WS CONNECTED')) + + ws.on('close', () => { + log('WS CLOSED') + resolve(lastData) + }) + + ws.on('message', async rawMsg => { + const handlers = { + exchange: msg => exchange(ws, transport, msg), + bulk: msg => bulk(ws, transport, msg), + success: msg => { + if (msg.data) { + lastData = msg.data + } + }, + error: msg => { + log('WS ERROR', ':(') + throw new Error(msg.data) + }, + } + try { + const msg = JSON.parse(rawMsg) + if (!(msg.query in handlers)) { + throw new Error(`Cannot handle msg of type ${msg.query}`) + } + logWS('RECEIVE', msg) + await handlers[msg.query](msg) + } catch (err) { + log('ERROR', err.toString()) + reject(err) + } + }) + } catch (err) { + reject(err) + } + }) +} + +/** + * Retrieve targetId and firmware version from device + */ +export async function getFirmwareInfo(transport: Transport<*>) { + try { + const res = await transport.send(...APDUS.GET_FIRMWARE) + const byteArray = [...res] + const data = byteArray.slice(0, byteArray.length - 2) + const targetIdStr = Buffer.from(data.slice(0, 4)) + const targetId = targetIdStr.readUIntBE(0, 4) + const versionLength = data[4] + const version = Buffer.from(data.slice(5, 5 + versionLength)).toString() + return { targetId, version } + } catch (err) { + const error = new Error(err.message) + error.stack = err.stack + throw error + } +} + +/** + * Debug helper + */ +export function log(namespace: string, str: string = '', color?: string) { + namespace = namespace.padEnd(15) + // $FlowFixMe + const coloredNamespace = color ? chalk[color](namespace) : namespace + if (__DEV__) { + console.log(`${chalk.bold(`> ${coloredNamespace}`)} ${str}`) // eslint-disable-line no-console + } +} + +/** + * Log a socket send/receive + */ +export function logWS(type: string, msg: Message) { + const arrow = type === 'SEND' ? '↑' : '↓' + const namespace = `${arrow} WS ${type}` + const color = type === 'SEND' ? 'blue' : 'red' + if (msg.nonce) { + let d = '' + if (msg.query === 'exchange') { + d = msg.data.length > 100 ? `${msg.data.substr(0, 97)}...` : msg.data + } else if (msg.query === 'bulk') { + d = `[bulk x ${msg.data.length}]` + } + log( + namespace, + `${String(msg.nonce).padEnd(2)} ${(msg.response || msg.query || '').padEnd(10)} ${d}`, + color, + ) + } else { + log(namespace, JSON.stringify(msg), color) + } +} + +/** + * Helpers to build OSU and Final firmware params + */ +export const buildParamsFromFirmware = (type: FirmwareUpdateType): Function => ( + data: any, +): LedgerScriptParams => ({ + firmware: data[`${type}_firmware`], + firmwareKey: data[`${type}_firmware_key`], + perso: data[`${type}_perso`], + targetId: data[`${type}_target_id`], +}) diff --git a/src/helpers/constants.js b/src/helpers/constants.js new file mode 100644 index 00000000..3ca86b53 --- /dev/null +++ b/src/helpers/constants.js @@ -0,0 +1,13 @@ +// Socket endpoint + +export const BASE_SOCKET_URL = 'ws://api.ledgerwallet.com' +// If you want to test locally with https://github.com/LedgerHQ/ledger-update-python-api +// export const BASE_SOCKET_URL = 'ws://localhost:3001/update' + +// List of APDUS +export const APDUS = { + GET_FIRMWARE: [0xe0, 0x01, 0x00, 0x00], + // we dont have common call that works inside app & dashboard + // TODO: this should disappear. + GET_FIRMWARE_FALLBACK: [0xe0, 0xc4, 0x00, 0x00], +} diff --git a/src/helpers/devices/getDeviceInfo.js b/src/helpers/devices/getDeviceInfo.js new file mode 100644 index 00000000..06a1e2be --- /dev/null +++ b/src/helpers/devices/getDeviceInfo.js @@ -0,0 +1,26 @@ +// @flow + +import type Transport from '@ledgerhq/hw-transport' + +import { getFirmwareInfo } from 'helpers/common' + +type Result = { + targetId: string | number, + version: string, + mcu: boolean, + final: boolean, +} + +export default async (transport: Transport<*>): Promise => { + try { + const { targetId, version } = await getFirmwareInfo(transport) + const finalReady = version.endsWith('-osu') + const mcuReady = targetId === 0x01000001 + + return { targetId, version, final: finalReady, mcu: mcuReady } + } catch (err) { + const error = Error(err.message) + error.stack = err.stack + throw error + } +} diff --git a/src/helpers/devices/getFirmwareInfo.js b/src/helpers/devices/getFirmwareInfo.js new file mode 100644 index 00000000..71e2ed02 --- /dev/null +++ b/src/helpers/devices/getFirmwareInfo.js @@ -0,0 +1,31 @@ +// @flow +import axios from 'axios' +import isEmpty from 'lodash/isEmpty' + +const { API_BASE_URL } = process.env + +type Input = { + version: string, + targetId: string | number, +} + +let error +export default async (data: Input) => { + try { + const { data: seFirmwareVersion } = await axios.post(`${API_BASE_URL}/firmware_versions_name`, { + se_firmware_name: data.version, + target_id: data.targetId, + }) + + if (!isEmpty(seFirmwareVersion)) { + return seFirmwareVersion + } + + error = Error('could not retrieve firmware informations, try again later') + throw error + } catch (err) { + error = Error(err.message) + error.stack = err.stack + throw error + } +} diff --git a/src/helpers/devices/getIsGenuine.js b/src/helpers/devices/getIsGenuine.js new file mode 100644 index 00000000..a0a8ad3a --- /dev/null +++ b/src/helpers/devices/getIsGenuine.js @@ -0,0 +1,5 @@ +// @flow + +// import type Transport from '@ledgerhq/hw-transport' + +export default async (/* transport: Transport<*> */) => new Promise(resolve => resolve(true)) diff --git a/src/helpers/devices/getLatestFirmwareForDevice.js b/src/helpers/devices/getLatestFirmwareForDevice.js new file mode 100644 index 00000000..5711a2b8 --- /dev/null +++ b/src/helpers/devices/getLatestFirmwareForDevice.js @@ -0,0 +1,43 @@ +// @flow +import axios from 'axios' +import isEmpty from 'lodash/isEmpty' + +import getFirmwareInfo from './getFirmwareInfo' + +const { API_BASE_URL } = process.env + +type Input = { + targetId: string | number, + version: string, +} + +export default async (data: Input) => { + try { + // Get firmware infos with firmware name and device version + const seFirmwareVersion = await getFirmwareInfo(data) + + // Get device infos from targetId + const { data: deviceVersion } = await axios.get( + `${API_BASE_URL}/device_versions_target_id/${data.targetId}`, + ) + + // Fetch next possible firmware + const { data: serverData } = await axios.post(`${API_BASE_URL}/get_latest_firmware`, { + current_se_firmware_version: seFirmwareVersion.id, + device_version: deviceVersion.id, + providers: [1], + }) + + const { se_firmware_version } = serverData + + if (!isEmpty(se_firmware_version)) { + return se_firmware_version + } + + return null + } catch (err) { + const error = Error(err.message) + error.stack = err.stack + throw error + } +} diff --git a/src/internals/manager/installFinalFirmware.js b/src/helpers/firmware/installFinalFirmware.js similarity index 89% rename from src/internals/manager/installFinalFirmware.js rename to src/helpers/firmware/installFinalFirmware.js index 1f7f32a6..6e77ecdd 100644 --- a/src/internals/manager/installFinalFirmware.js +++ b/src/helpers/firmware/installFinalFirmware.js @@ -3,7 +3,7 @@ import CommNodeHid from '@ledgerhq/hw-transport-node-hid' import type { IPCSend } from 'types/electron' -import { createSocketDialog, buildParamsFromFirmware } from './helpers' +import { createSocketDialog, buildParamsFromFirmware } from 'helpers/common' type DataType = { devicePath: string, diff --git a/src/internals/manager/installMcu.js b/src/helpers/firmware/installMcu.js similarity index 100% rename from src/internals/manager/installMcu.js rename to src/helpers/firmware/installMcu.js diff --git a/src/helpers/firmware/installOsuFirmware.js b/src/helpers/firmware/installOsuFirmware.js new file mode 100644 index 00000000..458cf2ab --- /dev/null +++ b/src/helpers/firmware/installOsuFirmware.js @@ -0,0 +1,27 @@ +// @flow + +import type Transport from '@ledgerhq/hw-transport' + +import { createSocketDialog, buildParamsFromFirmware } from 'helpers/common' + +type Input = { + devicePath: string, + firmware: Object, +} + +type Result = * + +const buildOsuParams = buildParamsFromFirmware('osu') + +export default async (transport: Transport<*>, data: Input): Result => { + try { + const osuData = buildOsuParams(data.firmware) + await createSocketDialog(transport, '/update/install', osuData) + return { success: true } + } catch (err) { + const error = Error(err.message) + error.stack = err.stack + const result = { success: false, error } + throw result + } +} diff --git a/src/internals/devices/index.js b/src/internals/devices/index.js index 7d19f51d..ac971950 100644 --- a/src/internals/devices/index.js +++ b/src/internals/devices/index.js @@ -3,9 +3,22 @@ import type { Command } from 'helpers/ipc' import getAddress from 'commands/getAddress' import signTransaction from 'commands/signTransaction' +import getDeviceInfo from 'commands/getDeviceInfo' +import getFirmwareInfo from 'commands/getFirmwareInfo' +import getIsGenuine from 'commands/getIsGenuine' +import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice' +import installApp from 'commands/installApp' import listen from './listen' // TODO port these to commands export { listen } -export const commands: Array> = [getAddress, signTransaction] +export const commands: Array> = [ + getAddress, + signTransaction, + getDeviceInfo, + getFirmwareInfo, + getIsGenuine, + getLatestFirmwareForDevice, + installApp, +] diff --git a/src/internals/manager/getFirmwareInfo.js b/src/internals/manager/getFirmwareInfo.js deleted file mode 100644 index 13d57154..00000000 --- a/src/internals/manager/getFirmwareInfo.js +++ /dev/null @@ -1,26 +0,0 @@ -// @flow - -import type Transport from '@ledgerhq/hw-transport' - -import type { IPCSend } from 'types/electron' - -import { createTransportHandler, getFirmwareInfo } from './helpers' - -const handler = async (transport: Transport<*>) => - new Promise(async resolve => { - try { - const { targetId, version } = await getFirmwareInfo(transport) - const finalReady = version.endsWith('-osu') - const mcuReady = targetId === 0x01000001 - resolve({ targetId, version, final: finalReady, mcu: mcuReady }) - } catch (err) { - throw err - } - }) - -export default async (send: IPCSend, data: any) => - createTransportHandler(send, { - action: handler, - successResponse: 'device.getFirmwareInfoSuccess', - errorResponse: 'device.getFirmwareInfoError', - })(data) diff --git a/src/internals/manager/getLatestFirmwareForDevice.js b/src/internals/manager/getLatestFirmwareForDevice.js deleted file mode 100644 index 0c956458..00000000 --- a/src/internals/manager/getLatestFirmwareForDevice.js +++ /dev/null @@ -1,44 +0,0 @@ -// @flow -import axios from 'axios' -import CommNodeHid from '@ledgerhq/hw-transport-node-hid' -import isEmpty from 'lodash/isEmpty' - -import type { IPCSend } from 'types/electron' - -import { getFirmwareInfo } from './helpers' - -const { API_BASE_URL } = process.env - -export default async (send: IPCSend, data: any) => { - try { - const transport = await CommNodeHid.open(data.devicePath) - const infos = await getFirmwareInfo(transport) - // Get device infos from targetId - const { data: deviceVersion } = await axios.get( - `${API_BASE_URL}/device_versions_target_id/${infos.targetId}`, - ) - - // Get firmware infos with firmware name and device version - const { data: seFirmwareVersion } = await axios.post(`${API_BASE_URL}/firmware_versions_name`, { - device_version: deviceVersion.id, - se_firmware_name: infos.version, - }) - - // Fetch next possible firmware - const { data: serverData } = await axios.post(`${API_BASE_URL}/get_latest_firmware`, { - current_se_firmware_version: seFirmwareVersion.id, - device_version: deviceVersion.id, - providers: [1], - }) - - const { se_firmware_version } = serverData - - if (!isEmpty(se_firmware_version)) { - send('manager.getLatestFirmwareForDeviceSuccess', se_firmware_version) - } else { - send('manager.getLatestFirmwareForDeviceError', { name: 'yolo', notes: 'fake' }) - } - } catch (error) { - send('manager.getLatestFirmwareForDeviceError', { name: 'yolo', notes: 'fake', error }) - } -} diff --git a/src/internals/manager/helpers.js b/src/internals/manager/helpers.js index d5cae4ce..2a41210d 100644 --- a/src/internals/manager/helpers.js +++ b/src/internals/manager/helpers.js @@ -9,6 +9,8 @@ import type Transport from '@ledgerhq/hw-transport' import type { IPCSend } from 'types/electron' import { BASE_SOCKET_URL, APDUS } from './constants' +// TODO: REMOVE FILE WHEN REFACTO IS OVER + type WebsocketType = { send: (string, any) => void, on: (string, Function) => void, @@ -28,8 +30,6 @@ type LedgerScriptParams = { deleteKey?: string, } -type FirmwareUpdateType = 'osu' | 'final' - /** * Generate handler which create transport with given * `devicePath` then call action with it @@ -65,30 +65,6 @@ export function createTransportHandler( } } -/** - * Install an app on the device - */ -export async function installApp( - transport: Transport<*>, - { appParams }: { appParams: LedgerScriptParams }, -): Promise { - return createSocketDialog(transport, '/update/install', appParams) -} - -/** - * Uninstall an app on the device - */ -export async function uninstallApp( - transport: Transport<*>, - { appParams }: { appParams: LedgerScriptParams }, -): Promise { - return createSocketDialog(transport, '/update/install', { - ...appParams, - firmware: appParams.delete, - firmwareKey: appParams.deleteKey, - }) -} - export async function getMemInfos(transport: Transport<*>): Promise { const { targetId } = await getFirmwareInfo(transport) // Dont ask me about this `perso_11`: I don't know. But we need it. @@ -208,14 +184,20 @@ export async function createSocketDialog( * Retrieve targetId and firmware version from device */ export async function getFirmwareInfo(transport: Transport<*>) { - const res = await transport.send(...APDUS.GET_FIRMWARE) - const byteArray = [...res] - const data = byteArray.slice(0, byteArray.length - 2) - const targetIdStr = Buffer.from(data.slice(0, 4)) - const targetId = targetIdStr.readUIntBE(0, 4) - const versionLength = data[4] - const version = Buffer.from(data.slice(5, 5 + versionLength)).toString() - return { targetId, version } + try { + const res = await transport.send(...APDUS.GET_FIRMWARE) + const byteArray = [...res] + const data = byteArray.slice(0, byteArray.length - 2) + const targetIdStr = Buffer.from(data.slice(0, 4)) + const targetId = targetIdStr.readUIntBE(0, 4) + const versionLength = data[4] + const version = Buffer.from(data.slice(5, 5 + versionLength)).toString() + return { targetId, version } + } catch (err) { + const error = new Error(err.message) + error.stack = err.stack + throw error + } } /** @@ -253,15 +235,3 @@ export function logWS(type: string, msg: Message) { log(namespace, JSON.stringify(msg), color) } } - -/** - * Helpers to build OSU and Final firmware params - */ -export const buildParamsFromFirmware = (type: FirmwareUpdateType): Function => ( - data: any, -): LedgerScriptParams => ({ - firmware: data[`${type}_firmware`], - firmwareKey: data[`${type}_firmware_key`], - perso: data[`${type}_perso`], - targetId: data[`${type}_target_id`], -}) diff --git a/src/internals/manager/index.js b/src/internals/manager/index.js index 13b7093c..3b2c0349 100644 --- a/src/internals/manager/index.js +++ b/src/internals/manager/index.js @@ -1,4 +1,7 @@ // @flow +import type { Command } from 'helpers/ipc' + +import listApps from 'commands/listApps' /** * Manager @@ -17,10 +20,5 @@ */ export { default as getMemInfos } from './getMemInfos' -export { default as installApp } from './installApp' -export { default as listApps } from './listApps' -export { default as uninstallApp } from './uninstallApp' -export { default as getLatestFirmwareForDevice } from './getLatestFirmwareForDevice' -export { default as installOsuFirmware } from './installOsuFirmware' -export { default as installFinalFirmware } from './installFinalFirmware' -export { default as getFirmwareInfo } from './getFirmwareInfo' + +export const commands: Array> = [listApps] diff --git a/src/internals/manager/installApp.js b/src/internals/manager/installApp.js deleted file mode 100644 index 566ed9fe..00000000 --- a/src/internals/manager/installApp.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow - -import type { IPCSend } from 'types/electron' - -import { createTransportHandler, installApp } from './helpers' - -export default (send: IPCSend, data: any) => - createTransportHandler(send, { - action: installApp, - successResponse: 'manager.appInstalled', - errorResponse: 'manager.appInstallError', - })(data) diff --git a/src/internals/manager/installOsuFirmware.js b/src/internals/manager/installOsuFirmware.js deleted file mode 100644 index 479b40e3..00000000 --- a/src/internals/manager/installOsuFirmware.js +++ /dev/null @@ -1,24 +0,0 @@ -// @flow - -import CommNodeHid from '@ledgerhq/hw-transport-node-hid' - -import type { IPCSend } from 'types/electron' -import { createSocketDialog, buildParamsFromFirmware } from './helpers' - -type DataType = { - devicePath: string, - firmware: Object, -} - -const buildOsuParams = buildParamsFromFirmware('osu') - -export default async (send: IPCSend, data: DataType) => { - try { - const transport = await CommNodeHid.open(data.devicePath) - const osuData = buildOsuParams(data.firmware) - await createSocketDialog(transport, '/update/install', osuData) - send('device.osuFirmwareInstallSuccess', { success: true }) - } catch (err) { - send('device.osuFirmwareInstallError', { success: false }) - } -} diff --git a/src/internals/manager/listApps.js b/src/internals/manager/listApps.js deleted file mode 100644 index 2aceeab4..00000000 --- a/src/internals/manager/listApps.js +++ /dev/null @@ -1,14 +0,0 @@ -// @flow - -import axios from 'axios' - -import type { IPCSend } from 'types/electron' - -export default async (send: IPCSend) => { - try { - const { data } = await axios.get('https://api.ledgerwallet.com/update/applications') - send('manager.listAppsSuccess', data['nanos-1.4']) - } catch (err) { - send('manager.listAppsError', { message: err.message, stack: err.stack }) - } -} diff --git a/src/internals/manager/uninstallApp.js b/src/internals/manager/uninstallApp.js deleted file mode 100644 index 233f8953..00000000 --- a/src/internals/manager/uninstallApp.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow - -import type { IPCSend } from 'types/electron' - -import { createTransportHandler, uninstallApp } from './helpers' - -export default (send: IPCSend, data: any) => - createTransportHandler(send, { - action: uninstallApp, - successResponse: 'manager.appUninstalled', - errorResponse: 'manager.appUninstallError', - })(data)