Skip to content

Commit 0eb82c3

Browse files
✨ (signer-btc) [DSDK-475]: Implement GetExtendedPublicKey device action with DefaultSigner DI (#532)
2 parents 55d75f6 + 5da11c0 commit 0eb82c3

23 files changed

+499
-26
lines changed

.changeset/twenty-beans-teach.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ledgerhq/device-signer-kit-bitcoin": minor
3+
---
4+
5+
Implement SignerBtc, DI & GetExtendedPkeyDA

packages/signer/signer-btc/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"@ledgerhq/signer-utils": "workspace:*",
5454
"@ledgerhq/tsconfig-dsdk": "workspace:*",
5555
"bitcoinjs-lib": "^6.1.6",
56+
"rxjs": "^7.8.1",
5657
"ts-node": "^10.9.2"
5758
},
5859
"peerDependencies": {
Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
// import { type AddressOptions } from "@api/model/AddressOptions";
2+
// import { type Psbt } from "@api/model/Psbt";
3+
// import { type Signature } from "@api/model/Signature";
4+
// import { type Wallet } from "@api/model/Wallet";
15
import { type AddressOptions } from "@api/model/AddressOptions";
2-
import { type Psbt } from "@api/model/Psbt";
3-
import { type Signature } from "@api/model/Signature";
4-
import { type Wallet } from "@api/model/Wallet";
6+
import { type GetExtendedPublicKeyReturnType } from "@root/src";
57

68
export interface SignerBtc {
7-
getExtendedPubkey: (
9+
getExtendedPublicKey: (
810
derivationPath: string,
9-
checkOnDevice?: boolean,
10-
) => Promise<string>;
11-
getAddress: (wallet: Wallet, options?: AddressOptions) => Promise<string>;
12-
signMessage: (wallet: Wallet, message: string) => Promise<Signature>;
13-
signPsbt: (wallet: Wallet, psbt: Psbt) => Promise<Psbt>;
14-
signTransaction: (wallet: Wallet, psbt: Psbt) => Promise<Uint8Array>;
11+
options: AddressOptions,
12+
) => GetExtendedPublicKeyReturnType;
13+
// getAddress: (wallet: Wallet, options?: AddressOptions) => Promise<string>;
14+
// signMessage: (wallet: Wallet, message: string) => Promise<Signature>;
15+
// signPsbt: (wallet: Wallet, psbt: Psbt) => Promise<Psbt>;
16+
// signTransaction: (wallet: Wallet, psbt: Psbt) => Promise<Uint8Array>;
1517
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {
2+
type ExecuteDeviceActionReturnType,
3+
type SendCommandInAppDAError,
4+
type SendCommandInAppDAIntermediateValue,
5+
type SendCommandInAppDAOutput,
6+
type UserInteractionRequired,
7+
} from "@ledgerhq/device-management-kit";
8+
9+
import {
10+
type GetExtendedPublicKeyCommandArgs,
11+
type GetExtendedPublicKeyCommandResponse,
12+
} from "@internal/app-binder/command/GetExtendedPublicKeyCommand";
13+
14+
type GetExtendedPublicKeyDARequiredInteraction =
15+
| UserInteractionRequired.None
16+
| UserInteractionRequired.VerifyAddress;
17+
18+
export type GetExtendedPublicKeyDAOutput =
19+
SendCommandInAppDAOutput<GetExtendedPublicKeyCommandResponse>;
20+
21+
export type GetExtendedPublicKeyDAError = SendCommandInAppDAError;
22+
23+
export type GetExtendedDAIntermediateValue =
24+
SendCommandInAppDAIntermediateValue<GetExtendedPublicKeyDARequiredInteraction>;
25+
26+
export type GetExtendedPublicKeyDAInput = GetExtendedPublicKeyCommandArgs;
27+
28+
export type GetExtendedPublicKeyReturnType = ExecuteDeviceActionReturnType<
29+
GetExtendedPublicKeyDAOutput,
30+
GetExtendedPublicKeyDAError,
31+
GetExtendedDAIntermediateValue
32+
>;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { type SignerBtc } from "./SignerBtc";
2+
export * from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes";
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
export * from "@api/SignerBtc";
1+
// inversify
2+
import "reflect-metadata";
3+
4+
export * from "@api/index";
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { type DeviceManagementKit } from "@ledgerhq/device-management-kit";
2+
3+
import { DefaultSignerBtc } from "@internal/DefaultSignerBtc";
4+
import { GetExtendedPublicKeyUseCase } from "@internal/use-cases/get-extended-public-key/GetExtendedPublicKeyUseCase";
5+
6+
describe("DefaultSignerSolana", () => {
7+
it("should be defined", () => {
8+
const signer = new DefaultSignerBtc({
9+
dmk: {} as DeviceManagementKit,
10+
sessionId: "session-id",
11+
});
12+
expect(signer).toBeDefined();
13+
});
14+
15+
it("should call getExtendedPublicKeyUseCase", () => {
16+
jest.spyOn(GetExtendedPublicKeyUseCase.prototype, "execute");
17+
const sessionId = "session-id";
18+
const dmk = {
19+
executeDeviceAction: jest.fn(),
20+
} as unknown as DeviceManagementKit;
21+
const signer = new DefaultSignerBtc({ dmk, sessionId });
22+
signer.getExtendedPublicKey("44'/0'/0'/0/0", {
23+
checkOnDevice: true,
24+
});
25+
expect(GetExtendedPublicKeyUseCase.prototype.execute).toHaveBeenCalled();
26+
});
27+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {
2+
type DeviceManagementKit,
3+
type DeviceSessionId,
4+
} from "@ledgerhq/device-management-kit";
5+
import { type Container } from "inversify";
6+
7+
import { type AddressOptions } from "@api/model/AddressOptions";
8+
import { type SignerBtc } from "@api/SignerBtc";
9+
import { useCasesTypes } from "@internal/use-cases/di/useCasesTypes";
10+
import { type GetExtendedPublicKeyUseCase } from "@internal/use-cases/get-extended-public-key/GetExtendedPublicKeyUseCase";
11+
12+
import { makeContainer } from "./di";
13+
14+
type DefaultSignerBtcConstructorArgs = {
15+
dmk: DeviceManagementKit;
16+
sessionId: DeviceSessionId;
17+
};
18+
19+
export class DefaultSignerBtc implements SignerBtc {
20+
private readonly _container: Container;
21+
22+
constructor({ dmk, sessionId }: DefaultSignerBtcConstructorArgs) {
23+
this._container = makeContainer({ dmk, sessionId });
24+
}
25+
26+
getExtendedPublicKey(
27+
derivationPath: string,
28+
{ checkOnDevice = false }: AddressOptions,
29+
) {
30+
return this._container
31+
.get<GetExtendedPublicKeyUseCase>(
32+
useCasesTypes.GetExtendedPublicKeyUseCase,
33+
)
34+
.execute(derivationPath, { checkOnDevice });
35+
}
36+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {
2+
type DeviceActionState,
3+
DeviceActionStatus,
4+
type DeviceManagementKit,
5+
type DeviceSessionId,
6+
SendCommandInAppDeviceAction,
7+
UserInteractionRequired,
8+
} from "@ledgerhq/device-management-kit";
9+
import { from, type Subscription } from "rxjs";
10+
11+
import {
12+
type GetExtendedDAIntermediateValue,
13+
type GetExtendedPublicKeyDAError,
14+
type GetExtendedPublicKeyDAOutput,
15+
} from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes";
16+
import { BtcAppBinder } from "@internal/app-binder/BtcAppBinder";
17+
import { GetExtendedPublicKeyCommand } from "@internal/app-binder/command/GetExtendedPublicKeyCommand";
18+
19+
describe("BtcAppBinder", () => {
20+
const mockedDmk: DeviceManagementKit = {
21+
sendCommand: jest.fn(),
22+
executeDeviceAction: jest.fn(),
23+
} as unknown as DeviceManagementKit;
24+
25+
beforeEach(() => {
26+
jest.clearAllMocks();
27+
});
28+
29+
it("should be defined", () => {
30+
const binder = new BtcAppBinder(
31+
{} as DeviceManagementKit,
32+
{} as DeviceSessionId,
33+
);
34+
expect(binder).toBeDefined();
35+
});
36+
37+
describe("getExtendedPublicKey", () => {
38+
let subscription: Subscription;
39+
afterEach(() => {
40+
if (subscription) {
41+
subscription.unsubscribe();
42+
}
43+
});
44+
it("should return the pub key", (done) => {
45+
// GIVEN
46+
const extendedPublicKey = "D2PPQSYFe83nDzk96FqGumVU8JA7J8vj2Rhjc2oXzEi5";
47+
48+
jest.spyOn(mockedDmk, "executeDeviceAction").mockReturnValue({
49+
observable: from([
50+
{
51+
status: DeviceActionStatus.Completed,
52+
output: { extendedPublicKey },
53+
} as DeviceActionState<
54+
GetExtendedPublicKeyDAOutput,
55+
GetExtendedPublicKeyDAError,
56+
GetExtendedDAIntermediateValue
57+
>,
58+
]),
59+
cancel: jest.fn(),
60+
});
61+
62+
// WHEN
63+
const appBinder = new BtcAppBinder(mockedDmk, "sessionId");
64+
const { observable } = appBinder.getExtendedPublicKey({
65+
derivationPath: "44'/501'",
66+
checkOnDevice: false,
67+
});
68+
69+
// THEN
70+
const states: DeviceActionState<
71+
GetExtendedPublicKeyDAOutput,
72+
GetExtendedPublicKeyDAError,
73+
GetExtendedDAIntermediateValue
74+
>[] = [];
75+
subscription = observable.subscribe({
76+
next: (state) => {
77+
states.push(state);
78+
},
79+
error: (err) => {
80+
done(err);
81+
},
82+
complete: () => {
83+
try {
84+
expect(states).toEqual([
85+
{
86+
status: DeviceActionStatus.Completed,
87+
output: { extendedPublicKey },
88+
},
89+
]);
90+
done();
91+
} catch (err) {
92+
done(err);
93+
}
94+
},
95+
});
96+
});
97+
98+
describe("calls of executeDeviceAction with the correct params", () => {
99+
const baseParams = {
100+
derivationPath: "44'/60'/3'/2/1",
101+
returnChainCode: false,
102+
};
103+
104+
it("when checkOnDevice is true: UserInteractionRequired.VerifyAddress", () => {
105+
// GIVEN
106+
const checkOnDevice = true;
107+
const params = {
108+
...baseParams,
109+
checkOnDevice,
110+
};
111+
112+
// WHEN
113+
const appBinder = new BtcAppBinder(mockedDmk, "sessionId");
114+
appBinder.getExtendedPublicKey(params);
115+
116+
// THEN
117+
expect(mockedDmk.executeDeviceAction).toHaveBeenCalledWith({
118+
sessionId: "sessionId",
119+
deviceAction: new SendCommandInAppDeviceAction({
120+
input: {
121+
command: new GetExtendedPublicKeyCommand(params),
122+
appName: "Bitcoin",
123+
requiredUserInteraction: UserInteractionRequired.VerifyAddress,
124+
},
125+
}),
126+
});
127+
});
128+
129+
it("when checkOnDevice is false: UserInteractionRequired.None", () => {
130+
// GIVEN
131+
const checkOnDevice = false;
132+
const params = {
133+
...baseParams,
134+
checkOnDevice,
135+
};
136+
137+
// WHEN
138+
const appBinder = new BtcAppBinder(mockedDmk, "sessionId");
139+
appBinder.getExtendedPublicKey(params);
140+
141+
// THEN
142+
expect(mockedDmk.executeDeviceAction).toHaveBeenCalledWith({
143+
sessionId: "sessionId",
144+
deviceAction: new SendCommandInAppDeviceAction({
145+
input: {
146+
command: new GetExtendedPublicKeyCommand(params),
147+
appName: "Bitcoin",
148+
requiredUserInteraction: UserInteractionRequired.None,
149+
},
150+
}),
151+
});
152+
});
153+
});
154+
});
155+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {
2+
DeviceManagementKit,
3+
type DeviceSessionId,
4+
SendCommandInAppDeviceAction,
5+
UserInteractionRequired,
6+
} from "@ledgerhq/device-management-kit";
7+
import { inject, injectable } from "inversify";
8+
9+
import {
10+
GetExtendedPublicKeyDAInput,
11+
GetExtendedPublicKeyReturnType,
12+
} from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes";
13+
import { GetExtendedPublicKeyCommand } from "@internal/app-binder/command/GetExtendedPublicKeyCommand";
14+
import { externalTypes } from "@internal/externalTypes";
15+
16+
@injectable()
17+
export class BtcAppBinder {
18+
constructor(
19+
@inject(externalTypes.Dmk) private dmk: DeviceManagementKit,
20+
@inject(externalTypes.SessionId) private sessionId: DeviceSessionId,
21+
) {}
22+
getExtendedPublicKey(
23+
args: GetExtendedPublicKeyDAInput,
24+
): GetExtendedPublicKeyReturnType {
25+
return this.dmk.executeDeviceAction({
26+
sessionId: this.sessionId,
27+
deviceAction: new SendCommandInAppDeviceAction({
28+
input: {
29+
command: new GetExtendedPublicKeyCommand(args),
30+
appName: "Bitcoin",
31+
requiredUserInteraction: args.checkOnDevice
32+
? UserInteractionRequired.VerifyAddress
33+
: UserInteractionRequired.None,
34+
},
35+
}),
36+
});
37+
}
38+
}

packages/signer/signer-btc/src/internal/app-binder/command/GetExtendedPublicKeyCommand.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const GET_EXTENDED_PUBLIC_KEY_VALID_RESPONSE = new Uint8Array([
4141
describe("GetExtendedPublicKeyCommand", () => {
4242
let command: GetExtendedPublicKeyCommand;
4343
const defaultArgs: GetExtendedPublicKeyCommandArgs = {
44-
displayOnDevice: true,
44+
checkOnDevice: true,
4545
derivationPath: "84'/0'/0'",
4646
};
4747

@@ -65,7 +65,7 @@ describe("GetExtendedPublicKeyCommand", () => {
6565
// GIVEN
6666
command = new GetExtendedPublicKeyCommand({
6767
...defaultArgs,
68-
displayOnDevice: false,
68+
checkOnDevice: false,
6969
});
7070

7171
// WHEN

packages/signer/signer-btc/src/internal/app-binder/command/GetExtendedPublicKeyCommand.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { DerivationPathUtils } from "@ledgerhq/signer-utils";
1717
const STATUS_CODE_LENGTH = 2;
1818

1919
export type GetExtendedPublicKeyCommandArgs = {
20-
displayOnDevice: boolean;
20+
checkOnDevice: boolean;
2121
derivationPath: string;
2222
};
2323

@@ -35,7 +35,7 @@ export class GetExtendedPublicKeyCommand
3535
constructor(private readonly args: GetExtendedPublicKeyCommandArgs) {}
3636

3737
getApdu(): Apdu {
38-
const { displayOnDevice, derivationPath } = this.args;
38+
const { checkOnDevice, derivationPath } = this.args;
3939

4040
const getExtendedPublicKeyArgs: ApduBuilderArgs = {
4141
cla: 0xe1,
@@ -44,7 +44,7 @@ export class GetExtendedPublicKeyCommand
4444
p2: 0x00,
4545
};
4646
const builder = new ApduBuilder(getExtendedPublicKeyArgs).add8BitUIntToData(
47-
displayOnDevice ? 0x01 : 0x00,
47+
checkOnDevice ? 0x01 : 0x00,
4848
);
4949

5050
const path = DerivationPathUtils.splitPath(derivationPath);

0 commit comments

Comments
 (0)