Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ (signer-btc) [DSDK-482]: Implement sign message #564

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slimy-toes-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-signer-kit-bitcoin": minor
---

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 bitcoin 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";
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);
}
},
});
});
});
});
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