Browse Source

WIP: Manager Workflow + refacto using commands

master
Valentin D. Pinkman 7 years ago
parent
commit
cb88f4efdc
No known key found for this signature in database GPG Key ID: E7D110669FFB8D3E
  1. 24
      src/commands/getDeviceInfo.js
  2. 19
      src/commands/getFirmwareInfo.js
  3. 15
      src/commands/getIsGenuine.js
  4. 19
      src/commands/getLatestFirmwareForDevice.js
  5. 25
      src/commands/installApp.js
  6. 15
      src/commands/listApps.js
  7. 41
      src/components/ManagerPage/AppsList.js
  8. 93
      src/components/ManagerPage/EnsureDashboard.js
  9. 37
      src/components/ManagerPage/EnsureDevice.js
  10. 87
      src/components/ManagerPage/EnsureGenuine.js
  11. 78
      src/components/ManagerPage/FinalFirmwareUpdate.js
  12. 111
      src/components/ManagerPage/FirmwareUpdate.js
  13. 0
      src/components/ManagerPage/FlashMcu.js
  14. 106
      src/components/ManagerPage/index.js
  15. 16
      src/helpers/apps/installApp.js
  16. 14
      src/helpers/apps/listApps.js
  17. 12
      src/helpers/apps/uninstallApp.js
  18. 236
      src/helpers/common.js
  19. 13
      src/helpers/constants.js
  20. 26
      src/helpers/devices/getDeviceInfo.js
  21. 31
      src/helpers/devices/getFirmwareInfo.js
  22. 5
      src/helpers/devices/getIsGenuine.js
  23. 43
      src/helpers/devices/getLatestFirmwareForDevice.js
  24. 2
      src/helpers/firmware/installFinalFirmware.js
  25. 0
      src/helpers/firmware/installMcu.js
  26. 27
      src/helpers/firmware/installOsuFirmware.js
  27. 15
      src/internals/devices/index.js
  28. 26
      src/internals/manager/getFirmwareInfo.js
  29. 44
      src/internals/manager/getLatestFirmwareForDevice.js
  30. 62
      src/internals/manager/helpers.js
  31. 12
      src/internals/manager/index.js
  32. 12
      src/internals/manager/installApp.js
  33. 24
      src/internals/manager/installOsuFirmware.js
  34. 14
      src/internals/manager/listApps.js
  35. 12
      src/internals/manager/uninstallApp.js

24
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<Input, Result> = createCommand('devices', 'getDeviceInfo', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => getDeviceInfo(transport))),
)
export default cmd

19
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<Input, Result> = createCommand('devices', 'getFirmwareInfo', data =>
fromPromise(getFirmwareInfo(data)),
)
export default cmd

15
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<Input, Result> = createCommand('devices', 'getIsGenuine', () =>
fromPromise(getIsGenuine()),
)
export default cmd

19
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<Input, Result> = createCommand('devices', 'getLatestFirmwareForDevice', data =>
fromPromise(getLatestFirmwareForDevice(data)),
)
export default cmd

25
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<Input, Result> = createCommand(
'devices',
'installApp',
({ devicePath, ...rest }) =>
fromPromise(withDevice(devicePath)(transport => installApp(transport, rest))),
)
export default cmd

15
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<Input, Result> = createCommand('manager', 'listApps', () =>
fromPromise(listApps()),
)
export default cmd

41
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<Props, State> {
_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<Props, State> {
name={c.name}
version={`Version ${c.version}`}
icon={ICONS_FALLBACK[c.icon] || c.icon}
onInstall={this.handleInstallApp(c)}
onUninstall={this.handleUninstallApp(c)}
onInstall={() => {}}
onUninstall={() => {}}
/>
))}
<Modal

93
src/components/ManagerPage/EnsureDashboard.js

@ -0,0 +1,93 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import { translate } from 'react-i18next'
import isEqual from 'lodash/isEqual'
// import type { Device, T } from 'types/common'
import type { Device } from 'types/common'
import getDeviceInfo from 'commands/getDeviceInfo'
type DeviceInfo = {
targetId: number | string,
version: string,
final: boolean,
mcu: boolean,
}
type Props = {
// t: T,
device: Device,
children: Function,
}
type State = {
deviceInfo: ?DeviceInfo,
error: ?{
message: string,
stack: string,
},
}
class EnsureDashboard extends PureComponent<Props, State> {
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 ? (
<Fragment>
<span>{error.message}</span>
<span>Please make sure your device is on the dashboard screen</span>
</Fragment>
) : null
}
}
export default translate()(EnsureDashboard)

37
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<Props, State> {
static defaultProps = {
device: null,
}
render() {
const { device, nbDevices, children } = this.props
return device ? children(device, nbDevices) : <span>Please connect your device</span>
}
}
export default compose(translate(), connect(mapStateToProps))(EnsureDevice)

