Skip to content

Commit

Permalink
✨ (signer-btc): Implement signMessage device action
Browse files Browse the repository at this point in the history
  • Loading branch information
fAnselmi-Ledger committed Dec 16, 2024
1 parent 882ed6e commit cc784d5
Show file tree
Hide file tree
Showing 35 changed files with 1,500 additions and 455 deletions.
2 changes: 1 addition & 1 deletion .changeset/slimy-toes-march.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"@ledgerhq/device-signer-kit-bitcoin": minor
---

Implement ClientCommands
Implement SignMessage
1 change: 1 addition & 0 deletions apps/sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@ledgerhq/device-mockserver-client": "workspace:*",
"@ledgerhq/device-signer-kit-ethereum": "workspace:*",
"@ledgerhq/device-signer-kit-solana": "workspace:*",
"@ledgerhq/device-signer-kit-bitcoin": "workspace:*",
"@ledgerhq/device-transport-kit-mockserver": "workspace:*",
"@ledgerhq/device-transport-kit-web-ble": "workspace:*",
"@ledgerhq/device-transport-kit-web-hid": "workspace:*",
Expand Down
11 changes: 11 additions & 0 deletions apps/sample/src/app/signer/bitcoin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";
import React from "react";

import { SessionIdWrapper } from "@/components/SessionIdWrapper";
import { SignerBitcoinView } from "@/components/SignerBtcView";

const Signer: React.FC = () => {
return <SessionIdWrapper ChildComponent={SignerBitcoinView} />;
};

export default Signer;
59 changes: 59 additions & 0 deletions apps/sample/src/components/SignerBtcView/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { useMemo } from "react";
import {
SignerBtcBuilder,
type SignMessageDAError,
type SignMessageDAIntermediateValue,
type SignMessageDAOutput,
} from "@ledgerhq/device-signer-kit-bitcoin";

import { DeviceActionsList } from "@/components/DeviceActionsView/DeviceActionsList";
import { type DeviceActionProps } from "@/components/DeviceActionsView/DeviceActionTester";
import { useDmk } from "@/providers/DeviceManagementKitProvider";

const DEFAULT_DERIVATION_PATH = "44'/501'/0'/0'";

export const SignerBitcoinView: React.FC<{ sessionId: string }> = ({
sessionId,
}) => {
const dmk = useDmk();
const signer = new SignerBtcBuilder({ dmk, sessionId }).build();

const deviceModelId = dmk.getConnectedDevice({
sessionId,
}).modelId;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const deviceActions: DeviceActionProps<any, any, any, any>[] = useMemo(
() => [
{
title: "Sign message",
description:
"Perform all the actions necessary to sign a message with the device",
executeDeviceAction: ({ derivationPath, message }) => {
if (!signer) {
throw new Error("Signer not initialized");
}
return signer.signMessage(derivationPath, message);
},
initialValues: {
derivationPath: DEFAULT_DERIVATION_PATH,
message: "Hello World",
},
deviceModelId,
} satisfies DeviceActionProps<
SignMessageDAOutput,
{
derivationPath: string;
message: string;
},
SignMessageDAError,
SignMessageDAIntermediateValue
>,
],
[deviceModelId, signer],
);

return (
<DeviceActionsList title="Bitcoin Signer" deviceActions={deviceActions} />
);
};
10 changes: 8 additions & 2 deletions packages/signer/signer-btc/src/api/SignerBtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@
// import { type Signature } from "@api/model/Signature";
// import { type Wallet } from "@api/model/Wallet";
import { type AddressOptions } from "@api/model/AddressOptions";
import { type GetExtendedPublicKeyReturnType } from "@root/src";
import {
type GetExtendedPublicKeyReturnType,
type SignMessageDAReturnType,
} from "@root/src";

export interface SignerBtc {
getExtendedPublicKey: (
derivationPath: string,
options: AddressOptions,
) => GetExtendedPublicKeyReturnType;
signMessage: (
derivationPath: string,
message: string,
) => SignMessageDAReturnType;
// getAddress: (wallet: Wallet, options?: AddressOptions) => Promise<string>;
// signMessage: (wallet: Wallet, message: string) => Promise<Signature>;
// signPsbt: (wallet: Wallet, psbt: Psbt) => Promise<Psbt>;
// signTransaction: (wallet: Wallet, psbt: Psbt) => Promise<Uint8Array>;
}
42 changes: 42 additions & 0 deletions packages/signer/signer-btc/src/api/SignerBtcBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
type DeviceManagementKit,
type DeviceSessionId,
} from "@ledgerhq/device-management-kit";

