Skip to content

Commit

Permalink
✨ (keyring-eth) [DSDK-435]: Add BuildTransactionContextTask (#215)
Browse files Browse the repository at this point in the history
  • Loading branch information
aussedatlo authored Aug 28, 2024
2 parents e5ece4c + 43eb989 commit d9cf78a
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 50 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-phones-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/keyring-eth": patch
---

Add BuildTransactionContextTask
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const LNX_RESPONSE_DATA_GOOD = new ApduResponse({

describe("SignTransactionCommand", () => {
const defaultArgs: SignTransactionCommandArgs = {
transaction: new Uint8Array(),
serializedTransaction: new Uint8Array(),
isFirstChunk: true,
};

Expand All @@ -59,7 +59,7 @@ describe("SignTransactionCommand", () => {
// GIVEN
const command = new SignTransactionCommand({
...defaultArgs,
transaction: new Uint8Array([0x01, 0x02, 0x03]),
serializedTransaction: new Uint8Array([0x01, 0x02, 0x03]),
});

// WHEN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type SignTransactionCommandArgs = {
/**
* The transaction to sign in max 150 bytes chunks
*/
readonly transaction: Uint8Array;
readonly serializedTransaction: Uint8Array;
/**
* If this is the first chunk of the message
*/
Expand All @@ -46,7 +46,7 @@ export class SignTransactionCommand
}

getApdu(): Apdu {
const { transaction, isFirstChunk } = this.args;
const { serializedTransaction, isFirstChunk } = this.args;

const signEthTransactionArgs: ApduBuilderArgs = {
cla: 0xe0,
Expand All @@ -55,7 +55,7 @@ export class SignTransactionCommand
p2: 0x00,
};
const builder = new ApduBuilder(signEthTransactionArgs);
return builder.addBufferToData(transaction).build();
return builder.addBufferToData(serializedTransaction).build();
}

parseResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { ClearSignContext } from "@ledgerhq/context-module";
import { Transaction } from "ethers-v6";
import { Left, Right } from "purify-ts";

import { TransactionMapperResult } from "@internal/transaction/service/mapper/model/TransactionMapperResult";
import { TransactionMapperService } from "@internal/transaction/service/mapper/TransactionMapperService";

import { BuildTransactionContextTask } from "./BuildTransactionContextTask";

describe("BuildTransactionContextTask", () => {
const contextModuleMock = {
getContexts: jest.fn(),
getTypedDataFilters: jest.fn(),
};
const mapperMock = {
mapTransactionToSubset: jest.fn(),
};
const defaultOptions = {
domain: "domain-name.eth",
};
let defaultTransaction: Transaction;

beforeEach(() => {
jest.clearAllMocks();

defaultTransaction = new Transaction();
defaultTransaction.chainId = 1n;
defaultTransaction.nonce = 0;
defaultTransaction.data = "0x";
});

it("should build the transaction context without clear sign contexts", async () => {
// GIVEN
const serializedTransaction = new Uint8Array([0x01, 0x02, 0x03]);
const clearSignContexts: ClearSignContext[] = [];
const mapperResult: TransactionMapperResult = {
subset: { chainId: 1, to: undefined, data: "0x" },
serializedTransaction,
};
mapperMock.mapTransactionToSubset.mockReturnValueOnce(Right(mapperResult));
contextModuleMock.getContexts.mockResolvedValueOnce(clearSignContexts);

// WHEN
const result = await new BuildTransactionContextTask(
contextModuleMock,
mapperMock as unknown as TransactionMapperService,
defaultTransaction,
defaultOptions,
"challenge",
).run();

// THEN
expect(result).toEqual({
clearSignContexts,
serializedTransaction,
});
});

it("should build the transaction context with clear sign contexts", async () => {
// GIVEN
const serializedTransaction = new Uint8Array([0x01, 0x02, 0x03]);
const clearSignContexts: ClearSignContext[] = [
{
type: "token",
payload: "payload-1",
},
{
type: "nft",
payload: "payload-2",
},
];
const mapperResult: TransactionMapperResult = {
subset: { chainId: 1, to: undefined, data: "0x" },
serializedTransaction,
};
mapperMock.mapTransactionToSubset.mockReturnValueOnce(Right(mapperResult));
contextModuleMock.getContexts.mockResolvedValueOnce(clearSignContexts);

// WHEN
const result = await new BuildTransactionContextTask(
contextModuleMock,
mapperMock as unknown as TransactionMapperService,
defaultTransaction,
defaultOptions,
"challenge",
).run();

// THEN
expect(result).toEqual({
clearSignContexts,
serializedTransaction,
});
});

it("should call the mapper with the transaction", async () => {
// GIVEN
const serializedTransaction = new Uint8Array([0x01, 0x02, 0x03]);
const clearSignContexts: ClearSignContext[] = [];
const mapperResult: TransactionMapperResult = {
subset: { chainId: 1, to: undefined, data: "0x" },
serializedTransaction,
};
mapperMock.mapTransactionToSubset.mockReturnValueOnce(Right(mapperResult));
contextModuleMock.getContexts.mockResolvedValueOnce(clearSignContexts);

// WHEN
await new BuildTransactionContextTask(
contextModuleMock,
mapperMock as unknown as TransactionMapperService,
defaultTransaction,
defaultOptions,
"challenge",
).run();

// THEN
expect(mapperMock.mapTransactionToSubset).toHaveBeenCalledWith(
defaultTransaction,
);
});

it("should call the context module with the correct parameters", async () => {
// GIVEN
const serializedTransaction = new Uint8Array([0x01, 0x02, 0x03]);
const clearSignContexts: ClearSignContext[] = [];
const mapperResult: TransactionMapperResult = {
subset: { chainId: 1, to: undefined, data: "0x" },
serializedTransaction,
};
mapperMock.mapTransactionToSubset.mockReturnValueOnce(Right(mapperResult));
contextModuleMock.getContexts.mockResolvedValueOnce(clearSignContexts);

// WHEN
await new BuildTransactionContextTask(
contextModuleMock,
mapperMock as unknown as TransactionMapperService,
defaultTransaction,
defaultOptions,
"challenge",
).run();

// THEN
expect(contextModuleMock.getContexts).toHaveBeenCalledWith({
challenge: "challenge",
domain: "domain-name.eth",
...mapperResult.subset,
});
});

it("should throw an error if the mapper returns an error", async () => {
// GIVEN
const error = new Error("error");
mapperMock.mapTransactionToSubset.mockReturnValueOnce(Left(error));

// WHEN
const task = new BuildTransactionContextTask(
contextModuleMock,
mapperMock as unknown as TransactionMapperService,
defaultTransaction,
defaultOptions,
"challenge",
);

// THEN
await expect(task.run()).rejects.toThrow(error);
});

it("should exclude error contexts from the result", async () => {
// GIVEN
const serializedTransaction = new Uint8Array([0x01, 0x02, 0x03]);
const clearSignContexts: ClearSignContext[] = [
{
type: "error",
error: new Error("error"),
},
{
type: "token",
payload: "payload-1",
},
{
type: "error",
error: new Error("error"),
},
{
type: "nft",
payload: "payload-2",
},
];
const mapperResult: TransactionMapperResult = {
subset: { chainId: 1, to: undefined, data: "0x" },
serializedTransaction,
};
mapperMock.mapTransactionToSubset.mockReturnValueOnce(Right(mapperResult));
contextModuleMock.getContexts.mockResolvedValueOnce(clearSignContexts);

// WHEN
const result = await new BuildTransactionContextTask(
contextModuleMock,
mapperMock as unknown as TransactionMapperService,
defaultTransaction,
defaultOptions,
"challenge",
).run();

// THEN
expect(result).toEqual({
clearSignContexts: [clearSignContexts[1], clearSignContexts[3]],
serializedTransaction,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
ClearSignContextSuccess,
ContextModule,
} from "@ledgerhq/context-module";

import { Transaction } from "@api/model/Transaction";
import { TransactionOptions } from "@api/model/TransactionOptions";
import { TransactionMapperService } from "@internal/transaction/service/mapper/TransactionMapperService";

export type BuildTransactionTaskResult = {
readonly clearSignContexts: ClearSignContextSuccess[];
readonly serializedTransaction: Uint8Array;
};

export class BuildTransactionContextTask {
constructor(
private contextModule: ContextModule,
private mapper: TransactionMapperService,
private transaction: Transaction,
private options: TransactionOptions,
private challenge: string,
) {}

async run(): Promise<BuildTransactionTaskResult> {
const parsed = this.mapper.mapTransactionToSubset(this.transaction);
parsed.ifLeft((err) => {
throw err;
});
const { subset, serializedTransaction } = parsed.unsafeCoerce();

const clearSignContexts = await this.contextModule.getContexts({
challenge: this.challenge,
domain: this.options.domain,
...subset,
});

// TODO: for now we ignore the error contexts
// as we consider that they are warnings and not blocking
const clearSignContextsSuccess: ClearSignContextSuccess[] =
clearSignContexts.filter((context) => context.type !== "error");

return {
clearSignContexts: clearSignContextsSuccess,
serializedTransaction,
};
}
}
Loading

0 comments on commit d9cf78a

Please sign in to comment.