Skip to content

Commit

Permalink
✨ (signer-solana) [DSDK-566]: Add solana SignMessageUseCase (#467)
Browse files Browse the repository at this point in the history
  • Loading branch information
fAnselmi-Ledger authored Nov 15, 2024
2 parents 6cb5afd + 925cb91 commit c9cd89b
Show file tree
Hide file tree
Showing 22 changed files with 1,070 additions and 24 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
29 changes: 28 additions & 1 deletion apps/sample/src/components/SignerSolanaView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
type GetAppConfigurationDAIntermediateValue,
type GetAppConfigurationDAOutput,
SignerSolanaBuilder,
type SignMessageDAError,
type SignMessageDAIntermediateValue,
type SignMessageDAOutput,
type SignTransactionDAError,
type SignTransactionDAIntermediateValue,
type SignTransactionDAOutput,
Expand All @@ -20,7 +23,7 @@ import { DeviceActionsList } from "@/components/DeviceActionsView/DeviceActionsL
import { type DeviceActionProps } from "@/components/DeviceActionsView/DeviceActionTester";
import { useDmk } from "@/providers/DeviceManagementKitProvider";

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

export const SignerSolanaView: React.FC<{ sessionId: string }> = ({
sessionId,
Expand Down Expand Up @@ -87,6 +90,30 @@ export const SignerSolanaView: React.FC<{ sessionId: string }> = ({
SignTransactionDAError,
SignTransactionDAIntermediateValue
>,
{
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: DEFAULT_DERIVATION_PATH,
message: "Hello World",
},
deviceModelId,
} satisfies DeviceActionProps<
SignMessageDAOutput,
{
derivationPath: string;
message: string;
},
SignMessageDAError,
SignMessageDAIntermediateValue
>,
{
title: "Get app configuration",
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SignMessageUseCase } from "./SignMessageUseCase";
describe("SignMessageUseCase", () => {
it("should call signPersonalMessage on appBinder with the correct arguments", () => {
// Given
const derivationPath = "44'/60'/0'/0/0";
const derivationPath = "44'/501'/0'/0'";
const message = "Hello world";
const appBinder = {
signPersonalMessage: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ import { EthAppBinder } from "@internal/app-binder/EthAppBinder";

@injectable()
export class SignMessageUseCase {
private _appBinding: EthAppBinder;
private _appBinder: EthAppBinder;

constructor(
@inject(appBinderTypes.AppBinding)
appBinding: EthAppBinder,
) {
this._appBinding = appBinding;
this._appBinder = appBinding;
}

execute(
derivationPath: string,
message: string | Uint8Array,
): SignPersonalMessageDAReturnType {
// 1- Sign the transaction using the app binding
return this._appBinding.signPersonalMessage({
return this._appBinder.signPersonalMessage({
derivationPath,
message,
});
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,6 +9,11 @@ export type {
GetAppConfigurationDAIntermediateValue,
GetAppConfigurationDAOutput,
} from "@api/app-binder/GetAppConfigurationDeviceActionTypes";
export type {
SignMessageDAError,
SignMessageDAIntermediateValue,
SignMessageDAOutput,
} from "@api/app-binder/SignMessageDeviceActionTypes";
export type {
SignTransactionDAError,
SignTransactionDAIntermediateValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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'/0'/0'", "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 { type SignTransactionUseCase } from "./use-cases/transaction/SignTransactionUseCase";
import { makeContainer } from "./di";

Expand All @@ -42,10 +43,12 @@ export class DefaultSignerSolana implements SignerSolana {
}

signMessage(
_derivationPath: string,
_message: string,
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 @@ -22,7 +24,9 @@ import {
type SignTransactionDAOutput,
} from "@api/index";

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

Expand Down Expand Up @@ -235,6 +239,82 @@ 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'/0'/0'",
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'/0'/0'",
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,
},
}),
});
});
});

describe("getAppConfiguration", () => {
it("should return the app configuration", (done) => {
// GIVEN
Expand Down Expand Up @@ -290,5 +370,25 @@ describe("SolanaAppBinder", () => {
},
});
});

it("should call executeDeviceAction with the correct params", () => {
// GIVEN
const appBinder = new SolanaAppBinder(mockedDmk, "sessionId");

// WHEN
appBinder.getAppConfiguration();

// THEN
expect(mockedDmk.executeDeviceAction).toHaveBeenCalledWith({
sessionId: "sessionId",
deviceAction: new SendCommandInAppDeviceAction({
input: {
command: new GetAppConfigurationCommand(), // Correct command
appName: "Solana",
requiredUserInteraction: UserInteractionRequired.None,
},
}),
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { externalTypes } from "@internal/externalTypes";

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

@injectable()
Expand Down Expand Up @@ -60,11 +61,19 @@ export class SolanaAppBinder {
});
}

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 @@ -3,7 +3,6 @@ import {
ApduResponse,
isSuccessCommandResult,
} from "@ledgerhq/device-management-kit";
import { Just } from "purify-ts";

import { SignOffChainMessageCommand } from "./SignOffChainMessageCommand";

Expand All @@ -17,6 +16,7 @@ describe("SignOffChainMessageCommand", () => {
beforeEach(() => {
command = new SignOffChainMessageCommand({
message: MESSAGE,
derivationPath: "44'/501'/0'/0'",
});
jest.clearAllMocks();
jest.requireActual("@ledgerhq/device-management-kit");
Expand Down Expand Up @@ -51,7 +51,7 @@ describe("SignOffChainMessageCommand", () => {

expect(isSuccessCommandResult(parsed)).toBe(true);
if (isSuccessCommandResult(parsed)) {
expect(parsed.data).toEqual(Just(signature));
expect(parsed.data).toEqual(signature);
} else {
fail("Expected success result");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import {
GlobalCommandErrorHandler,
InvalidStatusWordError,
} from "@ledgerhq/device-management-kit";
import { Just, type Maybe, Nothing } from "purify-ts";

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

const SIGNATURE_LENGTH = 64;

export type SignOffChainMessageCommandResponse = Maybe<Signature>;
export type SignOffChainMessageCommandResponse = Signature;
export type SignOffChainMessageCommandArgs = {
readonly message: Uint8Array;
readonly derivationPath: string;
};

export class SignOffChainMessageCommand
Expand Down Expand Up @@ -59,21 +59,14 @@ export class SignOffChainMessageCommand
});
}

if (parser.getUnparsedRemainingLength() === 0) {
return CommandResultFactory({
data: Nothing,
});
}

const signature = parser.extractFieldByLength(SIGNATURE_LENGTH);
if (!signature) {
return CommandResultFactory({
error: new InvalidStatusWordError("Unable to extract signature"),
error: new InvalidStatusWordError("Signature extraction failed"),
});
}

return CommandResultFactory({
data: Just(signature),
data: signature,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ describe("SignTransactionCommand", () => {
it("should return the signature when the response is not empty", () => {
// GIVEN
const command = new SignTransactionCommand(defaultArgs);
// Uint8Array of 64 bytes
const data = new Uint8Array(Array.from({ length: 64 }, (_, i) => i + 1));

// WHEN
Expand Down
Loading

0 comments on commit c9cd89b

Please sign in to comment.