Skip to content

Commit

Permalink
✨ (signer-btc): Use of CommandErrorHelper
Browse files Browse the repository at this point in the history
  • Loading branch information
jdabbech-ledger committed Jan 2, 2025
1 parent a7c9b56 commit bc4505a
Show file tree
Hide file tree
Showing 19 changed files with 511 additions and 443 deletions.
5 changes: 5 additions & 0 deletions .changeset/young-horses-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-signer-kit-bitcoin": patch
---

Use CommandErrorHelper in BTC commands
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {
ApduResponse,
CommandResultFactory,
GlobalCommandErrorHandler,
} from "@ledgerhq/device-management-kit";

import { SW_INTERRUPTED_EXECUTION } from "@internal/app-binder/command/utils/constants";
import { CommandUtils as BtcCommandUtils } from "@internal/utils/CommandUtils";

import { ContinueCommand } from "./ContinueCommand";

Expand All @@ -26,21 +24,10 @@ describe("ContinueCommand", (): void => {
0xef, // Payload data
]);

const parser = (response: ApduResponse) => {
if (BtcCommandUtils.isContinueResponse(response)) {
return CommandResultFactory({
data: response,
});
}
return CommandResultFactory({
error: GlobalCommandErrorHandler.handle(response),
});
};

describe("getApdu", () => {
it("should return correct APDU for given payload", () => {
// given
const command = new ContinueCommand(defaultArgs, parser);
const command = new ContinueCommand(defaultArgs);
// when
const apdu = command.getApdu();
// then
Expand All @@ -51,7 +38,7 @@ describe("ContinueCommand", (): void => {
describe("parseResponse", () => {
it("should return the APDU response if it's a continue response", () => {
// given
const command = new ContinueCommand(defaultArgs, parser);
const command = new ContinueCommand(defaultArgs);
const continueResponseData = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
const apduResponse = new ApduResponse({
statusCode: SW_INTERRUPTED_EXECUTION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,38 @@ import {
type ApduResponse,
type Command,
type CommandResult,
CommandResultFactory,
} from "@ledgerhq/device-management-kit";
import { CommandErrorHelper } from "@ledgerhq/signer-utils";
import { Maybe } from "purify-ts";

import {
BTC_APP_ERRORS,
BtcAppCommandErrorFactory,
type BtcErrorCodes,
} from "@internal/app-binder/command/utils/bitcoinAppErrors";
import { BtcCommandUtils } from "@internal/utils/BtcCommandUtils";

export type ContinueCommandArgs = {
payload: Uint8Array;
};

export class ContinueCommand<ResType>
implements Command<ResType, ContinueCommandArgs>
export type ContinueCommandResponse = ApduResponse;

export class ContinueCommand
implements
Command<ContinueCommandResponse, ContinueCommandArgs, BtcErrorCodes>
{
constructor(
private readonly args: ContinueCommandArgs,
private readonly parseFn: (
response: ApduResponse,
) => CommandResult<ResType>,
private readonly _args: ContinueCommandArgs,
private readonly _errorHelper = new CommandErrorHelper<
ContinueCommandResponse,
BtcErrorCodes
>(
BTC_APP_ERRORS,
BtcAppCommandErrorFactory,
BtcCommandUtils.isSuccessResponse,
),
) {}

getApdu(): Apdu {
Expand All @@ -27,11 +45,17 @@ export class ContinueCommand<ResType>
p1: 0x00,
p2: 0x00,
})
.addBufferToData(this.args.payload)
.addBufferToData(this._args.payload)
.build();
}

parseResponse(response: ApduResponse): CommandResult<ResType> {
return this.parseFn(response);
parseResponse(
response: ApduResponse,
): CommandResult<ContinueCommandResponse, BtcErrorCodes> {
return Maybe.fromNullable(this._errorHelper.getError(response)).orDefault(
CommandResultFactory({
data: response,
}),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,11 @@ describe("GetExtendedPublicKeyCommand", () => {
const result = command.parseResponse(response);

// THEN
if (!isSuccessCommandResult(result)) {
expect(result.error).toEqual(
new InvalidStatusWordError("Invalid response length"),
);
} else {
fail("Expected an error, but the result was successful");
}
expect(result).toStrictEqual(
CommandResultFactory({
error: new InvalidStatusWordError("Invalid response length"),
}),
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,20 @@ import {
type Command,
type CommandResult,
CommandResultFactory,
CommandUtils,
GlobalCommandErrorHandler,
InvalidStatusWordError,
} from "@ledgerhq/device-management-kit";
import { DerivationPathUtils } from "@ledgerhq/signer-utils";
import {
CommandErrorHelper,
DerivationPathUtils,
} from "@ledgerhq/signer-utils";
import { Maybe } from "purify-ts";

import {
BTC_APP_ERRORS,
BtcAppCommandErrorFactory,
type BtcErrorCodes,
} from "@internal/app-binder/command/utils/bitcoinAppErrors";
import { BtcCommandUtils } from "@internal/utils/BtcCommandUtils";

const STATUS_CODE_LENGTH = 2;

Expand All @@ -29,13 +38,24 @@ export class GetExtendedPublicKeyCommand
implements
Command<
GetExtendedPublicKeyCommandResponse,
GetExtendedPublicKeyCommandArgs
GetExtendedPublicKeyCommandArgs,
BtcErrorCodes
>
{
constructor(private readonly args: GetExtendedPublicKeyCommandArgs) {}
constructor(
private readonly _args: GetExtendedPublicKeyCommandArgs,
private readonly _errorHelper = new CommandErrorHelper<
GetExtendedPublicKeyCommandResponse,
BtcErrorCodes
>(
BTC_APP_ERRORS,
BtcAppCommandErrorFactory,
BtcCommandUtils.isSuccessResponse,
),
) {}

getApdu(): Apdu {
const { checkOnDevice, derivationPath } = this.args;
const { checkOnDevice, derivationPath } = this._args;

const getExtendedPublicKeyArgs: ApduBuilderArgs = {
cla: 0xe1,
Expand All @@ -58,31 +78,28 @@ export class GetExtendedPublicKeyCommand

parseResponse(
response: ApduResponse,
): CommandResult<GetExtendedPublicKeyCommandResponse> {
const parser = new ApduParser(response);
): CommandResult<GetExtendedPublicKeyCommandResponse, BtcErrorCodes> {
return Maybe.fromNullable(
this._errorHelper.getError(response),
).orDefaultLazy(() => {
const parser = new ApduParser(response);
const length = parser.getUnparsedRemainingLength() - STATUS_CODE_LENGTH;

if (!CommandUtils.isSuccessResponse(response)) {
return CommandResultFactory({
error: GlobalCommandErrorHandler.handle(response),
});
}
if (length <= 0) {
return CommandResultFactory({
error: new InvalidStatusWordError("Invalid response length"),
});
}

const length = parser.getUnparsedRemainingLength() - STATUS_CODE_LENGTH;
const extendedPublicKey = parser.encodeToString(
parser.extractFieldByLength(length),
);

if (length <= 0) {
return CommandResultFactory({
error: new InvalidStatusWordError("Invalid response length"),
data: {
extendedPublicKey,
},
});
}

const extendedPublicKey = parser.encodeToString(
parser.extractFieldByLength(length),
);

return CommandResultFactory({
data: {
extendedPublicKey,
},
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe("GetMasterFingerprintCommand", () => {
expect(result).toEqual(
CommandResultFactory({
data: {
masterFingerprint: "828dc2f3",
masterFingerprint: Uint8Array.from([0x82, 0x8d, 0xc2, 0xf3]),
},
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,37 @@ import {
type Command,
type CommandResult,
CommandResultFactory,
CommandUtils,
GlobalCommandErrorHandler,
InvalidStatusWordError,
} from "@ledgerhq/device-management-kit";
import { CommandErrorHelper } from "@ledgerhq/signer-utils";
import { Maybe } from "purify-ts";

import {
BTC_APP_ERRORS,
BtcAppCommandErrorFactory,
type BtcErrorCodes,
} from "@internal/app-binder/command/utils/bitcoinAppErrors";
import { BtcCommandUtils } from "@internal/utils/BtcCommandUtils";

const MASTER_FINGERPRINT_LENGTH = 4;

type GetMasterFingerprintCommandResponse = {
masterFingerprint: string;
masterFingerprint: Uint8Array;
};

export class GetMasterFingerprintCommand
implements Command<GetMasterFingerprintCommandResponse, void>
implements Command<GetMasterFingerprintCommandResponse, void, BtcErrorCodes>
{
constructor(
private readonly _errorHelper = new CommandErrorHelper<
GetMasterFingerprintCommandResponse,
BtcErrorCodes
>(
BTC_APP_ERRORS,
BtcAppCommandErrorFactory,
BtcCommandUtils.isSuccessResponse,
),
) {}
getApdu(): Apdu {
const getMasterFingerprintArgs: ApduBuilderArgs = {
cla: 0xe1,
Expand All @@ -34,29 +51,26 @@ export class GetMasterFingerprintCommand

parseResponse(
response: ApduResponse,
): CommandResult<GetMasterFingerprintCommandResponse> {
const parser = new ApduParser(response);
): CommandResult<GetMasterFingerprintCommandResponse, BtcErrorCodes> {
return Maybe.fromNullable(
this._errorHelper.getError(response),
).orDefaultLazy(() => {
const parser = new ApduParser(response);

if (!CommandUtils.isSuccessResponse(response)) {
return CommandResultFactory({
error: GlobalCommandErrorHandler.handle(response),
});
}
const masterFingerprint = parser.extractFieldByLength(
MASTER_FINGERPRINT_LENGTH,
);
if (!masterFingerprint) {
return CommandResultFactory({
error: new InvalidStatusWordError("Master fingerprint is missing"),
});
}

if (!parser.testMinimalLength(MASTER_FINGERPRINT_LENGTH)) {
return CommandResultFactory({
error: new InvalidStatusWordError("Master fingerprint is missing"),
data: {
masterFingerprint,
},
});
}

const masterFingerprint = parser.encodeToHexaString(
parser.extractFieldByLength(MASTER_FINGERPRINT_LENGTH),
);

return CommandResultFactory({
data: {
masterFingerprint,
},
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import {
isSuccessCommandResult,
} from "@ledgerhq/device-management-kit";

import {
BitcoinAppCommandError,
bitcoinAppErrors,
} from "./utils/bitcoinAppErrors";
import { BTC_APP_ERRORS, BtcAppCommandError } from "./utils/bitcoinAppErrors";
import {
GetWalletAddressCommand,
type GetWalletAddressCommandArgs,
Expand Down Expand Up @@ -113,10 +110,9 @@ describe("GetWalletAddressCommand", () => {

expect(isSuccessCommandResult(result)).toBe(false);
if (!isSuccessCommandResult(result)) {
expect(result.error).toBeInstanceOf(BitcoinAppCommandError);
const error = result.error as BitcoinAppCommandError;
expect(error.customErrorCode).toBe("6985");
const expectedErrorInfo = bitcoinAppErrors["6985"];
expect(result.error).toBeInstanceOf(BtcAppCommandError);
const error = result.error as BtcAppCommandError;
const expectedErrorInfo = BTC_APP_ERRORS["6985"];
expect(expectedErrorInfo).toBeDefined();
if (expectedErrorInfo) {
expect(error.message).toBe(expectedErrorInfo.message);
Expand Down
Loading

0 comments on commit bc4505a

Please sign in to comment.