From 22d1ec0298a51aab4ee1e5de14e4d87f00cdc04d Mon Sep 17 00:00:00 2001 From: fAnselmi-Ledger Date: Tue, 21 Jan 2025 15:05:18 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(signer-solana):=20Improve?= =?UTF-8?q?=20solana=20error=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/seven-paws-marry.md | 5 ++ .../app-binder/GetAddressDeviceActionTypes.ts | 3 +- .../GetAppConfigurationDeviceActionTypes.ts | 4 +- .../SignMessageDeviceActionTypes.ts | 7 +- .../SignTransactionDeviceActionTypes.ts | 5 +- .../GetAppConfigurationCommand.test.ts | 14 ++-- .../command/GetAppConfigurationCommand.ts | 80 ++++++++++--------- .../command/GetPubKeyCommand.test.ts | 7 +- .../app-binder/command/GetPubKeyCommand.ts | 63 ++++++++------- .../SignOffChainMessageCommand.test.ts | 7 +- .../command/SignOffChainMessageCommand.ts | 52 ++++++------ .../command/SignTransactionCommand.ts | 64 ++++++++------- .../command/utils/SolanaAppErrors.test.ts | 61 ++++++++++++++ .../command/utils/SolanaApplicationErrors.ts | 32 ++++++++ .../command/utils/solanaAppError.test.ts | 75 ----------------- .../command/utils/solanaAppErrors.ts | 41 ---------- .../SignMessage/SignMessageDeviceAction.ts | 3 +- .../SignTransactionDeviceAction.ts | 13 ++- .../task/SendCommandInChunksTask.ts | 6 +- .../app-binder/task/SendSignDataTask.ts | 11 ++- .../app-binder/task/SendSignMessageTask.ts | 4 +- 21 files changed, 292 insertions(+), 265 deletions(-) create mode 100644 .changeset/seven-paws-marry.md create mode 100644 packages/signer/signer-solana/src/internal/app-binder/command/utils/SolanaAppErrors.test.ts create mode 100644 packages/signer/signer-solana/src/internal/app-binder/command/utils/SolanaApplicationErrors.ts delete mode 100644 packages/signer/signer-solana/src/internal/app-binder/command/utils/solanaAppError.test.ts delete mode 100644 packages/signer/signer-solana/src/internal/app-binder/command/utils/solanaAppErrors.ts diff --git a/.changeset/seven-paws-marry.md b/.changeset/seven-paws-marry.md new file mode 100644 index 000000000..5d7460d10 --- /dev/null +++ b/.changeset/seven-paws-marry.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-signer-kit-solana": patch +--- + +Improve error handling diff --git a/packages/signer/signer-solana/src/api/app-binder/GetAddressDeviceActionTypes.ts b/packages/signer/signer-solana/src/api/app-binder/GetAddressDeviceActionTypes.ts index d851aaaac..61ca8377a 100644 --- a/packages/signer/signer-solana/src/api/app-binder/GetAddressDeviceActionTypes.ts +++ b/packages/signer/signer-solana/src/api/app-binder/GetAddressDeviceActionTypes.ts @@ -7,13 +7,14 @@ import { } from "@ledgerhq/device-management-kit"; import { type PublicKey } from "@api/model/PublicKey"; +import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors"; type GetAddressDAUserInteractionRequired = | UserInteractionRequired.None | UserInteractionRequired.VerifyAddress; export type GetAddressDAOutput = SendCommandInAppDAOutput; -export type GetAddressDAError = SendCommandInAppDAError; +export type GetAddressDAError = SendCommandInAppDAError; export type GetAddressDAIntermediateValue = SendCommandInAppDAIntermediateValue; diff --git a/packages/signer/signer-solana/src/api/app-binder/GetAppConfigurationDeviceActionTypes.ts b/packages/signer/signer-solana/src/api/app-binder/GetAppConfigurationDeviceActionTypes.ts index e5e2543ad..d27e8c3ac 100644 --- a/packages/signer/signer-solana/src/api/app-binder/GetAppConfigurationDeviceActionTypes.ts +++ b/packages/signer/signer-solana/src/api/app-binder/GetAppConfigurationDeviceActionTypes.ts @@ -7,11 +7,13 @@ import { } from "@ledgerhq/device-management-kit"; import { type AppConfiguration } from "@api/model/AppConfiguration"; +import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors"; export type GetAppConfigurationDAOutput = SendCommandInAppDAOutput; -export type GetAppConfigurationDAError = SendCommandInAppDAError; +export type GetAppConfigurationDAError = + SendCommandInAppDAError; export type GetAppConfigurationDAIntermediateValue = SendCommandInAppDAIntermediateValue; diff --git a/packages/signer/signer-solana/src/api/app-binder/SignMessageDeviceActionTypes.ts b/packages/signer/signer-solana/src/api/app-binder/SignMessageDeviceActionTypes.ts index 289334468..d96a30f35 100644 --- a/packages/signer/signer-solana/src/api/app-binder/SignMessageDeviceActionTypes.ts +++ b/packages/signer/signer-solana/src/api/app-binder/SignMessageDeviceActionTypes.ts @@ -1,13 +1,14 @@ import { - type CommandErrorResult, type DeviceActionState, type ExecuteDeviceActionReturnType, type OpenAppDAError, type OpenAppDARequiredInteraction, + type SendCommandInAppDAError, type UserInteractionRequired, } from "@ledgerhq/device-management-kit"; import { type Signature } from "@api/model/Signature"; +import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors"; export type SignMessageDAOutput = Signature; @@ -16,7 +17,9 @@ export type SignMessageDAInput = { readonly message: string; }; -export type SignMessageDAError = OpenAppDAError | CommandErrorResult["error"]; +export type SignMessageDAError = + | OpenAppDAError + | SendCommandInAppDAError; type SignMessageDARequiredInteraction = | OpenAppDARequiredInteraction diff --git a/packages/signer/signer-solana/src/api/app-binder/SignTransactionDeviceActionTypes.ts b/packages/signer/signer-solana/src/api/app-binder/SignTransactionDeviceActionTypes.ts index 84dfc0723..5d303eceb 100644 --- a/packages/signer/signer-solana/src/api/app-binder/SignTransactionDeviceActionTypes.ts +++ b/packages/signer/signer-solana/src/api/app-binder/SignTransactionDeviceActionTypes.ts @@ -1,15 +1,16 @@ import { - type CommandErrorResult, type DeviceActionState, type ExecuteDeviceActionReturnType, type OpenAppDAError, type OpenAppDARequiredInteraction, + type SendCommandInAppDAError, type UserInteractionRequired, } from "@ledgerhq/device-management-kit"; import { type Signature } from "@api/model/Signature"; import { type Transaction } from "@api/model/Transaction"; import { type TransactionOptions } from "@api/model/TransactionOptions"; +import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors"; export type SignTransactionDAOutput = Signature; @@ -21,7 +22,7 @@ export type SignTransactionDAInput = { export type SignTransactionDAError = | OpenAppDAError - | CommandErrorResult["error"]; + | SendCommandInAppDAError; type SignTransactionDARequiredInteraction = | OpenAppDARequiredInteraction diff --git a/packages/signer/signer-solana/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts b/packages/signer/signer-solana/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts index d8fd0e8e4..2ae4416af 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts @@ -84,10 +84,9 @@ describe("GetAppConfigurationCommand", () => { if (!isSuccessCommandResult(result)) { expect(result.error).toEqual( expect.objectContaining({ - _tag: "InvalidStatusWordError", - originalError: expect.objectContaining({ - message: "Invalid response", - }), + _tag: "SolanaAppCommandError", + errorCode: "6a82", + message: "Invalid off-chain message format", }), ); } else { @@ -105,10 +104,9 @@ describe("GetAppConfigurationCommand", () => { if (!isSuccessCommandResult(result)) { expect(result.error).toEqual( expect.objectContaining({ - _tag: "InvalidStatusWordError", - originalError: expect.objectContaining({ - message: "Invalid response", - }), + _tag: "SolanaAppCommandError", + errorCode: "6a82", + message: "Invalid off-chain message format", }), ); } else { diff --git a/packages/signer/signer-solana/src/internal/app-binder/command/GetAppConfigurationCommand.ts b/packages/signer/signer-solana/src/internal/app-binder/command/GetAppConfigurationCommand.ts index 97fff9d6b..5e5b59391 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/command/GetAppConfigurationCommand.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/command/GetAppConfigurationCommand.ts @@ -7,22 +7,34 @@ import { type CommandResult, CommandResultFactory, InvalidStatusWordError, - isCommandErrorCode, } from "@ledgerhq/device-management-kit"; +import { CommandErrorHelper } from "@ledgerhq/signer-utils"; +import { Maybe } from "purify-ts"; import { type AppConfiguration } from "@api/model/AppConfiguration"; import { PublicKeyDisplayMode } from "@api/model/PublicKeyDisplayMode"; import { - SolanaAppCommandError, - solanaAppErrors, -} from "./utils/solanaAppErrors"; + SOLANA_APP_ERRORS, + SolanaAppCommandErrorFactory, + type SolanaAppErrorCodes, +} from "./utils/SolanaApplicationErrors"; type GetAppConfigurationCommandArgs = void; export class GetAppConfigurationCommand - implements Command + implements + Command< + AppConfiguration, + GetAppConfigurationCommandArgs, + SolanaAppErrorCodes + > { + private readonly errorHelper = new CommandErrorHelper< + AppConfiguration, + SolanaAppErrorCodes + >(SOLANA_APP_ERRORS, SolanaAppCommandErrorFactory); + args: GetAppConfigurationCommandArgs; constructor(args: GetAppConfigurationCommandArgs) { @@ -38,40 +50,36 @@ export class GetAppConfigurationCommand }).build(); } - parseResponse(response: ApduResponse): CommandResult { - const parser = new ApduParser(response); - const errorCode = parser.encodeToHexaString(response.statusCode); - if (isCommandErrorCode(errorCode, solanaAppErrors)) { - return CommandResultFactory({ - error: new SolanaAppCommandError({ - ...solanaAppErrors[errorCode], - errorCode, - }), - }); - } + parseResponse( + response: ApduResponse, + ): CommandResult { + return Maybe.fromNullable( + this.errorHelper.getError(response), + ).orDefaultLazy(() => { + const parser = new ApduParser(response); + const buffer = parser.extractFieldByLength(5); + if ( + !buffer || + buffer.length !== 5 || + buffer.some((element) => element === undefined) + ) { + return CommandResultFactory({ + error: new InvalidStatusWordError("Invalid response"), + }); + } + + const config: AppConfiguration = { + blindSigningEnabled: Boolean(buffer[0]), + pubKeyDisplayMode: + buffer[1] === 0 + ? PublicKeyDisplayMode.LONG + : PublicKeyDisplayMode.SHORT, + version: `${buffer[2]}.${buffer[3]}.${buffer[4]}`, + }; - const buffer = parser.extractFieldByLength(5); - if ( - !buffer || - buffer.length !== 5 || - buffer.some((element) => element === undefined) - ) { return CommandResultFactory({ - error: new InvalidStatusWordError("Invalid response"), + data: config, }); - } - - const config: AppConfiguration = { - blindSigningEnabled: Boolean(buffer[0]), - pubKeyDisplayMode: - buffer[1] === 0 - ? PublicKeyDisplayMode.LONG - : PublicKeyDisplayMode.SHORT, - version: `${buffer[2]}.${buffer[3]}.${buffer[4]}`, - }; - - return CommandResultFactory({ - data: config, }); } } diff --git a/packages/signer/signer-solana/src/internal/app-binder/command/GetPubKeyCommand.test.ts b/packages/signer/signer-solana/src/internal/app-binder/command/GetPubKeyCommand.test.ts index d427df215..230c0f10a 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/command/GetPubKeyCommand.test.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/command/GetPubKeyCommand.test.ts @@ -96,10 +96,9 @@ describe("GetPubKeyCommand", () => { if (!isSuccessCommandResult(result)) { expect(result.error).toEqual( expect.objectContaining({ - _tag: "InvalidStatusWordError", - originalError: expect.objectContaining({ - message: "Public key is missing", - }), + _tag: "SolanaAppCommandError", + errorCode: "6a82", + message: "Invalid off-chain message format", }), ); } else { diff --git a/packages/signer/signer-solana/src/internal/app-binder/command/GetPubKeyCommand.ts b/packages/signer/signer-solana/src/internal/app-binder/command/GetPubKeyCommand.ts index 08745dfe2..bdce44334 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/command/GetPubKeyCommand.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/command/GetPubKeyCommand.ts @@ -8,17 +8,21 @@ import { type CommandResult, CommandResultFactory, InvalidStatusWordError, - isCommandErrorCode, } from "@ledgerhq/device-management-kit"; -import { DerivationPathUtils } from "@ledgerhq/signer-utils"; +import { + CommandErrorHelper, + DerivationPathUtils, +} from "@ledgerhq/signer-utils"; import bs58 from "bs58"; +import { Maybe } from "purify-ts"; import { type PublicKey } from "@api/model/PublicKey"; import { - SolanaAppCommandError, - solanaAppErrors, -} from "./utils/solanaAppErrors"; + SOLANA_APP_ERRORS, + SolanaAppCommandErrorFactory, + type SolanaAppErrorCodes, +} from "./utils/SolanaApplicationErrors"; const PUBKEY_LENGTH = 32; @@ -29,8 +33,14 @@ type GetPubKeyCommandArgs = { }; export class GetPubKeyCommand - implements Command + implements + Command { + private readonly errorHelper = new CommandErrorHelper< + GetPubKeyCommandResponse, + SolanaAppErrorCodes + >(SOLANA_APP_ERRORS, SolanaAppCommandErrorFactory); + args: GetPubKeyCommandArgs; constructor(args: GetPubKeyCommandArgs) { @@ -58,33 +68,28 @@ export class GetPubKeyCommand parseResponse( response: ApduResponse, - ): CommandResult { - const parser = new ApduParser(response); - const errorCode = parser.encodeToHexaString(response.statusCode); - if (isCommandErrorCode(errorCode, solanaAppErrors)) { - return CommandResultFactory({ - error: new SolanaAppCommandError({ - ...solanaAppErrors[errorCode], - errorCode, - }), - }); - } + ): CommandResult { + return Maybe.fromNullable( + this.errorHelper.getError(response), + ).orDefaultLazy(() => { + const parser = new ApduParser(response); - if (parser.testMinimalLength(PUBKEY_LENGTH) === false) { - return CommandResultFactory({ - error: new InvalidStatusWordError("Public key is missing"), - }); - } + if (parser.testMinimalLength(PUBKEY_LENGTH) === false) { + return CommandResultFactory({ + error: new InvalidStatusWordError("Public key is missing"), + }); + } + + const buffer = parser.extractFieldByLength(PUBKEY_LENGTH); + if (buffer === undefined) { + return CommandResultFactory({ + error: new InvalidStatusWordError("Unable to extract public key"), + }); + } - const buffer = parser.extractFieldByLength(PUBKEY_LENGTH); - if (buffer === undefined) { return CommandResultFactory({ - error: new InvalidStatusWordError("Unable to extract public key"), + data: bs58.encode(buffer), }); - } - - return CommandResultFactory({ - data: bs58.encode(buffer), }); } } diff --git a/packages/signer/signer-solana/src/internal/app-binder/command/SignOffChainMessageCommand.test.ts b/packages/signer/signer-solana/src/internal/app-binder/command/SignOffChainMessageCommand.test.ts index 8ba9f43a6..596eb8b95 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/command/SignOffChainMessageCommand.test.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/command/SignOffChainMessageCommand.test.ts @@ -70,10 +70,9 @@ describe("SignOffChainMessageCommand", () => { if (!isSuccessCommandResult(result)) { expect(result.error).toEqual( expect.objectContaining({ - _tag: "InvalidStatusWordError", // Adjust this based on your actual implementation - originalError: expect.objectContaining({ - message: "Signature extraction failed", - }), + _tag: "SolanaAppCommandError", + errorCode: "6a82", + message: "Invalid off-chain message format", }), ); } else { diff --git a/packages/signer/signer-solana/src/internal/app-binder/command/SignOffChainMessageCommand.ts b/packages/signer/signer-solana/src/internal/app-binder/command/SignOffChainMessageCommand.ts index de0a98e3b..839f2f1f2 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/command/SignOffChainMessageCommand.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/command/SignOffChainMessageCommand.ts @@ -7,15 +7,17 @@ import { type CommandResult, CommandResultFactory, InvalidStatusWordError, - isCommandErrorCode, } from "@ledgerhq/device-management-kit"; +import { CommandErrorHelper } from "@ledgerhq/signer-utils"; +import { Maybe } from "purify-ts"; import { type Signature } from "@api/model/Signature"; import { - SolanaAppCommandError, - solanaAppErrors, -} from "./utils/solanaAppErrors"; + SOLANA_APP_ERRORS, + SolanaAppCommandErrorFactory, + type SolanaAppErrorCodes, +} from "./utils/SolanaApplicationErrors"; const SIGNATURE_LENGTH = 64; @@ -27,8 +29,17 @@ export type SignOffChainMessageCommandArgs = { export class SignOffChainMessageCommand implements - Command + Command< + SignOffChainMessageCommandResponse, + SignOffChainMessageCommandArgs, + SolanaAppErrorCodes + > { + private readonly errorHelper = new CommandErrorHelper< + SignOffChainMessageCommandResponse, + SolanaAppErrorCodes + >(SOLANA_APP_ERRORS, SolanaAppCommandErrorFactory); + args: SignOffChainMessageCommandArgs; constructor(args: SignOffChainMessageCommandArgs) { @@ -48,27 +59,22 @@ export class SignOffChainMessageCommand parseResponse( response: ApduResponse, - ): CommandResult { - const parser = new ApduParser(response); - const errorCode = parser.encodeToHexaString(response.statusCode); - if (isCommandErrorCode(errorCode, solanaAppErrors)) { - return CommandResultFactory({ - error: new SolanaAppCommandError({ - ...solanaAppErrors[errorCode], - errorCode, - }), - }); - } + ): CommandResult { + return Maybe.fromNullable( + this.errorHelper.getError(response), + ).orDefaultLazy(() => { + const parser = new ApduParser(response); + + const signature = parser.extractFieldByLength(SIGNATURE_LENGTH); + if (!signature) { + return CommandResultFactory({ + error: new InvalidStatusWordError("Signature extraction failed"), + }); + } - const signature = parser.extractFieldByLength(SIGNATURE_LENGTH); - if (!signature) { return CommandResultFactory({ - error: new InvalidStatusWordError("Signature extraction failed"), + data: signature, }); - } - - return CommandResultFactory({ - data: signature, }); } } diff --git a/packages/signer/signer-solana/src/internal/app-binder/command/SignTransactionCommand.ts b/packages/signer/signer-solana/src/internal/app-binder/command/SignTransactionCommand.ts index 82c6758fc..4396875d3 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/command/SignTransactionCommand.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/command/SignTransactionCommand.ts @@ -8,16 +8,17 @@ import { type CommandResult, CommandResultFactory, InvalidStatusWordError, - isCommandErrorCode, } from "@ledgerhq/device-management-kit"; -import { Just, type Maybe, Nothing } from "purify-ts"; +import { CommandErrorHelper } from "@ledgerhq/signer-utils"; +import { Just, Maybe, Nothing } from "purify-ts"; import { type Signature } from "@api/model/Signature"; import { - SolanaAppCommandError, - solanaAppErrors, -} from "./utils/solanaAppErrors"; + SOLANA_APP_ERRORS, + SolanaAppCommandErrorFactory, + type SolanaAppErrorCodes, +} from "./utils/SolanaApplicationErrors"; const SIGNATURE_LENGTH = 64; @@ -32,8 +33,18 @@ export type SignTransactionCommandArgs = { }; export class SignTransactionCommand - implements Command + implements + Command< + SignTransactionCommandResponse, + SignTransactionCommandArgs, + SolanaAppErrorCodes + > { + private readonly errorHelper = new CommandErrorHelper< + SignTransactionCommandResponse, + SolanaAppErrorCodes + >(SOLANA_APP_ERRORS, SolanaAppCommandErrorFactory); + args: SignTransactionCommandArgs; constructor(args: SignTransactionCommandArgs) { @@ -60,33 +71,28 @@ export class SignTransactionCommand parseResponse( response: ApduResponse, - ): CommandResult { - const parser = new ApduParser(response); - const errorCode = parser.encodeToHexaString(response.statusCode); - if (isCommandErrorCode(errorCode, solanaAppErrors)) { - return CommandResultFactory({ - error: new SolanaAppCommandError({ - ...solanaAppErrors[errorCode], - errorCode, - }), - }); - } + ): CommandResult { + return Maybe.fromNullable( + this.errorHelper.getError(response), + ).orDefaultLazy(() => { + const parser = new ApduParser(response); - if (parser.getUnparsedRemainingLength() === 0) { - return CommandResultFactory({ - data: Nothing, - }); - } + if (parser.getUnparsedRemainingLength() === 0) { + return CommandResultFactory({ + data: Nothing, + }); + } + + const signature = parser.extractFieldByLength(SIGNATURE_LENGTH); + if (!signature) { + return CommandResultFactory({ + error: new InvalidStatusWordError("Signature is missing"), + }); + } - const signature = parser.extractFieldByLength(SIGNATURE_LENGTH); - if (!signature) { return CommandResultFactory({ - error: new InvalidStatusWordError("Signature is missing"), + data: Just(signature), }); - } - - return CommandResultFactory({ - data: Just(signature), }); } } diff --git a/packages/signer/signer-solana/src/internal/app-binder/command/utils/SolanaAppErrors.test.ts b/packages/signer/signer-solana/src/internal/app-binder/command/utils/SolanaAppErrors.test.ts new file mode 100644 index 000000000..e1f8fa22d --- /dev/null +++ b/packages/signer/signer-solana/src/internal/app-binder/command/utils/SolanaAppErrors.test.ts @@ -0,0 +1,61 @@ +import { DeviceExchangeError } from "@ledgerhq/device-management-kit"; + +import { + SOLANA_APP_ERRORS, + SolanaAppCommandError, + type SolanaAppErrorCodes, +} from "./SolanaApplicationErrors"; + +describe("SolanaAppCommandError", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + jest.resetModules(); + }); + + it("should be an instance of DeviceExchangeError", () => { + const error = new SolanaAppCommandError({ + message: "Test error message", + errorCode: "6700", + }); + + expect(error).toBeInstanceOf(DeviceExchangeError); + }); + + it("should set the correct message when provided", () => { + const customMessage = "Custom error message"; + const error = new SolanaAppCommandError({ + message: customMessage, + errorCode: "6700", + }); + + expect(error.message).toBe(customMessage); + }); + + it("should set the correct customErrorCode", () => { + const errorCode: SolanaAppErrorCodes = "6a80"; + const error = new SolanaAppCommandError({ + message: "Invalid data", + errorCode, + }); + + expect(error.errorCode).toBe(errorCode); + }); + + it("should correlate error codes with messages from solanaAppErrors", () => { + const errorCode: SolanaAppErrorCodes = "6b00"; + const expectedMessage = SOLANA_APP_ERRORS[errorCode].message; + + const error = new SolanaAppCommandError({ + message: expectedMessage, + errorCode, + }); + + expect(error.errorCode).toBe(errorCode); + expect(error.message).toBe(expectedMessage); + + expect(error).toBeInstanceOf(DeviceExchangeError); + }); +}); diff --git a/packages/signer/signer-solana/src/internal/app-binder/command/utils/SolanaApplicationErrors.ts b/packages/signer/signer-solana/src/internal/app-binder/command/utils/SolanaApplicationErrors.ts new file mode 100644 index 000000000..c0b3a733f --- /dev/null +++ b/packages/signer/signer-solana/src/internal/app-binder/command/utils/SolanaApplicationErrors.ts @@ -0,0 +1,32 @@ +import { + type CommandErrorArgs, + type CommandErrors, + DeviceExchangeError, +} from "@ledgerhq/device-management-kit"; + +export type SolanaAppErrorCodes = + | "6700" + | "6982" + | "6a80" + | "6a81" + | "6a82" + | "6b00"; + +export const SOLANA_APP_ERRORS: CommandErrors = { + "6700": { message: "Incorrect length" }, + "6982": { message: "Security status not satisfied (Canceled by user)" }, + "6a80": { message: "Invalid data" }, + "6a81": { message: "Invalid off-chain message header" }, + "6a82": { message: "Invalid off-chain message format" }, + "6b00": { message: "Incorrect parameter P1 or P2" }, +}; + +export class SolanaAppCommandError extends DeviceExchangeError { + constructor(args: CommandErrorArgs) { + super({ tag: "SolanaAppCommandError", ...args }); + } +} + +export const SolanaAppCommandErrorFactory = ( + args: CommandErrorArgs, +) => new SolanaAppCommandError(args); diff --git a/packages/signer/signer-solana/src/internal/app-binder/command/utils/solanaAppError.test.ts b/packages/signer/signer-solana/src/internal/app-binder/command/utils/solanaAppError.test.ts deleted file mode 100644 index daa108ad9..000000000 --- a/packages/signer/signer-solana/src/internal/app-binder/command/utils/solanaAppError.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { DeviceExchangeError } from "@ledgerhq/device-management-kit"; - -import { SolanaAppCommandError } from "./solanaAppErrors"; - -describe("SolanaAppCommandError", () => { - it("should correctly instantiate with a message and error code", () => { - const message = "Invalid data"; - const errorCode = "6A80"; - - const error = new SolanaAppCommandError({ - message, - errorCode, - }); - - expect(error).toBeInstanceOf(SolanaAppCommandError); - expect(error.message).toBe(message); - expect(error.customErrorCode).toBe(errorCode); - expect(error._tag).toBe("SolanaAppCommandError"); - }); - - it("should correctly instantiate without an error code", () => { - const message = "Some generic error"; - - const error = new SolanaAppCommandError({ - message, - errorCode: undefined, - }); - - expect(error).toBeInstanceOf(SolanaAppCommandError); - expect(error.message).toBe(message); - expect(error.customErrorCode).toBeUndefined(); - expect(error._tag).toBe("SolanaAppCommandError"); - }); - - it("should handle default message if none is provided", () => { - const error = new SolanaAppCommandError({ - message: "An error occurred during device exchange.", - errorCode: "6A80", - }); - - expect(error).toBeInstanceOf(SolanaAppCommandError); - expect(error.message).toBe("An error occurred during device exchange."); - expect(error.customErrorCode).toBe("6A80"); - expect(error._tag).toBe("SolanaAppCommandError"); - }); - - it("should extend DeviceExchangeError", () => { - const message = "Security status not satisfied"; - const errorCode = "6982"; - - const error = new SolanaAppCommandError({ - message, - errorCode, - }); - - expect(error).toBeInstanceOf(DeviceExchangeError); - expect(error).toBeInstanceOf(SolanaAppCommandError); - expect(error._tag).toBe("SolanaAppCommandError"); - expect(error.message).toBe(message); - expect(error.customErrorCode).toBe(errorCode); - }); - - it("should allow custom properties to be checked", () => { - const message = "Invalid parameter"; - const errorCode = "6B00"; - - const error = new SolanaAppCommandError({ - message, - errorCode, - }); - - expect(error.customErrorCode).toBe("6B00"); - expect(error.customErrorCode).not.toBe("6A80"); - }); -}); diff --git a/packages/signer/signer-solana/src/internal/app-binder/command/utils/solanaAppErrors.ts b/packages/signer/signer-solana/src/internal/app-binder/command/utils/solanaAppErrors.ts deleted file mode 100644 index 29ee35d0c..000000000 --- a/packages/signer/signer-solana/src/internal/app-binder/command/utils/solanaAppErrors.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - type CommandErrorArgs, - type CommandErrors, - DeviceExchangeError, - type DmkError, -} from "@ledgerhq/device-management-kit"; - -export type SolanaAppErrorCodes = - | "6700" - | "6982" - | "6A80" - | "6A81" - | "6A82" - | "6B00"; - -export const solanaAppErrors: CommandErrors = { - "6700": { message: "Incorrect length" }, - "6982": { message: "Security status not satisfied (Canceled by user)" }, - "6A80": { message: "Invalid data" }, - "6A81": { message: "Invalid off-chain message header" }, - "6A82": { message: "Invalid off-chain message format" }, - "6B00": { message: "Incorrect parameter P1 or P2" }, -}; - -export class SolanaAppCommandError - extends DeviceExchangeError - implements DmkError -{ - constructor({ - message, - errorCode, - }: CommandErrorArgs) { - super({ - tag: "SolanaAppCommandError", - message: message || "An error occurred during device exchange.", - errorCode: undefined, - }); - this.customErrorCode = errorCode || undefined; - } - customErrorCode?: SolanaAppErrorCodes; -} diff --git a/packages/signer/signer-solana/src/internal/app-binder/device-action/SignMessage/SignMessageDeviceAction.ts b/packages/signer/signer-solana/src/internal/app-binder/device-action/SignMessage/SignMessageDeviceAction.ts index 1e9965967..f9fd41647 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/device-action/SignMessage/SignMessageDeviceAction.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/device-action/SignMessage/SignMessageDeviceAction.ts @@ -21,6 +21,7 @@ import { type SignMessageDAOutput, } from "@api/app-binder/SignMessageDeviceActionTypes"; import { type Signature } from "@api/model/Signature"; +import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors"; import { SendSignMessageTask, type SendSignMessageTaskArgs, @@ -29,7 +30,7 @@ import { export type MachineDependencies = { readonly signMessage: (arg0: { input: SendSignMessageTaskArgs; - }) => Promise>; + }) => Promise>; }; export type ExtractMachineDependencies = ( diff --git a/packages/signer/signer-solana/src/internal/app-binder/device-action/SignTransactionDeviceAction.ts b/packages/signer/signer-solana/src/internal/app-binder/device-action/SignTransactionDeviceAction.ts index 0d695eac2..3d74f1c42 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/device-action/SignTransactionDeviceAction.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/device-action/SignTransactionDeviceAction.ts @@ -21,6 +21,7 @@ import { } from "@api/app-binder/SignTransactionDeviceActionTypes"; import { type Signature } from "@api/model/Signature"; import { SignTransactionCommand } from "@internal/app-binder/command/SignTransactionCommand"; +import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors"; import { SignDataTask } from "@internal/app-binder/task/SendSignDataTask"; export type MachineDependencies = { @@ -29,7 +30,9 @@ export type MachineDependencies = { derivationPath: string; serializedTransaction: Uint8Array; }; - }) => Promise>>; + }) => Promise< + CommandResult, SolanaAppErrorCodes> + >; }; export class SignTransactionDeviceAction extends XStateDeviceAction< @@ -168,10 +171,14 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< error: event.output.error, }; - if (event.output.data.isJust()) + const data = event.output.data.extract(); + if ( + event.output.data.isJust() && + data instanceof Uint8Array + ) return { ...context._internalState, - signature: event.output.data.extract(), + signature: data, }; return { diff --git a/packages/signer/signer-solana/src/internal/app-binder/task/SendCommandInChunksTask.ts b/packages/signer/signer-solana/src/internal/app-binder/task/SendCommandInChunksTask.ts index 9710940a7..25da0b7a7 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/task/SendCommandInChunksTask.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/task/SendCommandInChunksTask.ts @@ -9,6 +9,8 @@ import { isSuccessCommandResult, } from "@ledgerhq/device-management-kit"; +import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors"; + export type SendCommandInChunksTaskArgs = { data: Uint8Array; commandFactory: CommandFactory; @@ -16,7 +18,7 @@ export type SendCommandInChunksTaskArgs = { export type CommandFactory = ( args: ChunkableCommandArgs, -) => Command; +) => Command; export type ChunkableCommandArgs = { chunkedData: Uint8Array; @@ -30,7 +32,7 @@ export class SendCommandInChunksTask { private args: SendCommandInChunksTaskArgs, ) {} - async run(): Promise> { + async run(): Promise> { const { data: fullPayload, commandFactory } = this.args; const dataBuffer = new ByteArrayBuilder(fullPayload.length) diff --git a/packages/signer/signer-solana/src/internal/app-binder/task/SendSignDataTask.ts b/packages/signer/signer-solana/src/internal/app-binder/task/SendSignDataTask.ts index 2ecd1791f..82899f22f 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/task/SendSignDataTask.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/task/SendSignDataTask.ts @@ -7,6 +7,7 @@ import { DerivationPathUtils } from "@ledgerhq/signer-utils"; import { type Maybe } from "purify-ts"; import { type Signature } from "@api/model/Signature"; +import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors"; import { type CommandFactory, @@ -18,7 +19,7 @@ const PATH_SIZE = 4; type SignDataTaskArgs = { sendingData: Uint8Array; derivationPath: string; - commandFactory: CommandFactory>; + commandFactory: CommandFactory>; }; export class SignDataTask { @@ -27,7 +28,9 @@ export class SignDataTask { private args: SignDataTaskArgs, ) {} - async run(): Promise, void>> { + async run(): Promise< + CommandResult, SolanaAppErrorCodes> + > { const { sendingData, derivationPath, commandFactory } = this.args; const paths = DerivationPathUtils.splitPath(derivationPath); @@ -43,7 +46,9 @@ export class SignDataTask { builder.addBufferToData(sendingData); const buffer = builder.build(); - return await new SendCommandInChunksTask>(this.api, { + return await new SendCommandInChunksTask< + Maybe + >(this.api, { data: buffer, commandFactory, }).run(); diff --git a/packages/signer/signer-solana/src/internal/app-binder/task/SendSignMessageTask.ts b/packages/signer/signer-solana/src/internal/app-binder/task/SendSignMessageTask.ts index 6613f7671..779f1b37e 100644 --- a/packages/signer/signer-solana/src/internal/app-binder/task/SendSignMessageTask.ts +++ b/packages/signer/signer-solana/src/internal/app-binder/task/SendSignMessageTask.ts @@ -5,7 +5,9 @@ import { } from "@ledgerhq/device-management-kit"; import { DerivationPathUtils } from "@ledgerhq/signer-utils"; +import { type Signature } from "@api/index"; import { SignOffChainMessageCommand } from "@internal/app-binder/command/SignOffChainMessageCommand"; +import { type SolanaAppErrorCodes } from "@internal/app-binder/command/utils/SolanaApplicationErrors"; export type SendSignMessageTaskArgs = { sendingData: Uint8Array; @@ -13,7 +15,7 @@ export type SendSignMessageTaskArgs = { }; export type SendSignMessageTaskRunFunctionReturn = Promise< - CommandResult + CommandResult >; export class SendSignMessageTask {