Skip to content

Commit

Permalink
✨ (keyring-eth) [DSDK-387]: Implement signTransaction use case (#228)
Browse files Browse the repository at this point in the history
  • Loading branch information
aussedatlo authored Sep 3, 2024
2 parents 6c46cde + 25a7e5c commit 8bfaa71
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 64 deletions.
5 changes: 5 additions & 0 deletions .changeset/real-crews-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/keyring-eth": minor
---

Implement SignTransactionUseCase
1 change: 1 addition & 0 deletions packages/signer/keyring-eth/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const config: JestConfigWithTsJest = {
"!src/**/*.stub.ts",
"!src/index.ts",
"!src/api/index.ts",
"!src/**/__test-utils__/*",
],
moduleNameMapper: {
...paths,
Expand Down
4 changes: 2 additions & 2 deletions packages/signer/keyring-eth/src/api/KeyringEth.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { GetAddressDAReturnType } from "@api/app-binder/GetAddressDeviceActionTypes";
import { SignTypedDataDAReturnType } from "@api/app-binder/SignTypedDataDeviceActionTypes";
import { AddressOptions } from "@api/model/AddressOptions";
import { Signature } from "@api/model/Signature";
import { Transaction } from "@api/model/Transaction";
import { TransactionOptions } from "@api/model/TransactionOptions";
import { TypedData } from "@api/model/TypedData";

import { SignPersonalMessageDAReturnType } from "./app-binder/SignPersonalMessageDeviceActionTypes";
import { SignTransactionDAReturnType } from "./app-binder/SignTransactionDeviceActionTypes";

export interface KeyringEth {
signTransaction: (
derivationPath: string,
transaction: Transaction,
options?: TransactionOptions,
) => Promise<Signature>;
) => SignTransactionDAReturnType;
signMessage: (
derivationPath: string,
message: string,
Expand Down
7 changes: 7 additions & 0 deletions packages/signer/keyring-eth/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ export type {
SignPersonalMessageDAOutput,
SignPersonalMessageDAState,
} from "@api/app-binder/SignPersonalMessageDeviceActionTypes";
export type {
SignTransactionDAError,
SignTransactionDAInput,
SignTransactionDAIntermediateValue,
SignTransactionDAOutput,
SignTransactionDAState,
} from "@api/app-binder/SignTransactionDeviceActionTypes";
export {
type SignTypedDataDAError,
type SignTypedDataDAInput,
Expand Down
6 changes: 3 additions & 3 deletions packages/signer/keyring-eth/src/internal/DefaultKeyringEth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { Container } from "inversify";

import { GetAddressDAReturnType } from "@api/app-binder/GetAddressDeviceActionTypes";
import { SignPersonalMessageDAReturnType } from "@api/app-binder/SignPersonalMessageDeviceActionTypes";
import { SignTransactionDAReturnType } from "@api/app-binder/SignTransactionDeviceActionTypes";
import { SignTypedDataDAReturnType } from "@api/app-binder/SignTypedDataDeviceActionTypes";
import { KeyringEth } from "@api/KeyringEth";
import { AddressOptions } from "@api/model/AddressOptions";
import { Signature } from "@api/model/Signature";
import { Transaction } from "@api/model/Transaction";
import { TransactionOptions } from "@api/model/TransactionOptions";
import { TypedData } from "@api/model/TypedData";
Expand Down Expand Up @@ -38,11 +38,11 @@ export class DefaultKeyringEth implements KeyringEth {
this._container = makeContainer({ sdk, sessionId, contextModule });
}

async signTransaction(
signTransaction(
derivationPath: string,
transaction: Transaction,
options?: TransactionOptions,
): Promise<Signature> {
): SignTransactionDAReturnType {
return this._container
.get<SignTransactionUseCase>(transactionTypes.SignTransactionUseCase)
.execute(derivationPath, transaction, options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,32 @@ import { DeviceActionState, DeviceSdk } from "@ledgerhq/device-sdk-core";
import { DeviceActionStatus } from "@ledgerhq/device-sdk-core";
import { SendCommandInAppDeviceAction } from "@ledgerhq/device-sdk-core";
import { UserInteractionRequired } from "@ledgerhq/device-sdk-core";
import { Transaction } from "ethers-v6";
import { from } from "rxjs";

import {
GetAddressDAError,
GetAddressDAIntermediateValue,
GetAddressDAOutput,
} from "@api/app-binder/GetAddressDeviceActionTypes";
import {
SignPersonalMessageDAError,
SignPersonalMessageDAIntermediateValue,
SignPersonalMessageDAOutput,
} from "@api/app-binder/SignPersonalMessageDeviceActionTypes";
import {
SignTransactionDAError,
SignTransactionDAIntermediateValue,
SignTransactionDAOutput,
} from "@api/app-binder/SignTransactionDeviceActionTypes";
import {
SignTypedDataDAError,
SignTypedDataDAIntermediateValue,
SignTypedDataDAOutput,
} from "@api/app-binder/SignTypedDataDeviceActionTypes";
import { type Signature } from "@api/model/Signature";
import { type TypedData } from "@api/model/TypedData";
import { TransactionMapperService } from "@internal/transaction/service/mapper/TransactionMapperService";
import { type TypedDataParserService } from "@internal/typed-data/service/TypedDataParserService";

import { GetAddressCommand } from "./command/GetAddressCommand";
Expand All @@ -31,6 +43,9 @@ describe("EthAppBinder", () => {
getContexts: jest.fn(),
getTypedDataFilters: jest.fn(),
};
const mockedMapper: TransactionMapperService = {
mapTransactionToSubset: jest.fn(),
} as unknown as TransactionMapperService;

beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -61,6 +76,7 @@ describe("EthAppBinder", () => {
const appBinder = new EthAppBinder(
mockedSdk,
mockedContextModule,
mockedMapper,
"sessionId",
);
const { observable } = appBinder.getAddress({
Expand Down Expand Up @@ -114,6 +130,7 @@ describe("EthAppBinder", () => {
const appBinder = new EthAppBinder(
mockedSdk,
mockedContextModule,
mockedMapper,
"sessionId",
);
appBinder.getAddress(params);
Expand Down Expand Up @@ -143,6 +160,7 @@ describe("EthAppBinder", () => {
const appBinder = new EthAppBinder(
mockedSdk,
mockedContextModule,
mockedMapper,
"sessionId",
);
appBinder.getAddress(params);
Expand All @@ -162,6 +180,209 @@ describe("EthAppBinder", () => {
});
});

describe("signTransaction", () => {
it("should return the signature", (done) => {
// GIVEN
const signature: Signature = {
r: `0xDEAD`,
s: `0xBEEF`,
v: 0,
};
const transaction: Transaction = new Transaction();
transaction.to = "0x1234567890123456789012345678901234567890";
transaction.value = 0n;
const options = {};

jest.spyOn(mockedSdk, "executeDeviceAction").mockReturnValue({
observable: from([
{
status: DeviceActionStatus.Completed,
output: signature,
} as DeviceActionState<
SignTypedDataDAOutput,
SignTypedDataDAError,
SignTypedDataDAIntermediateValue
>,
]),
cancel: jest.fn(),
});

// WHEN
const appBinder = new EthAppBinder(
mockedSdk,
mockedContextModule,
mockedMapper,
"sessionId",
);
const { observable } = appBinder.signTransaction({
derivationPath: "44'/60'/3'/2/1",
transaction,
options,
});

// THEN
const states: DeviceActionState<
SignTransactionDAOutput,
SignTransactionDAError,
SignTransactionDAIntermediateValue
>[] = [];
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);
}
},
});
});

it("should return the signature without options", (done) => {
// GIVEN
const signature: Signature = {
r: `0xDEAD`,
s: `0xBEEF`,
v: 0,
};
const transaction: Transaction = new Transaction();
transaction.to = "0x1234567890123456789012345678901234567890";
transaction.value = 0n;

jest.spyOn(mockedSdk, "executeDeviceAction").mockReturnValue({
observable: from([
{
status: DeviceActionStatus.Completed,
output: signature,
} as DeviceActionState<
SignTypedDataDAOutput,
SignTypedDataDAError,
SignTypedDataDAIntermediateValue
>,
]),
cancel: jest.fn(),
});

// WHEN
const appBinder = new EthAppBinder(
mockedSdk,
mockedContextModule,
mockedMapper,
"sessionId",
);
const { observable } = appBinder.signTransaction({
derivationPath: "44'/60'/3'/2/1",
transaction,
options: undefined,
});

// THEN
const states: DeviceActionState<
SignTransactionDAOutput,
SignTransactionDAError,
SignTransactionDAIntermediateValue
>[] = [];
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);
}
},
});
});
});

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

jest.spyOn(mockedSdk, "executeDeviceAction").mockReturnValue({
observable: from([
{
status: DeviceActionStatus.Completed,
output: signature,
} as DeviceActionState<
SignPersonalMessageDAOutput,
SignPersonalMessageDAError,
SignPersonalMessageDAIntermediateValue
>,
]),
cancel: jest.fn(),
});

// WHEN
const appBinder = new EthAppBinder(
mockedSdk,
mockedContextModule,
mockedMapper,
"sessionId",
);
const { observable } = appBinder.signPersonalMessage({
derivationPath: "44'/60'/3'/2/1",
message,
});

// THEN
const states: DeviceActionState<
SignPersonalMessageDAOutput,
SignPersonalMessageDAError,
SignPersonalMessageDAIntermediateValue
>[] = [];
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);
}
},
});
});
});

describe("signTypedData", () => {
it("should return the signature", (done) => {
// GIVEN
Expand Down Expand Up @@ -198,6 +419,7 @@ describe("EthAppBinder", () => {
const appBinder = new EthAppBinder(
mockedSdk,
mockedContextModule,
mockedMapper,
"sessionId",
);
const { observable } = appBinder.signTypedData({
Expand Down
Loading

0 comments on commit 8bfaa71

Please sign in to comment.