Skip to content

Commit

Permalink
✨ (signer-solana): Add solana SignMessageUseCase
Browse files Browse the repository at this point in the history
  • Loading branch information
fAnselmi-Ledger committed Nov 7, 2024
1 parent 02ec232 commit 3e21b75
Show file tree
Hide file tree
Showing 21 changed files with 873 additions and 83 deletions.
5 changes: 5 additions & 0 deletions .changeset/tender-starfishes-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-signer-kit-solana": patch
---

Added Solana SignMessageUseCase
27 changes: 27 additions & 0 deletions apps/sample/src/components/SignerSolanaView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
type GetAppConfigurationDAIntermediateValue,
type GetAppConfigurationDAOutput,
SignerSolanaBuilder,
type SignMessageDAError,
type SignMessageDAIntermediateValue,
type SignMessageDAOutput,
} from "@ledgerhq/device-signer-kit-solana";

import { DeviceActionsList } from "@/components/DeviceActionsView/DeviceActionsList";
Expand Down Expand Up @@ -51,6 +54,30 @@ export const SignerSolanaView: React.FC<{ sessionId: string }> = ({
GetAddressDAError,
GetAddressDAIntermediateValue
>,
{
title: "Sign off chain message",
description:
"Perform all the actions necessary to sign a solana off-chain message from the device",
executeDeviceAction: ({ derivationPath, message }) => {
if (!signer) {
throw new Error("Signer not initialized");
}
return signer.signMessage(derivationPath, message);
},
initialValues: {
derivationPath: "44'/501'/0'/0'",
message: "Hello World",
},
deviceModelId,
} satisfies DeviceActionProps<
SignMessageDAOutput,
{
derivationPath: string;
message: string;
},
SignMessageDAError,
SignMessageDAIntermediateValue
>,
{
title: "Get app configuration",
description:
Expand Down
5 changes: 5 additions & 0 deletions packages/signer/signer-solana/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@ export type {
GetAppConfigurationDAIntermediateValue,
GetAppConfigurationDAOutput,
} from "@api/app-binder/GetAppConfigurationDeviceActionTypes";
export type {
SignMessageDAError,
SignMessageDAIntermediateValue,
SignMessageDAOutput,
} from "@api/app-binder/SignMessageDeviceActionTypes";
export type { SignerSolana } from "@api/SignerSolana";
export { SignerSolanaBuilder } from "@api/SignerSolanaBuilder";
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ describe("DefaultSignerSolana", () => {
expect(dmk.executeDeviceAction).toHaveBeenCalled();
});

it("should call signMessage", () => {
const dmk = {
executeDeviceAction: jest.fn(),
} as unknown as DeviceManagementKit;
const sessionId = {} as DeviceSessionId;
const signer = new DefaultSignerSolana({ dmk, sessionId });
signer.signMessage("44'/501'", "Hello world");
expect(dmk.executeDeviceAction).toHaveBeenCalled();
});

it("should call getAppConfiguration", () => {
const dmk = {
executeDeviceAction: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { type SignerSolana } from "@api/SignerSolana";
import { type GetAddressUseCase } from "./use-cases/address/GetAddressUseCase";
import { type GetAppConfigurationUseCase } from "./use-cases/app-configuration/GetAppConfigurationUseCase";
import { useCasesTypes } from "./use-cases/di/useCasesTypes";
import { type SignMessageUseCase } from "./use-cases/message/SignMessageUseCase";
import { makeContainer } from "./di";

export type DefaultSignerSolanaConstructorArgs = {
Expand All @@ -42,7 +43,9 @@ export class DefaultSignerSolana implements SignerSolana {
_derivationPath: string,
_message: string,
): SignMessageDAReturnType {
return {} as SignMessageDAReturnType;
return this._container
.get<SignMessageUseCase>(useCasesTypes.SignMessageUseCase)
.execute(_derivationPath, _message);
}

getAddress(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
type DeviceActionIntermediateValue,
type DeviceActionState,
DeviceActionStatus,
type DeviceManagementKit,
type DeviceSessionId,
type DmkError,
SendCommandInAppDeviceAction,
UserInteractionRequired,
} from "@ledgerhq/device-management-kit";
Expand All @@ -20,6 +22,7 @@ import {
} from "@api/index";

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

describe("SolanaAppBinder", () => {
Expand Down Expand Up @@ -209,4 +212,80 @@ describe("SolanaAppBinder", () => {
});
});
});

describe("signMessage", () => {
it("should return the signed message", (done) => {
// GIVEN
const signedMessage = new Uint8Array([0x1c, 0x8a, 0x54, 0x05, 0x10]);
const signMessageArgs = {
derivationPath: "44'/501'",
message: "Hello world",
};

jest.spyOn(mockedDmk, "executeDeviceAction").mockReturnValue({
observable: from([
{
status: DeviceActionStatus.Completed,
output: signedMessage,
} as DeviceActionState<
Uint8Array,
DmkError,
DeviceActionIntermediateValue
>,
]),
cancel: jest.fn(),
});

// WHEN
const appBinder = new SolanaAppBinder(mockedDmk, "sessionId");
const { observable } = appBinder.signMessage(signMessageArgs);

// THEN
const states: DeviceActionState<Uint8Array, unknown, unknown>[] = [];
observable.subscribe({
next: (state) => {
states.push(state);
},
error: (err) => {
done(err);
},
complete: () => {
try {
expect(states).toEqual([
{
status: DeviceActionStatus.Completed,
output: signedMessage,
},
]);
done();
} catch (err) {
done(err);
}
},
});
});

it("should call executeDeviceAction with correct parameters", () => {
// GIVEN
const signMessageArgs = {
derivationPath: "44'/501'",
message: "Hello world",
};

// WHEN
const appBinder = new SolanaAppBinder(mockedDmk, "sessionId");
appBinder.signMessage(signMessageArgs);

// THEN
expect(mockedDmk.executeDeviceAction).toHaveBeenCalledWith({
sessionId: "sessionId",
deviceAction: new SignMessageDeviceAction({
input: {
derivationPath: signMessageArgs.derivationPath,
message: signMessageArgs.message,
},
}),
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { externalTypes } from "@internal/externalTypes";

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

@injectable()
export class SolanaAppBinder {
Expand Down Expand Up @@ -48,11 +49,19 @@ export class SolanaAppBinder {
return {} as SignTransactionDAReturnType;
}

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

getAppConfiguration(): GetAppConfigurationDAReturnType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe("SignOffChainMessageCommand", () => {
beforeEach(() => {
command = new SignOffChainMessageCommand({
message: MESSAGE,
derivationPath: "m/44'/501''",
});
jest.clearAllMocks();
jest.requireActual("@ledgerhq/device-management-kit");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const SIGNATURE_LENGTH = 64;
export type SignOffChainMessageCommandResponse = Maybe<Signature>;
export type SignOffChainMessageCommandArgs = {
readonly message: Uint8Array;
readonly derivationPath: string;
};

export class SignOffChainMessageCommand
Expand Down
Loading

0 comments on commit 3e21b75

Please sign in to comment.