import { DefaultSignerBtc } from "@internal/DefaultSignerBtc";

type SignerBtcBuilderConstructorArgs = {
dmk: DeviceManagementKit;
sessionId: DeviceSessionId;
};

/**
* Builder for the `SignerBtc` class.
*
* @example
* ```
* const signer = new SignerBtcBuilder({ dmk, sessionId })
* .build();
* ```
*/
export class SignerBtcBuilder {
private _dmk: DeviceManagementKit;
private _sessionId: DeviceSessionId;

constructor({ dmk, sessionId }: SignerBtcBuilderConstructorArgs) {
this._dmk = dmk;
this._sessionId = sessionId;
}

/**
* Build the btc signer
*
* @returns the bitcoin signer
*/
public build() {
return new DefaultSignerBtc({
dmk: this._dmk,
sessionId: this._sessionId,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
type CommandErrorResult,
type DeviceActionState,
type ExecuteDeviceActionReturnType,
type OpenAppDAError,
type OpenAppDARequiredInteraction,
type UserInteractionRequired,
} from "@ledgerhq/device-management-kit";

import { type Signature } from "@api/model/Signature";

export type SignMessageDAOutput = Signature;

export type SignMessageDAInput = {
readonly derivationPath: string;
readonly message: string;
};

export type SignMessageDAError = OpenAppDAError | CommandErrorResult["error"];

type SignMessageDARequiredInteraction =
| OpenAppDARequiredInteraction
| UserInteractionRequired.SignPersonalMessage;

export type SignMessageDAIntermediateValue = {
requiredUserInteraction: SignMessageDARequiredInteraction;
};

export type SignMessageDAState = DeviceActionState<
SignMessageDAOutput,
SignMessageDAError,
SignMessageDAIntermediateValue
>;

export type SignMessageDAInternalState = {
readonly error: SignMessageDAError | null;
readonly signature: Signature | null;
};

export type SignMessageDAReturnType = ExecuteDeviceActionReturnType<
SignMessageDAOutput,
SignMessageDAError,
SignMessageDAIntermediateValue
>;
10 changes: 10 additions & 0 deletions packages/signer/signer-btc/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
export { type SignerBtc } from "./SignerBtc";
export * from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes";
export type {
SignMessageDAError,
SignMessageDAInput,
SignMessageDAIntermediateValue,
SignMessageDAOutput,
SignMessageDAState,
} from "@api/app-binder/SignMessageDeviceActionType";
export * from "@api/app-binder/SignMessageDeviceActionType";
export * from "@api/SignerBtc";
export * from "@api/SignerBtcBuilder";
17 changes: 16 additions & 1 deletion packages/signer/signer-btc/src/internal/DefaultSignerBtc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { type DeviceManagementKit } from "@ledgerhq/device-management-kit";
import { DefaultSignerBtc } from "@internal/DefaultSignerBtc";
import { GetExtendedPublicKeyUseCase } from "@internal/use-cases/get-extended-public-key/GetExtendedPublicKeyUseCase";

describe("DefaultSignerSolana", () => {
import { SignMessageUseCase } from "./use-cases/sign-message/SignMessageUseCase";

describe("DefaultSignerBtc", () => {
it("should be defined", () => {
const signer = new DefaultSignerBtc({
dmk: {} as DeviceManagementKit,
Expand All @@ -24,4 +26,17 @@ describe("DefaultSignerSolana", () => {
});
expect(GetExtendedPublicKeyUseCase.prototype.execute).toHaveBeenCalled();
});

it("should call signMessageUseCase", () => {
jest.spyOn(SignMessageUseCase.prototype, "execute");
const sessionId = "session-id";
const dmk = {
executeDeviceAction: jest.fn(),
} as unknown as DeviceManagementKit;
const derivationPath = "44'/0'/0'/0/0";
const message = "message";
const signer = new DefaultSignerBtc({ dmk, sessionId });
signer.signMessage(derivationPath, message);
expect(SignMessageUseCase.prototype.execute).toHaveBeenCalled();
});
});
11 changes: 11 additions & 0 deletions packages/signer/signer-btc/src/internal/DefaultSignerBtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import {
} from "@ledgerhq/device-management-kit";
import { type Container } from "inversify";

import { type SignMessageDAReturnType } from "@api/app-binder/SignMessageDeviceActionType";
import { type AddressOptions } from "@api/model/AddressOptions";
import { type SignerBtc } from "@api/SignerBtc";
import { useCasesTypes } from "@internal/use-cases/di/useCasesTypes";
import { type GetExtendedPublicKeyUseCase } from "@internal/use-cases/get-extended-public-key/GetExtendedPublicKeyUseCase";

import { type SignMessageUseCase } from "./use-cases/sign-message/SignMessageUseCase";
import { makeContainer } from "./di";

type DefaultSignerBtcConstructorArgs = {
Expand All @@ -33,4 +35,13 @@ export class DefaultSignerBtc implements SignerBtc {
)
.execute(derivationPath, { checkOnDevice });
}

signMessage(
_derivationPath: string,
_message: string,
): SignMessageDAReturnType {
return this._container
.get<SignMessageUseCase>(useCasesTypes.SignMessageUseCase)
.execute(_derivationPath, _message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import {
type GetExtendedPublicKeyDAError,
type GetExtendedPublicKeyDAOutput,
} from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes";
import {
type SignMessageDAError,
type SignMessageDAIntermediateValue,
type SignMessageDAOutput,
} from "@api/index";
import { type Signature } from "@api/model/Signature";
import { BtcAppBinder } from "@internal/app-binder/BtcAppBinder";
import { GetExtendedPublicKeyCommand } from "@internal/app-binder/command/GetExtendedPublicKeyCommand";

Expand Down Expand Up @@ -152,4 +158,65 @@ describe("BtcAppBinder", () => {
});
});
});

describe("signMessage", () => {
it("should return the signature", (done) => {
// GIVEN
const signature: Signature = {
r: `0xDEF1`,
s: `0xAFAF`,
v: 0,
};
const message = "Hello, World!";

jest.spyOn(mockedDmk, "executeDeviceAction").mockReturnValue({
observable: from([
{
status: DeviceActionStatus.Completed,
output: signature,
} as DeviceActionState<
SignMessageDAOutput,
SignMessageDAError,
SignMessageDAIntermediateValue
>,
]),
cancel: jest.fn(),
});

// WHEN
const appBinder = new BtcAppBinder(mockedDmk, "sessionId");
const { observable } = appBinder.signMessage({
derivationPath: "44'/60'/3'/2/1",
message,
});

// THEN
const states: DeviceActionState<
SignMessageDAOutput,
SignMessageDAError,
SignMessageDAIntermediateValue
>[] = [];
observable.subscribe({
next: (state) => {
states.push(state);
},
error: (err) => {
done(err);
},
complete: () => {
try {
expect(states).toEqual([
{
status: DeviceActionStatus.Completed,
output: signature,
},
]);
done();
} catch (err) {
done(err);
}
},
});
});
});
});
19 changes: 19 additions & 0 deletions packages/signer/signer-btc/src/internal/app-binder/BtcAppBinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ import {
GetExtendedPublicKeyDAInput,
GetExtendedPublicKeyReturnType,
} from "@api/app-binder/GetExtendedPublicKeyDeviceActionTypes";
import { SignMessageDAReturnType } from "@api/index";
import { GetExtendedPublicKeyCommand } from "@internal/app-binder/command/GetExtendedPublicKeyCommand";
import { externalTypes } from "@internal/externalTypes";

import { SignMessageDeviceAction } from "./device-action/SignMessage/SignMessageDeviceAction";

@injectable()
export class BtcAppBinder {
constructor(
@inject(externalTypes.Dmk) private dmk: DeviceManagementKit,
@inject(externalTypes.SessionId) private sessionId: DeviceSessionId,
) {}

getExtendedPublicKey(
args: GetExtendedPublicKeyDAInput,
): GetExtendedPublicKeyReturnType {
Expand All @@ -35,4 +39,19 @@ export class BtcAppBinder {
}),
});
}

signMessage(args: {
derivationPath: string;
message: string;
}): SignMessageDAReturnType {
return this.dmk.executeDeviceAction({
sessionId: this.sessionId,
deviceAction: new SignMessageDeviceAction({
input: {
derivationPath: args.derivationPath,
message: args.message,
},
}),
});
}
}
Loading

0 comments on commit cc784d5

Please sign in to comment.