Skip to content

Commit

Permalink
updates ledger.ts for upgrade to @zondax/ledger-js
Browse files Browse the repository at this point in the history
removes response handling for error codes, which are no longer included in
responses

implements tryInstruction to handle errors from any app instructions

adds methods to help discriminate between KeyResponse types and checks type of
KeyResponse for each type of key

adds method to recognize Ledger response errors

defines classes for common recoverable Ledger errors: locked device and app not
open. the error code for the app not being open is identified in the docs as a
'technical error', but this is the error code we get when the app isn't open
  • Loading branch information
hughy committed Sep 19, 2024
1 parent 9eb6c6e commit 23b522f
Showing 1 changed file with 73 additions and 52 deletions.
125 changes: 73 additions & 52 deletions ironfish-cli/src/utils/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { AccountImport } from '@ironfish/sdk/src/wallet/exporter'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import IronfishApp, {
IronfishKeys,
KeyResponse,
ResponseAddress,
ResponseProofGenKey,
ResponseSign,
ResponseViewKey,
} from '@zondax/ledger-ironfish'
import { ResponseError } from '@zondax/ledger-js'

export class Ledger {
app: IronfishApp | undefined
Expand All @@ -22,30 +24,42 @@ export class Ledger {
this.logger = logger ? logger : createRootLogger()
}

tryInstruction = async <T>(promise: Promise<T>) => {
try {
return await promise
} catch (error: unknown) {
if (isResponseError(error)) {
this.logger.debug(`Ledger ResponseError returnCode: ${error.returnCode.toString(16)}`)
if (error.returnCode === LedgerDeviceLockedError.returnCode) {
throw new LedgerDeviceLockedError('Please unlock your Ledger device.')
} else if (error.returnCode === LedgerAppNotOpenError.returnCode) {
throw new LedgerAppNotOpenError(
'Please open the Iron Fish app on your Ledger device.',
)
}

throw new LedgerError(error.errorMessage)
}

throw error
}
}

connect = async () => {
const transport = await TransportNodeHid.create(3000, 3000)

if (transport.deviceModel) {
this.logger.debug(`${transport.deviceModel.productName} found.`)
}

const app = new IronfishApp(transport)
const app = new IronfishApp(transport, false)

const appInfo = await this.tryInstruction(app.appInfo())

const appInfo = await app.appInfo()
this.logger.debug(appInfo.appName ?? 'no app name')

if (appInfo.appName !== 'Ironfish') {
this.logger.debug(appInfo.appName ?? 'no app name')
this.logger.debug(appInfo.returnCode.toString(16))
this.logger.debug(appInfo.errorMessage.toString())

// references:
// https://github.com/LedgerHQ/ledger-live/blob/173bb3c84cc855f83ab8dc49362bc381afecc31e/libs/ledgerjs/packages/errors/src/index.ts#L263
// https://github.com/Zondax/ledger-ironfish/blob/bf43a4b8d403d15138699ee3bb1a3d6dfdb428bc/docs/APDUSPEC.md?plain=1#L25
if (appInfo.returnCode === 0x5515) {
throw new Error('Please unlock your Ledger device.')
}

throw new Error('Please open the Iron Fish app on your ledger device.')
}

Expand All @@ -63,61 +77,46 @@ export class Ledger {
throw new Error('Connect to Ledger first')
}

const response: ResponseAddress = await this.app.retrieveKeys(
this.PATH,
IronfishKeys.PublicAddress,
false,
const response = await this.tryInstruction(
this.app.retrieveKeys(this.PATH, IronfishKeys.PublicAddress, false),
)

if (!response.publicAddress) {
this.logger.debug(`No public address returned.`)
this.logger.debug(response.returnCode.toString())
throw new Error(response.errorMessage)
if (!isResponseAddress(response)) {
throw new Error(`No public address returned`)
} else {
return response.publicAddress.toString('hex')
}

return response.publicAddress.toString('hex')
}

importAccount = async () => {
if (!this.app) {
throw new Error('Connect to Ledger first')
}

const responseAddress: ResponseAddress = await this.app.retrieveKeys(
this.PATH,
IronfishKeys.PublicAddress,
false,
const responseAddress: KeyResponse = await this.tryInstruction(
this.app.retrieveKeys(this.PATH, IronfishKeys.PublicAddress, false),
)

if (!responseAddress.publicAddress) {
this.logger.debug(`No public address returned.`)
this.logger.debug(responseAddress.returnCode.toString())
throw new Error(responseAddress.errorMessage)
if (!isResponseAddress(responseAddress)) {
throw new Error(`No public address returned.`)
}

this.logger.log('Please confirm the request on your ledger device.')

const responseViewKey: ResponseViewKey = await this.app.retrieveKeys(
this.PATH,
IronfishKeys.ViewKey,
true,
const responseViewKey = await this.tryInstruction(
this.app.retrieveKeys(this.PATH, IronfishKeys.ViewKey, true),
)

if (!responseViewKey.viewKey || !responseViewKey.ovk || !responseViewKey.ivk) {
this.logger.debug(`No view key returned.`)
this.logger.debug(responseViewKey.returnCode.toString())
throw new Error(responseViewKey.errorMessage)
if (!isResponseViewKey(responseViewKey)) {
throw new Error(`No view key returned.`)
}

const responsePGK: ResponseProofGenKey = await this.app.retrieveKeys(
this.PATH,
IronfishKeys.ProofGenerationKey,
false,
const responsePGK: KeyResponse = await this.tryInstruction(
this.app.retrieveKeys(this.PATH, IronfishKeys.ProofGenerationKey, false),
)

if (!responsePGK.ak || !responsePGK.nsk) {
this.logger.debug(`No proof authorizing key returned.`)
throw new Error(responsePGK.errorMessage)
if (!isResponseProofGenKey(responsePGK)) {
throw new Error(`No proof authorizing key returned.`)
}

const accountImport: AccountImport = {
Expand Down Expand Up @@ -149,14 +148,36 @@ export class Ledger {
throw new Error('Transaction size is too large, must be less than 16kb.')
}

const response: ResponseSign = await this.app.sign(this.PATH, buffer)

if (!response.signature) {
this.logger.debug(`No signatures returned.`)
this.logger.debug(response.returnCode.toString())
throw new Error(response.errorMessage)
}
const response: ResponseSign = await this.tryInstruction(this.app.sign(this.PATH, buffer))

return response.signature
}
}

function isResponseAddress(response: KeyResponse): response is ResponseAddress {
return 'publicAddress' in response
}

function isResponseViewKey(response: KeyResponse): response is ResponseViewKey {
return 'viewKey' in response
}

function isResponseProofGenKey(response: KeyResponse): response is ResponseProofGenKey {
return 'ak' in response
}

function isResponseError(error: unknown): error is ResponseError {
return 'errorMessage' in (error as object) && 'returnCode' in (error as object)
}

export class LedgerError extends Error {
name = this.constructor.name
}

export class LedgerDeviceLockedError extends LedgerError {
static returnCode = 0x5515
}

export class LedgerAppNotOpenError extends LedgerError {
static returnCode = 0x6f00
}

0 comments on commit 23b522f

Please sign in to comment.