-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ (btc-signer) [DSDK-465]: Add GetWalletAddressCommand (#512)
- Loading branch information
Showing
5 changed files
with
356 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@ledgerhq/device-signer-kit-bitcoin": minor | ||
--- | ||
|
||
Add GetWalletAddressCommand |
149 changes: 149 additions & 0 deletions
149
packages/signer/signer-btc/src/internal/app-binder/command/GetWalletAddressCommand.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import { | ||
ApduResponse, | ||
CommandResultFactory, | ||
isSuccessCommandResult, | ||
} from "@ledgerhq/device-management-kit"; | ||
|
||
import { | ||
BitcoinAppCommandError, | ||
bitcoinAppErrors, | ||
} from "./utils/bitcoinAppErrors"; | ||
import { | ||
GetWalletAddressCommand, | ||
type GetWalletAddressCommandArgs, | ||
} from "./GetWalletAddressCommand"; | ||
|
||
describe("GetWalletAddressCommand", () => { | ||
let command: GetWalletAddressCommand; | ||
const defaultArgs: GetWalletAddressCommandArgs = { | ||
display: true, | ||
walletId: Uint8Array.from("walletIdBuffer", (c) => c.charCodeAt(0)), | ||
walletHmac: Uint8Array.from("walletHmacBuffer", (c) => c.charCodeAt(0)), | ||
change: false, | ||
addressIndex: 0x00000000, | ||
}; | ||
|
||
beforeEach(() => { | ||
command = new GetWalletAddressCommand(defaultArgs); | ||
jest.clearAllMocks(); | ||
jest.requireActual("@ledgerhq/device-management-kit"); | ||
}); | ||
|
||
describe("getApdu", () => { | ||
it("should return correct APDU for default arguments", () => { | ||
const apdu = command.getApdu(); | ||
const expectedApdu = Uint8Array.from([ | ||
0xe1, // CLA | ||
0x03, // INS | ||
0x00, // P1 | ||
0x01, // P2 | ||
0x24, // Length of data: 36 bytes | ||
0x01, // display: true | ||
...Uint8Array.from("walletIdBuffer", (c) => c.charCodeAt(0)), | ||
...Uint8Array.from("walletHmacBuffer", (c) => c.charCodeAt(0)), | ||
0x00, // change: false | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x00, // addressIndex: 0x00000000 | ||
]); | ||
expect(apdu.getRawApdu()).toEqual(expectedApdu); | ||
}); | ||
|
||
it("should return correct APDU for different arguments", () => { | ||
const args: GetWalletAddressCommandArgs = { | ||
display: false, | ||
walletId: Uint8Array.from("anotherWalletId", (c) => c.charCodeAt(0)), | ||
walletHmac: Uint8Array.from("anotherWalletHmac", (c) => | ||
c.charCodeAt(0), | ||
), | ||
change: true, | ||
addressIndex: 0x00000005, | ||
}; | ||
command = new GetWalletAddressCommand(args); | ||
const apdu = command.getApdu(); | ||
const expectedApdu = Uint8Array.from([ | ||
0xe1, // CLA | ||
0x03, // INS | ||
0x00, // P1 | ||
0x01, // P2 | ||
0x26, // Length of data | ||
0x00, // display: false | ||
...Uint8Array.from("anotherWalletId", (c) => c.charCodeAt(0)), | ||
...Uint8Array.from("anotherWalletHmac", (c) => c.charCodeAt(0)), | ||
0x01, // change: true | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x05, // addressIndex: 0x00000005 | ||
]); | ||
expect(apdu.getRawApdu()).toEqual(expectedApdu); | ||
}); | ||
}); | ||
|
||
describe("parseResponse", () => { | ||
it("should parse the response and extract the address", () => { | ||
const responseData = Uint8Array.from("myAddressData", (c) => | ||
c.charCodeAt(0), | ||
); | ||
const response = new ApduResponse({ | ||
statusCode: Uint8Array.from([0x90, 0x00]), | ||
data: responseData, | ||
}); | ||
|
||
const result = command.parseResponse(response); | ||
|
||
expect(result).toStrictEqual( | ||
CommandResultFactory({ | ||
data: { | ||
address: new TextDecoder().decode(responseData), | ||
}, | ||
}), | ||
); | ||
}); | ||
|
||
it("should return an error if response status code is an error code", () => { | ||
const errorStatusCode = Uint8Array.from([0x69, 0x85]); | ||
const response = new ApduResponse({ | ||
statusCode: errorStatusCode, | ||
data: new Uint8Array(), | ||
}); | ||
|
||
const result = command.parseResponse(response); | ||
|
||
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(expectedErrorInfo).toBeDefined(); | ||
if (expectedErrorInfo) { | ||
expect(error.message).toBe(expectedErrorInfo.message); | ||
} | ||
} else { | ||
fail("Expected error"); | ||
} | ||
}); | ||
|
||
it("should return an error if address cannot be extracted", () => { | ||
const response = new ApduResponse({ | ||
statusCode: Uint8Array.from([0x90, 0x00]), | ||
data: new Uint8Array(), | ||
}); | ||
|
||
const result = command.parseResponse(response); | ||
|
||
expect(isSuccessCommandResult(result)).toBe(false); | ||
if (!isSuccessCommandResult(result)) { | ||
expect(result.error.originalError).toEqual( | ||
expect.objectContaining({ | ||
message: "Failed to extract address from response", | ||
}), | ||
); | ||
} else { | ||
fail("Expected error"); | ||
} | ||
}); | ||
}); | ||
}); |
90 changes: 90 additions & 0 deletions
90
packages/signer/signer-btc/src/internal/app-binder/command/GetWalletAddressCommand.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { | ||
type Apdu, | ||
ApduBuilder, | ||
ApduParser, | ||
type ApduResponse, | ||
type Command, | ||
type CommandResult, | ||
CommandResultFactory, | ||
CommandUtils, | ||
GlobalCommandErrorHandler, | ||
InvalidStatusWordError, | ||
isCommandErrorCode, | ||
} from "@ledgerhq/device-management-kit"; | ||
|
||
import { PROTOCOL_VERSION } from "@internal/app-binder/command/utils/constants"; | ||
|
||
import { | ||
BitcoinAppCommandError, | ||
bitcoinAppErrors, | ||
} from "./utils/bitcoinAppErrors"; | ||
|
||
export type GetWalletAddressCommandResponse = { | ||
readonly address: string; | ||
}; | ||
|
||
export type GetWalletAddressCommandArgs = { | ||
readonly display: boolean; | ||
readonly walletId: Uint8Array; | ||
readonly walletHmac: Uint8Array; | ||
readonly change: boolean; | ||
readonly addressIndex: number; | ||
}; | ||
|
||
export class GetWalletAddressCommand | ||
implements | ||
Command<GetWalletAddressCommandResponse, GetWalletAddressCommandArgs> | ||
{ | ||
constructor(private readonly args: GetWalletAddressCommandArgs) {} | ||
|
||
getApdu(): Apdu { | ||
return new ApduBuilder({ | ||
cla: 0xe1, | ||
ins: 0x03, | ||
p1: 0x00, | ||
p2: PROTOCOL_VERSION, | ||
}) | ||
.addBufferToData(Uint8Array.from([this.args.display ? 1 : 0])) | ||
.addBufferToData(this.args.walletId) | ||
.addBufferToData(this.args.walletHmac) | ||
.addBufferToData(Uint8Array.from([this.args.change ? 1 : 0])) | ||
.add32BitUIntToData(this.args.addressIndex) | ||
.build(); | ||
} | ||
|
||
parseResponse( | ||
response: ApduResponse, | ||
): CommandResult<GetWalletAddressCommandResponse> { | ||
const parser = new ApduParser(response); | ||
const errorCode = parser.encodeToHexaString(response.statusCode); | ||
if (isCommandErrorCode(errorCode, bitcoinAppErrors)) { | ||
return CommandResultFactory<GetWalletAddressCommandResponse>({ | ||
error: new BitcoinAppCommandError({ | ||
...bitcoinAppErrors[errorCode], | ||
errorCode, | ||
}), | ||
}); | ||
} | ||
|
||
if (!CommandUtils.isSuccessResponse(response)) { | ||
return CommandResultFactory({ | ||
error: GlobalCommandErrorHandler.handle(response), | ||
}); | ||
} | ||
|
||
if (response.data.length === 0) { | ||
return CommandResultFactory({ | ||
error: new InvalidStatusWordError( | ||
"Failed to extract address from response", | ||
), | ||
}); | ||
} | ||
|
||
const address = parser.encodeToString(response.data); | ||
return CommandResultFactory({ | ||
data: { | ||
address, | ||
}, | ||
}); | ||
} | ||
} |
70 changes: 70 additions & 0 deletions
70
packages/signer/signer-btc/src/internal/app-binder/command/utils/bitcoinAppError.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { DeviceExchangeError } from "@ledgerhq/device-management-kit"; | ||
|
||
import { | ||
BitcoinAppCommandError, | ||
type BitcoinAppErrorCodes, | ||
bitcoinAppErrors, | ||
} from "./bitcoinAppErrors"; | ||
|
||
describe("BitcoinAppCommandError", () => { | ||
afterEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.resetModules(); | ||
}); | ||
|
||
it("should be an instance of DeviceExchangeError", () => { | ||
const error = new BitcoinAppCommandError({ | ||
message: "Test error message", | ||
errorCode: "6985", | ||
}); | ||
|
||
expect(error).toBeInstanceOf(DeviceExchangeError); | ||
}); | ||
|
||
it("should set the correct message when provided", () => { | ||
const customMessage = "Custom error message"; | ||
const error = new BitcoinAppCommandError({ | ||
message: customMessage, | ||
errorCode: "6985", | ||
}); | ||
|
||
expect(error.message).toBe(customMessage); | ||
}); | ||
|
||
it("should set the default message when none is provided", () => { | ||
const error = new BitcoinAppCommandError({ | ||
message: undefined, | ||
errorCode: "6985", | ||
}); | ||
|
||
expect(error.message).toBe("An error occurred during device exchange."); | ||
}); | ||
|
||
it("should set the correct customErrorCode", () => { | ||
const errorCode: BitcoinAppErrorCodes = "6A86"; | ||
const error = new BitcoinAppCommandError({ | ||
message: "Either P1 or P2 is incorrect", | ||
errorCode, | ||
}); | ||
|
||
expect(error.customErrorCode).toBe(errorCode); | ||
}); | ||
|
||
it("should correlate error codes with messages from bitcoinAppErrors", () => { | ||
const errorCode: BitcoinAppErrorCodes = "6E00"; | ||
const expectedMessage = bitcoinAppErrors[errorCode].message; | ||
|
||
const error = new BitcoinAppCommandError({ | ||
message: expectedMessage, | ||
errorCode, | ||
}); | ||
|
||
expect(error.customErrorCode).toBe(errorCode); | ||
expect(error.message).toBe(expectedMessage); | ||
|
||
expect(error).toBeInstanceOf(DeviceExchangeError); | ||
}); | ||
}); |
42 changes: 42 additions & 0 deletions
42
packages/signer/signer-btc/src/internal/app-binder/command/utils/bitcoinAppErrors.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { | ||
type CommandErrors, | ||
DeviceExchangeError, | ||
type DmkError, | ||
} from "@ledgerhq/device-management-kit"; | ||
|
||
export type BitcoinAppErrorCodes = | ||
| "6985" | ||
| "6A86" | ||
| "6A87" | ||
| "6D00" | ||
| "6E00" | ||
| "B000" | ||
| "B007" | ||
| "B008"; | ||
|
||
export const bitcoinAppErrors: CommandErrors<BitcoinAppErrorCodes> = { | ||
"6985": { message: "Rejected by user" }, | ||
"6A86": { message: "Either P1 or P2 is incorrect" }, | ||
"6A87": { message: "Lc or minimum APDU length is incorrect" }, | ||
"6D00": { message: "No command exists with the provided INS" }, | ||
"6E00": { message: "Bad CLA used for this application" }, | ||
B000: { message: "Wrong response length (buffer size problem)" }, | ||
B007: { message: "Aborted due to unexpected state reached" }, | ||
B008: { message: "Invalid signature or HMAC" }, | ||
}; | ||
|
||
export class BitcoinAppCommandError | ||
extends DeviceExchangeError<void> | ||
implements DmkError | ||
{ | ||
public readonly customErrorCode?: BitcoinAppErrorCodes; | ||
|
||
constructor(args: { message?: string; errorCode?: BitcoinAppErrorCodes }) { | ||
super({ | ||
tag: "BitcoinAppCommandError", | ||
message: args.message || "An error occurred during device exchange.", | ||
errorCode: undefined, | ||
}); | ||
this.customErrorCode = args.errorCode; | ||
} | ||
} |