From 23b522f69ab90729903a725824564f52a6c450e5 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham Date: Wed, 18 Sep 2024 10:51:04 -0700 Subject: [PATCH] updates ledger.ts for upgrade to @zondax/ledger-js 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 --- ironfish-cli/src/utils/ledger.ts | 125 ++++++++++++++++++------------- 1 file changed, 73 insertions(+), 52 deletions(-) diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index fe472383ec..a5850907cf 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -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 @@ -22,6 +24,27 @@ export class Ledger { this.logger = logger ? logger : createRootLogger() } + tryInstruction = async (promise: Promise) => { + 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) @@ -29,23 +52,14 @@ export class Ledger { 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.') } @@ -63,19 +77,15 @@ 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 () => { @@ -83,41 +93,30 @@ export class Ledger { 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 = { @@ -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 +}