Skip to content

Commit

Permalink
✨ (signer-solana) [DSDK-550]: Add GetPubKey command (#441)
Browse files Browse the repository at this point in the history
  • Loading branch information
aussedatlo authored Oct 25, 2024
2 parents 639a46a + 7dc299f commit 487655e
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-berries-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-signer-kit-solana": minor
---

Add GetPubKey command
4 changes: 3 additions & 1 deletion packages/signer/signer-solana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@
"prettier": "prettier . --check",
"prettier:fix": "prettier . --write",
"typecheck": "tsc --noEmit",
"test": "jest --passWithNoTests",
"test": "jest",
"test:watch": "pnpm test -- --watch",
"test:coverage": "pnpm test -- --coverage"
},
"dependencies": {
"bs58": "^6.0.0",
"inversify": "^6.0.2",
"inversify-logger-middleware": "^3.1.0",
"purify-ts": "^2.1.0",
Expand All @@ -47,6 +48,7 @@
"@ledgerhq/eslint-config-dsdk": "workspace:*",
"@ledgerhq/jest-config-dsdk": "workspace:*",
"@ledgerhq/prettier-config-dsdk": "workspace:*",
"@ledgerhq/signer-utils": "workspace:*",
"@ledgerhq/tsconfig-dsdk": "workspace:*",
"ts-node": "^10.9.2"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/signer/signer-solana/src/api/model/PublicKey.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type PublicKey = Uint8Array[32];
export type PublicKey = string;
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {
ApduResponse,
CommandResultFactory,
isSuccessCommandResult,
} from "@ledgerhq/device-management-kit";

import { GetPubKeyCommand } from "./GetPubKeyCommand";

const GET_PUBKYEY_APDU_DEFAULT_PATH_WITH_CONFIRM = new Uint8Array([
0xe0, 0x05, 0x01, 0x00, 0x09, 0x02, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, 0x01,
0xf5,
]);

const GET_PUBKYEY_APDU_DEFAULT_PATH_WITHOUT_CONFIRM = new Uint8Array([
0xe0, 0x05, 0x00, 0x00, 0x09, 0x02, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, 0x01,
0xf5,
]);

const GET_PUBKEY_APDU_DIFFERENT_PATH = new Uint8Array([
0xe0, 0x05, 0x01, 0x00, 0x09, 0x02, 0x80, 0x00, 0x00, 0x2c, 0x80, 0x00, 0x01,
0xf6,
]);

// D2PPQSYFe83nDzk96FqGumVU8JA7J8vj2Rhjc2oXzEi5
const GET_PUBKEY_APDU = new Uint8Array([
0xb2, 0xa7, 0x22, 0xdc, 0x18, 0xdd, 0x5c, 0x49, 0xc3, 0xf4, 0x8e, 0x9b, 0x07,
0x26, 0xf1, 0x1b, 0xe6, 0x67, 0x86, 0xe9, 0x1c, 0xac, 0x57, 0x34, 0x98, 0xd6,
0xee, 0x88, 0x39, 0x2c, 0xc9, 0x6a, 0x90, 0x00,
]);

const GET_PUBKEY_APDU_RESPONSE = new ApduResponse({
statusCode: Uint8Array.from([0x90, 0x00]),
data: GET_PUBKEY_APDU,
});

describe("GetPubKeyCommand", () => {
let command: GetPubKeyCommand;
const defaultArgs = {
derivationPath: "44'/501'",
checkOnDevice: true,
};

beforeEach(() => {
command = new GetPubKeyCommand(defaultArgs);
jest.clearAllMocks();
jest.requireActual("@ledgerhq/device-management-kit");
});

describe("getApdu", () => {
it("should return APDU", () => {
const apdu = command.getApdu();
expect(apdu.getRawApdu()).toEqual(
GET_PUBKYEY_APDU_DEFAULT_PATH_WITH_CONFIRM,
);
});

it("should return APDU without confirm", () => {
command = new GetPubKeyCommand({
...defaultArgs,
checkOnDevice: false,
});
const apdu = command.getApdu();
expect(apdu.getRawApdu()).toEqual(
GET_PUBKYEY_APDU_DEFAULT_PATH_WITHOUT_CONFIRM,
);
});

it("should return APDU with different path", () => {
command = new GetPubKeyCommand({
...defaultArgs,
derivationPath: "44'/502'",
});
const apdu = command.getApdu();
expect(apdu.getRawApdu()).toEqual(GET_PUBKEY_APDU_DIFFERENT_PATH);
});
});

describe("parseResponse", () => {
it("should parse the response", () => {
const parsed = command.parseResponse(GET_PUBKEY_APDU_RESPONSE);
expect(parsed).toStrictEqual(
CommandResultFactory({
data: "D2PPQSYFe83nDzk96FqGumVU8JA7J8vj2Rhjc2oXzEi5",
}),
);
});

describe("error handling", () => {
it("should return error if response is not success", () => {
const response = new ApduResponse({
statusCode: Uint8Array.from([0x6a, 0x82]),
data: new Uint8Array(0),
});
const result = command.parseResponse(response);
expect(isSuccessCommandResult(result)).toBe(false);
if (!isSuccessCommandResult(result)) {
expect(result.error).toEqual(
expect.objectContaining({
message: "Unexpected device exchange error happened.",
}),
);
} else {
fail("Expected error");
}
});

it("should return error if public key is missing", () => {
const response = new ApduResponse({
statusCode: Uint8Array.from([0x90, 0x00]),
data: new Uint8Array(0),
});
const result = command.parseResponse(response);
expect(isSuccessCommandResult(result)).toBe(false);
if (!isSuccessCommandResult(result)) {
expect(result.error.originalError).toEqual(
expect.objectContaining({
message: "Public key is missing",
}),
);
} else {
fail("Expected error");
}
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
Apdu,
ApduBuilder,
type ApduBuilderArgs,
ApduParser,
ApduResponse,
type Command,
CommandResult,
CommandResultFactory,
CommandUtils,
GlobalCommandErrorHandler,
InvalidStatusWordError,
} from "@ledgerhq/device-management-kit";
import { DerivationPathUtils } from "@ledgerhq/signer-utils";
import bs58 from "bs58";

import { PublicKey } from "@api/model/PublicKey";

const PUBKEY_LENGTH = 32;

type GetPubKeyCommandResponse = PublicKey;
type GetPubKeyCommandArgs = {
derivationPath: string;
checkOnDevice: boolean;
};

export class GetPubKeyCommand
implements Command<GetPubKeyCommandResponse, GetPubKeyCommandArgs>
{
args: GetPubKeyCommandArgs;

constructor(args: GetPubKeyCommandArgs) {
this.args = args;
}

getApdu(): Apdu {
const getPubKeyArgs: ApduBuilderArgs = {
cla: 0xe0,
ins: 0x05,
p1: this.args.checkOnDevice ? 0x01 : 0x00,
p2: 0x00,
};
const builder = new ApduBuilder(getPubKeyArgs);
const derivationPath = this.args.derivationPath;

const path = DerivationPathUtils.splitPath(derivationPath);
builder.add8BitUIntToData(path.length);
path.forEach((element) => {
builder.add32BitUIntToData(element);
});

return builder.build();
}

parseResponse(
response: ApduResponse,
): CommandResult<GetPubKeyCommandResponse> {
const parser = new ApduParser(response);

if (!CommandUtils.isSuccessResponse(response)) {
return CommandResultFactory({
error: GlobalCommandErrorHandler.handle(response),
});
}

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 publicKey = bs58.encode(buffer);

return CommandResultFactory({
data: publicKey,
});
}
}
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 487655e

Please sign in to comment.