Skip to content

Commit

Permalink
✨ (context-module) [DSDK-527]: Update SignTransaction with generic-pa…
Browse files Browse the repository at this point in the history
…rser (#518)
  • Loading branch information
paoun-ledger authored Dec 2, 2024
2 parents d4b9d66 + 4061dbc commit 32461ba
Show file tree
Hide file tree
Showing 20 changed files with 824 additions and 56 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-camels-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-signer-kit-ethereum": minor
---

Update SignTransaction device action with generic-parser support
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,13 @@ export interface CalldataDescriptorValueV1 {
type_size?: number;
}

export interface CalldataDescriptorContainerPathV1 {
type: "CONTAINER";
value: CalldataDescriptorContainerPathTypeV1;
}

export interface CalldataDescriptorPathElementsV1 {
type: "DATA";
elements: CalldataDescriptorPathElementV1[];
}

Expand Down Expand Up @@ -150,7 +156,7 @@ export interface CalldataDescriptorPathElementSliceV1 {
end?: number;
}

export type CalldataDescriptorContainerPathV1 = "FROM" | "TO" | "VALUE";
export type CalldataDescriptorContainerPathTypeV1 = "FROM" | "TO" | "VALUE";
export type CalldataDescriptorPathLeafTypeV1 =
| "ARRAY_LEAF"
| "TUPLE_LEAF"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ describe("HttpTransactionDataSource", () => {
param: {
value: {
binary_path: {
type: "DATA",
elements: [
{
type: "TUPLE",
Expand All @@ -83,6 +84,7 @@ describe("HttpTransactionDataSource", () => {
type: "TOKEN_AMOUNT",
token: {
binary_path: {
type: "DATA",
elements: [
{
type: "ARRAY",
Expand All @@ -106,7 +108,10 @@ describe("HttpTransactionDataSource", () => {
fieldTrustedName = {
param: {
value: {
binary_path: "TO",
binary_path: {
type: "CONTAINER",
value: "TO",
},
type_family: "STRING",
type_size: 20,
},
Expand All @@ -121,6 +126,7 @@ describe("HttpTransactionDataSource", () => {
param: {
value: {
binary_path: {
type: "DATA",
elements: [
{
type: "ARRAY",
Expand All @@ -141,6 +147,7 @@ describe("HttpTransactionDataSource", () => {
},
collection: {
binary_path: {
type: "DATA",
elements: [
{
type: "REF",
Expand All @@ -166,15 +173,18 @@ describe("HttpTransactionDataSource", () => {
});

function createFieldWithoutReference(
binary_path: unknown,
binary_path: string,
type_family: string,
type: string,
descriptor: string,
): CalldataFieldV1 {
return {
param: {
value: {
binary_path,
binary_path: {
type: "CONTAINER",
value: binary_path,
},
type_family,
type_size: 32,
},
Expand Down Expand Up @@ -310,7 +320,7 @@ describe("HttpTransactionDataSource", () => {
expect(result.extract()).toEqual([
{
payload:
"0001000108000000000000000102147d2768de32b0b80b7a3454c06bdac94a69ddc7a9030469328dec04207d5e9ed0004b8035b164edd9d78c37415ad6b1d123be4943d0abd5a50035cae3050857697468647261770604416176650708416176652044414f081068747470733a2f2f616176652e636f6d0a045fc4ba9c3045022100eb67599abfd9c7360b07599a2a2cb769c6e3f0f74e1e52444d788c8f577a16d20220402e92b0adbf97d890fa2f9654bc30c7bd70dacabe870f160e6842d9eb73d36f",
"0001000108000000000000000102147d2768de32b0b80b7a3454c06bdac94a69ddc7a9030469328dec04207d5e9ed0004b8035b164edd9d78c37415ad6b1d123be4943d0abd5a50035cae3050857697468647261770604416176650708416176652044414f081068747470733a2f2f616176652e636f6d0a045fc4ba9c81ff473045022100eb67599abfd9c7360b07599a2a2cb769c6e3f0f74e1e52444d788c8f577a16d20220402e92b0adbf97d890fa2f9654bc30c7bd70dacabe870f160e6842d9eb73d36f",
type: "transactionInfo",
},
{
Expand Down Expand Up @@ -393,7 +403,7 @@ describe("HttpTransactionDataSource", () => {
expect(result.extract()).toEqual([
{
payload:
"0001000108000000000000000102147d2768de32b0b80b7a3454c06bdac94a69ddc7a9030469328dec04207d5e9ed0004b8035b164edd9d78c37415ad6b1d123be4943d0abd5a50035cae3050857697468647261770604416176650708416176652044414f081068747470733a2f2f616176652e636f6d0a045fc4ba9c3045022100eb67599abfd9c7360b07599a2a2cb769c6e3f0f74e1e52444d788c8f577a16d20220402e92b0adbf97d890fa2f9654bc30c7bd70dacabe870f160e6842d9eb73d36f",
"0001000108000000000000000102147d2768de32b0b80b7a3454c06bdac94a69ddc7a9030469328dec04207d5e9ed0004b8035b164edd9d78c37415ad6b1d123be4943d0abd5a50035cae3050857697468647261770604416176650708416176652044414f081068747470733a2f2f616176652e636f6d0a045fc4ba9c81ff473045022100eb67599abfd9c7360b07599a2a2cb769c6e3f0f74e1e52444d788c8f577a16d20220402e92b0adbf97d890fa2f9654bc30c7bd70dacabe870f160e6842d9eb73d36f",
type: "transactionInfo",
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class HttpTransactionDataSource implements TransactionDataSource {
];
const info: ClearSignContextSuccess = {
type: ClearSignContextType.TRANSACTION_INFO,
payload: `${infoData}${infoSignature}`,
payload: this.formatTransactionInfo(infoData, infoSignature),
};
const enums: ClearSignContextSuccess[] = calldataDescriptor.enums.map(
(e) => ({
Expand All @@ -126,6 +126,20 @@ export class HttpTransactionDataSource implements TransactionDataSource {
return Right([info, ...enums, ...fields]);
}

private formatTransactionInfo(
infoData: string,
infoSignature: string,
): string {
// Ensure correct padding
if (infoSignature.length % 2 !== 0) {
infoSignature = "0" + infoSignature;
}
// TLV encoding as according to generic parser documentation
const infoSignatureTag = "81ff";
const infoSignatureLength = (infoSignature.length / 2).toString(16);
return `${infoData}${infoSignatureTag}${infoSignatureLength}${infoSignature}`;
}

private getReference(
param: CalldataDescriptorParam,
): ClearSignContextReference | undefined {
Expand Down Expand Up @@ -153,8 +167,8 @@ export class HttpTransactionDataSource implements TransactionDataSource {
private toGenericPath(
path: CalldataDescriptorContainerPathV1 | CalldataDescriptorPathElementsV1,
): GenericPath {
if (typeof path !== "object") {
return path;
if (path.type === "CONTAINER") {
return path.value;
}
return path.elements.map((element) => {
if (element.type === "ARRAY") {
Expand Down Expand Up @@ -259,9 +273,10 @@ export class HttpTransactionDataSource implements TransactionDataSource {
].includes(data.type_family) &&
(typeof data.type_size === "undefined" ||
typeof data.type_size === "number") &&
((typeof data.binary_path === "string" &&
["FROM", "TO", "VALUE"].includes(data.binary_path)) ||
(typeof data.binary_path === "object" &&
((typeof data.binary_path === "object" &&
data.binary_path.type === "CONTAINER" &&
["FROM", "TO", "VALUE"].includes(data.binary_path.value)) ||
(data.binary_path.type === "DATA" &&
Array.isArray(data.binary_path.elements) &&
data.binary_path.elements.every((e) => this.isPathElementV1(e))))
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ describe("HttpTrustedNameDataSource", () => {
// GIVEN
const response = {
data: {
signedDescriptor: { data: "payload", signatures: { prod: "sig" } },
signedDescriptor: { data: "payload" },
},
};
jest.spyOn(axios, "request").mockResolvedValue(response);
Expand All @@ -187,7 +187,28 @@ describe("HttpTrustedNameDataSource", () => {
});

// THEN
expect(result).toEqual(Right("payloadsig"));
expect(result).toEqual(Right("payload"));
});

it("should return a payload with a signature", async () => {
// GIVEN
const response = {
data: {
signedDescriptor: { data: "payload", signatures: { prod: "12345" } },
},
};
jest.spyOn(axios, "request").mockResolvedValue(response);

// WHEN
const result = await datasource.getTrustedNamePayload({
address: "0x1234",
challenge: "",
sources: ["ens"],
types: ["eoa"],
});

// THEN
expect(result).toEqual(Right("payload153012345"));
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export class HttpTrustedNameDataSource implements TrustedNameDataSource {
types,
}: GetTrustedNameInfosParams): Promise<Either<Error, string>> {
try {
// TODO remove that filtering once https://ledgerhq.atlassian.net/browse/BACK-8075 is done
// For now we have to filter or trusted names won't work with the generic parser, because transaction
// fields descriptors can contain unsupported sources.
sources = sources.filter(
(source) => source === "ens" || source === "crypto_assets_list",
);
const response = await axios.request<TrustedNameDto>({
method: "GET",
url: `https://nft.api.live.ledger.com/v2/names/ethereum/1/reverse/${address}?types=${types.join(",")}&sources=${sources.join(",")}&challenge=${challenge}`,
Expand All @@ -65,28 +71,28 @@ export class HttpTrustedNameDataSource implements TrustedNameDataSource {
},
});
const trustedName = response.data;
if (!trustedName?.signedDescriptor?.data) {
return Left(
new Error(
`[ContextModule] HttpTrustedNameDataSource: no trusted name metadata for address ${address}`,
),
);
}
const payload = trustedName.signedDescriptor.data;

if (
!trustedName ||
!trustedName.signedDescriptor ||
!trustedName.signedDescriptor.data ||
!trustedName.signedDescriptor.signatures ||
typeof trustedName.signedDescriptor.signatures[this.config.cal.mode] !==
"string"
) {
return Left(
new Error(
`[ContextModule] HttpTrustedNameDataSource: no trusted name metadata for address ${address}`,
),
);
// If we have no separated signature but a valid descriptor, it may mean the descriptor was
// signed on-the-fly for dynamic sources such as ens
return Right(payload);
}

return Right(
[
trustedName.signedDescriptor.data,
trustedName.signedDescriptor.signatures[this.config.cal.mode],
].join(""),
);
const signature =
trustedName.signedDescriptor.signatures[this.config.cal.mode]!;
return Right(this.formatTrustedName(payload, signature));
} catch (_error) {
return Left(
new Error(
Expand All @@ -95,4 +101,15 @@ export class HttpTrustedNameDataSource implements TrustedNameDataSource {
);
}
}

private formatTrustedName(payload: string, signature: string): string {
// Ensure correct padding
if (signature.length % 2 !== 0) {
signature = "0" + signature;
}
// TLV encoding as according to trusted name documentation
const signatureTag = "15";
const signatureLength = (signature.length / 2).toString(16);
return `${payload}${signatureTag}${signatureLength}${signature}`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ import { type Signature } from "@api/model/Signature";
import { type Transaction, type TransactionType } from "@api/model/Transaction";
import { type TransactionOptions } from "@api/model/TransactionOptions";
import { type ProvideTransactionContextTaskErrorCodes } from "@internal/app-binder/task/ProvideTransactionContextTask";
import { type GenericContext } from "@internal/app-binder/task/ProvideTransactionGenericContextTask";
import { type TransactionMapperService } from "@internal/transaction/service/mapper/TransactionMapperService";
import { type TransactionParserService } from "@internal/transaction/service/parser/TransactionParserService";

export type SignTransactionDAOutput = Signature;

export type SignTransactionDAInput = {
readonly derivationPath: string;
readonly transaction: Transaction;
readonly mapper: TransactionMapperService;
readonly parser: TransactionParserService;
readonly contextModule: ContextModule;
readonly options: TransactionOptions;
};
Expand All @@ -49,10 +52,11 @@ export type SignTransactionDAState = DeviceActionState<
export type SignTransactionDAInternalState = {
readonly error: SignTransactionDAError | null;
readonly challenge: string | null;
readonly clearSignContexts: ClearSignContextSuccess[] | null;
readonly clearSignContexts: ClearSignContextSuccess[] | GenericContext | null;
readonly serializedTransaction: Uint8Array | null;
readonly chainId: number | null;
readonly transactionType: TransactionType | null;
readonly isLegacy: boolean;
readonly signature: Signature | null;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import { type Signature } from "@api/model/Signature";
import { type TypedData } from "@api/model/TypedData";
import { type TransactionMapperService } from "@internal/transaction/service/mapper/TransactionMapperService";
import { type TransactionParserService } from "@internal/transaction/service/parser/TransactionParserService";
import { type TypedDataParserService } from "@internal/typed-data/service/TypedDataParserService";

import { GetAddressCommand } from "./command/GetAddressCommand";
Expand All @@ -50,6 +51,9 @@ describe("EthAppBinder", () => {
const mockedMapper: TransactionMapperService = {
mapTransactionToSubset: jest.fn(),
} as unknown as TransactionMapperService;
const mockedParser: TransactionParserService = {
extractValue: jest.fn(),
} as unknown as TransactionParserService;

beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -81,6 +85,7 @@ describe("EthAppBinder", () => {
mockedDmk,
mockedContextModule,
mockedMapper,
mockedParser,
"sessionId",
);
const { observable } = appBinder.getAddress({
Expand Down Expand Up @@ -135,6 +140,7 @@ describe("EthAppBinder", () => {
mockedDmk,
mockedContextModule,
mockedMapper,
mockedParser,
"sessionId",
);
appBinder.getAddress(params);
Expand Down Expand Up @@ -165,6 +171,7 @@ describe("EthAppBinder", () => {
mockedDmk,
mockedContextModule,
mockedMapper,
mockedParser,
"sessionId",
);
appBinder.getAddress(params);
Expand Down Expand Up @@ -216,6 +223,7 @@ describe("EthAppBinder", () => {
mockedDmk,
mockedContextModule,
mockedMapper,
mockedParser,
"sessionId",
);
const { observable } = appBinder.signTransaction({
Expand Down Expand Up @@ -283,6 +291,7 @@ describe("EthAppBinder", () => {
mockedDmk,
mockedContextModule,
mockedMapper,
mockedParser,
"sessionId",
);
const { observable } = appBinder.signTransaction({
Expand Down Expand Up @@ -350,6 +359,7 @@ describe("EthAppBinder", () => {
mockedDmk,
mockedContextModule,
mockedMapper,
mockedParser,
"sessionId",
);
const { observable } = appBinder.signPersonalMessage({
Expand Down Expand Up @@ -424,6 +434,7 @@ describe("EthAppBinder", () => {
mockedDmk,
mockedContextModule,
mockedMapper,
mockedParser,
"sessionId",
);
const { observable } = appBinder.signTypedData({
Expand Down
Loading

0 comments on commit 32461ba

Please sign in to comment.