87
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<Props, State> {
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 ? (
<Fragment>
<span>{error.message}</span>
<span>You did not approve request on your device or your device is not genuine</span>
</Fragment>
) : null
}
}
export default translate()(EnsureGenuine)

78
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<Props, State> {
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 (
<Box flow={4} {...props}>
<Box color="dark" ff="Museo Sans" fontSize={6}>
{t('manager:firmwareUpdate')}
</Box>
<Card flow={2} {...props}>
<Box horizontal align="center" flow={2} />
</Card>
</Box>
)
}
}
export default translate()(FirmwareUpdate)

111
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<Props, State> {
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<Props, State> {
}
}
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<Props, State> {
<Card flow={2} {...props}>
<Box horizontal align="center" flow={2}>
<Box ff="Museo Sans">{`Latest firmware: ${latestFirmware.name}`}</Box>
<Button outline onClick={this.handleInstallFirmware}>
<Button outline onClick={this.installFirmware}>
{'Install'}
</Button>
</Box>

0
src/components/ManagerPage/FlashMcu.js

106
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<Props, State> {
state = {
currentTab: 'apps',
}
class ManagerPage extends Component<Props, State> {
// 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<Props, State> {
}
return { ...i, label }
}).filter(Boolean)
}
render() {
const { t } = this.props
// const { currentTab } = this.state
return (
<Fragment>
<Pills items={tabs} activeKey={currentTab} onChange={this.handleTabChange} mb={6} />
{currentTab === 'apps' && (
<Fragment>
<FirmwareUpdate t={t} device={device} mb={4} />
<AppsList device={device} />
</Fragment>
)}
{currentTab === 'device' && <DeviceInfos device={device} />}
<EnsureDevice>
{device => (
<EnsureDashboard device={device}>
{deviceInfo => (
<Fragment>
{/* <Pills
items={this.createTabs(device, nbDevices)}
activeKey={currentTab}
onChange={this.handleTabChange}
mb={6}
/> */}
{deviceInfo.mcu && <span>bootloader mode</span>}
{deviceInfo.final && <span>osu mode</span>}
{!deviceInfo.mcu &&
!deviceInfo.final && (
<EnsureGenuine device={device} t={t}>
{/* <FirmwareUpdate
infos={{
targetId: deviceInfo.targetId,
version: deviceInfo.version,
}}
device={device}
t={t}
/> */}
<AppsList device={device} />
</EnsureGenuine>
)}
</Fragment>
)}
</EnsureDashboard>
)}
</EnsureDevice>
</Fragment>
)
}
}
export default compose(translate(), connect(mapStateToProps))(ManagerPage)
export default translate()(ManagerPage)

16
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<void> {
return createSocketDialog(transport, '/update/install', appParams)
}

14
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
}
}

12
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)

236
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<void> {
// return createSocketDialog(transport, '/update/install', appParams)
// }
/**
* Uninstall an app on the device
*/
export async function uninstallApp(
transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams },
): Promise<void> {
return createSocketDialog(transport, '/update/install', {
...appParams,
firmware: appParams.delete,
firmwareKey: appParams.deleteKey,
})
}
export async function getMemInfos(transport: Transport<*>): Promise<Object> {
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<void> {
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`],
})

13
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],
}

26
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<Result> => {
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
}
}

31
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
}
}

5
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))

43
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
}
}

2
src/internals/manager/installFinalFirmware.js → 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,

0
src/internals/manager/installMcu.js → src/helpers/firmware/installMcu.js

27
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
}
}

15
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<Command<any, any>> = [getAddress, signTransaction]
export const commands: Array<Command<any, any>> = [
getAddress,
signTransaction,
getDeviceInfo,
getFirmwareInfo,
getIsGenuine,
getLatestFirmwareForDevice,
installApp,
]

26
src/internals/manager/getFirmwareInfo.js

@ -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)

44
src/internals/manager/getLatestFirmwareForDevice.js

@ -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 })
}
}

62
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<void> {
return createSocketDialog(transport, '/update/install', appParams)
}
/**
* Uninstall an app on the device
*/
export async function uninstallApp(
transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams },
): Promise<void> {
return createSocketDialog(transport, '/update/install', {
...appParams,
firmware: appParams.delete,
firmwareKey: appParams.deleteKey,
})
}
export async function getMemInfos(transport: Transport<*>): Promise<Object> {
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`],
})

12
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<Command<any, any>> = [listApps]

12
src/internals/manager/installApp.js

@ -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)

24
src/internals/manager/installOsuFirmware.js

@ -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 })
}
}

14
src/internals/manager/listApps.js

@ -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 })
}
}

12
src/internals/manager/uninstallApp.js

@ -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)
Loading…
Cancel
Save