From 9ad6c3161dca12c71fb49b1c625f0a524dbbc0f5 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Mon, 3 Feb 2025 10:35:30 +0100 Subject: [PATCH 01/12] =?UTF-8?q?=E2=9C=A8=20(context-module):=20Add=20web?= =?UTF-8?q?3checks=20loader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/six-toys-lay.md | 5 + .../context-module/src/ContextModule.ts | 4 + .../src/ContextModuleBuilder.test.ts | 40 +++++++- .../src/ContextModuleBuilder.ts | 32 ++++++ .../src/DefaultContextModule.test.ts | 47 ++++++++- .../src/DefaultContextModule.ts | 30 ++++++ .../src/config/model/ContextModuleConfig.ts | 7 ++ packages/signer/context-module/src/di.ts | 2 + packages/signer/context-module/src/index.ts | 2 + .../src/shared/model/ClearSignContext.ts | 1 + .../src/shared/model/TransactionSubset.ts | 2 + .../data/HttpWeb3CheckDataSource.test.ts | 97 +++++++++++++++++++ .../data/HttpWeb3CheckDataSource.ts | 82 ++++++++++++++++ .../web3-check/data/Web3CheckDataSource.ts | 10 ++ .../src/web3-check/data/Web3CheckDto.ts | 15 +++ .../web3-check/di/web3CheckModuleFactory.ts | 14 +++ .../src/web3-check/di/web3CheckTypes.ts | 4 + .../domain/DefaultWeb3CheckLoader.test.ts | 81 ++++++++++++++++ .../domain/DefaultWeb3CheckLoader.ts | 49 ++++++++++ .../domain/Web3CheckContextLoader.ts | 7 ++ .../src/web3-check/domain/web3CheckTypes.ts | 10 ++ 21 files changed, 537 insertions(+), 4 deletions(-) create mode 100644 .changeset/six-toys-lay.md create mode 100644 packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts create mode 100644 packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts create mode 100644 packages/signer/context-module/src/web3-check/data/Web3CheckDataSource.ts create mode 100644 packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts create mode 100644 packages/signer/context-module/src/web3-check/di/web3CheckModuleFactory.ts create mode 100644 packages/signer/context-module/src/web3-check/di/web3CheckTypes.ts create mode 100644 packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.test.ts create mode 100644 packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.ts create mode 100644 packages/signer/context-module/src/web3-check/domain/Web3CheckContextLoader.ts create mode 100644 packages/signer/context-module/src/web3-check/domain/web3CheckTypes.ts diff --git a/.changeset/six-toys-lay.md b/.changeset/six-toys-lay.md new file mode 100644 index 000000000..452c86274 --- /dev/null +++ b/.changeset/six-toys-lay.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/context-module": minor +--- + +Add web3checks loader diff --git a/packages/signer/context-module/src/ContextModule.ts b/packages/signer/context-module/src/ContextModule.ts index 7a5df37d4..57740b3ae 100644 --- a/packages/signer/context-module/src/ContextModule.ts +++ b/packages/signer/context-module/src/ContextModule.ts @@ -6,6 +6,7 @@ import { } from "./shared/model/TransactionContext"; import { type TypedDataClearSignContext } from "./shared/model/TypedDataClearSignContext"; import { type TypedDataContext } from "./shared/model/TypedDataContext"; +import { type Web3CheckContext } from "./web3-check/domain/web3CheckTypes"; export interface ContextModule { getContext(field: TransactionFieldContext): Promise; @@ -13,4 +14,7 @@ export interface ContextModule { getTypedDataFilters( typedData: TypedDataContext, ): Promise; + getWeb3Checks( + transactionContext: Web3CheckContext, + ): Promise; } diff --git a/packages/signer/context-module/src/ContextModuleBuilder.test.ts b/packages/signer/context-module/src/ContextModuleBuilder.test.ts index 658acce61..103cfa2c2 100644 --- a/packages/signer/context-module/src/ContextModuleBuilder.test.ts +++ b/packages/signer/context-module/src/ContextModuleBuilder.test.ts @@ -1,13 +1,22 @@ -import { type ContextModuleCalConfig } from "./config/model/ContextModuleConfig"; +import { type Container } from "inversify"; + +import { configTypes } from "./config/di/configTypes"; +import { + type ContextModuleCalConfig, + type ContextModuleConfig, +} from "./config/model/ContextModuleConfig"; import { ContextModuleBuilder } from "./ContextModuleBuilder"; import { DefaultContextModule } from "./DefaultContextModule"; describe("ContextModuleBuilder", () => { const defaultCalConfig: ContextModuleCalConfig = { - url: "https://crypto-assets-service.api.ledger.com/v1", + url: "https://cal/v1", mode: "prod", branch: "main", }; + const defaultWeb3ChecksConfig = { + url: "https://web3checks/v1", + }; it("should return a default context module", () => { const contextModuleBuilder = new ContextModuleBuilder(); @@ -38,13 +47,38 @@ describe("ContextModuleBuilder", () => { .build(); expect(res).toBeInstanceOf(DefaultContextModule); + // @ts-expect-error _typedDataLoader is private + expect(res["_typedDataLoader"]).toBe(customLoader); }); it("should return a custom context module with a custom config", () => { const contextModuleBuilder = new ContextModuleBuilder(); - const res = contextModuleBuilder.addCalConfig(defaultCalConfig).build(); + const res = contextModuleBuilder + .addCalConfig(defaultCalConfig) + .addWeb3ChecksConfig(defaultWeb3ChecksConfig) + .build(); + // @ts-expect-error _container is private + const config = (res["_container"] as Container).get( + configTypes.Config, + ); + + expect(res).toBeInstanceOf(DefaultContextModule); + expect(config.cal).toEqual(defaultCalConfig); + expect(config.web3checks).toEqual(defaultWeb3ChecksConfig); + }); + + it("should return a custom context module with a custom custom web3checks loader", () => { + const contextModuleBuilder = new ContextModuleBuilder(); + const customLoader = { load: jest.fn() }; + + const res = contextModuleBuilder + .removeDefaultLoaders() + .addWeb3CheckLoader(customLoader) + .build(); expect(res).toBeInstanceOf(DefaultContextModule); + // @ts-expect-error _web3CheckLoader is private + expect(res["_web3CheckLoader"]).toBe(customLoader); }); }); diff --git a/packages/signer/context-module/src/ContextModuleBuilder.ts b/packages/signer/context-module/src/ContextModuleBuilder.ts index 5689f8107..eb060cc24 100644 --- a/packages/signer/context-module/src/ContextModuleBuilder.ts +++ b/packages/signer/context-module/src/ContextModuleBuilder.ts @@ -1,13 +1,17 @@ import { type ContextModuleCalConfig, type ContextModuleConfig, + type ContextModuleWeb3ChecksConfig, } from "./config/model/ContextModuleConfig"; import { type ContextLoader } from "./shared/domain/ContextLoader"; import { type TypedDataContextLoader } from "./typed-data/domain/TypedDataContextLoader"; +import { type Web3CheckContextLoader } from "./web3-check/domain/Web3CheckContextLoader"; import { type ContextModule } from "./ContextModule"; import { DefaultContextModule } from "./DefaultContextModule"; const DEFAULT_CAL_URL = "https://crypto-assets-service.api.ledger.com/v1"; +const DEFAULT_WEB3_CHECKS_URL = + "https://web3checks-backend.api.aws.prd.ldg-tech.com/v3"; export const DEFAULT_CONFIG: ContextModuleConfig = { cal: { @@ -15,6 +19,9 @@ export const DEFAULT_CONFIG: ContextModuleConfig = { mode: "prod", branch: "main", }, + web3checks: { + url: DEFAULT_WEB3_CHECKS_URL, + }, defaultLoaders: true, customLoaders: [], customTypedDataLoader: undefined, @@ -57,6 +64,17 @@ export class ContextModuleBuilder { return this; } + /** + * Replace the default loader for web3 checks + * + * @param loader loader to use for web3 checks + * @returns this + */ + addWeb3CheckLoader(loader: Web3CheckContextLoader) { + this.config.customWeb3CheckLoader = loader; + return this; + } + /** * Add a custom CAL configuration * @@ -68,6 +86,20 @@ export class ContextModuleBuilder { return this; } + /** + * Add a custom web3 checks configuration + * + * @param web3ChecksConfig + * @returns this + */ + addWeb3ChecksConfig(web3ChecksConfig: ContextModuleWeb3ChecksConfig) { + this.config.web3checks = { + ...DEFAULT_CONFIG.web3checks, + ...web3ChecksConfig, + }; + return this; + } + /** * Build the context module * diff --git a/packages/signer/context-module/src/DefaultContextModule.test.ts b/packages/signer/context-module/src/DefaultContextModule.test.ts index 05e41b89d..ac66402ae 100644 --- a/packages/signer/context-module/src/DefaultContextModule.test.ts +++ b/packages/signer/context-module/src/DefaultContextModule.test.ts @@ -1,3 +1,5 @@ +import { Left, Right } from "purify-ts"; + import { type ContextModuleConfig } from "./config/model/ContextModuleConfig"; import { type TransactionContext, @@ -18,10 +20,13 @@ describe("DefaultContextModule", () => { defaultLoaders: false, customTypedDataLoader: typedDataLoader, cal: { - url: "https://crypto-assets-service.api.ledger.com/v1", + url: "https://cal/v1", mode: "prod", branch: "main", }, + web3checks: { + url: "https://web3checks/v3", + }, }; beforeEach(() => { @@ -109,6 +114,46 @@ describe("DefaultContextModule", () => { expect(res).toEqual({ type: "token", payload: "payload" }); }); + it("should return a web3 check context", async () => { + const loader = contextLoaderStubBuilder(); + jest + .spyOn(loader, "load") + .mockResolvedValueOnce(Right({ descriptor: "payload" })); + const contextModule = new DefaultContextModule({ + ...defaultContextModuleConfig, + customLoaders: [], + customWeb3CheckLoader: loader, + }); + + const res = await contextModule.getWeb3Checks({ + from: "from", + rawTx: "rawTx", + chainId: 1, + }); + + expect(loader.load).toHaveBeenCalledTimes(1); + expect(res).toEqual({ type: "web3Check", payload: "payload" }); + }); + + it("should return null if no web3 check context", async () => { + const loader = contextLoaderStubBuilder(); + jest.spyOn(loader, "load").mockResolvedValue(Left(new Error("error"))); + const contextModule = new DefaultContextModule({ + ...defaultContextModuleConfig, + customLoaders: [], + customWeb3CheckLoader: loader, + }); + + const res = await contextModule.getWeb3Checks({ + from: "from", + rawTx: "rawTx", + chainId: 1, + }); + + expect(loader.load).toHaveBeenCalledTimes(1); + expect(res).toBeNull(); + }); + it("context field not supported", async () => { const loader = contextLoaderStubBuilder(); const responses = [null, null]; diff --git a/packages/signer/context-module/src/DefaultContextModule.ts b/packages/signer/context-module/src/DefaultContextModule.ts index 11a1ba1ed..0ff284a45 100644 --- a/packages/signer/context-module/src/DefaultContextModule.ts +++ b/packages/signer/context-module/src/DefaultContextModule.ts @@ -25,6 +25,9 @@ import { type TransactionContextLoader } from "./transaction/domain/TransactionC import { type TrustedNameContextLoader } from "./trusted-name/domain/TrustedNameContextLoader"; import { typedDataTypes } from "./typed-data/di/typedDataTypes"; import type { TypedDataContextLoader } from "./typed-data/domain/TypedDataContextLoader"; +import { web3CheckTypes } from "./web3-check/di/web3CheckTypes"; +import { type Web3CheckContextLoader } from "./web3-check/domain/Web3CheckContextLoader"; +import { type Web3CheckContext } from "./web3-check/domain/web3CheckTypes"; import { type ContextModule } from "./ContextModule"; import { makeContainer } from "./di"; @@ -32,6 +35,7 @@ export class DefaultContextModule implements ContextModule { private _container: Container; private _loaders: ContextLoader[]; private _typedDataLoader: TypedDataContextLoader; + private _web3CheckLoader: Web3CheckContextLoader; constructor(args: ContextModuleConfig) { this._container = makeContainer({ config: args }); @@ -39,6 +43,8 @@ export class DefaultContextModule implements ContextModule { this._loaders.push(...args.customLoaders); this._typedDataLoader = args.customTypedDataLoader ?? this._getDefaultTypedDataLoader(); + this._web3CheckLoader = + args.customWeb3CheckLoader ?? this._getWeb3CheckLoader(); } private _getDefaultLoaders(): ContextLoader[] { @@ -63,6 +69,12 @@ export class DefaultContextModule implements ContextModule { ); } + private _getWeb3CheckLoader(): Web3CheckContextLoader { + return this._container.get( + web3CheckTypes.Web3CheckContextLoader, + ); + } + public async getContexts( transaction: TransactionContext, ): Promise { @@ -91,4 +103,22 @@ export class DefaultContextModule implements ContextModule { ): Promise { return this._typedDataLoader.load(typedData); } + + public async getWeb3Checks( + transactionContext: Web3CheckContext, + ): Promise { + const web3Checks = await this._web3CheckLoader.load(transactionContext); + + if (web3Checks.isLeft()) { + return null; + } else { + const web3ChecksValue = web3Checks.unsafeCoerce(); + // add Nano PKI fetch here should looks like => + // const web3CheckCertificate = await this._pkiCertificateLoader.fetchCertificate(...) + return { + type: ClearSignContextType.WEB3_CHECK, + payload: web3ChecksValue.descriptor, + }; + } + } } diff --git a/packages/signer/context-module/src/config/model/ContextModuleConfig.ts b/packages/signer/context-module/src/config/model/ContextModuleConfig.ts index 18213a200..422be589d 100644 --- a/packages/signer/context-module/src/config/model/ContextModuleConfig.ts +++ b/packages/signer/context-module/src/config/model/ContextModuleConfig.ts @@ -1,5 +1,6 @@ import { type ContextLoader } from "@/shared/domain/ContextLoader"; import { type TypedDataContextLoader } from "@/typed-data/domain/TypedDataContextLoader"; +import { type Web3CheckContextLoader } from "@/web3-check/domain/Web3CheckContextLoader"; export type ContextModuleCalMode = "prod" | "test"; export type ContextModuleCalBranch = "next" | "main" | "demo"; @@ -10,9 +11,15 @@ export type ContextModuleCalConfig = { branch: ContextModuleCalBranch; }; +export type ContextModuleWeb3ChecksConfig = { + url: string; +}; + export type ContextModuleConfig = { cal: ContextModuleCalConfig; + web3checks: ContextModuleWeb3ChecksConfig; defaultLoaders: boolean; customLoaders: ContextLoader[]; customTypedDataLoader?: TypedDataContextLoader; + customWeb3CheckLoader?: Web3CheckContextLoader; }; diff --git a/packages/signer/context-module/src/di.ts b/packages/signer/context-module/src/di.ts index 35d45d774..b8598ea36 100644 --- a/packages/signer/context-module/src/di.ts +++ b/packages/signer/context-module/src/di.ts @@ -9,6 +9,7 @@ import { tokenModuleFactory } from "@/token/di/tokenModuleFactory"; import { transactionModuleFactory } from "@/transaction/di/transactionModuleFactory"; import { trustedNameModuleFactory } from "@/trusted-name/di/trustedNameModuleFactory"; import { typedDataModuleFactory } from "@/typed-data/di/typedDataModuleFactory"; +import { web3CheckModuleFactory } from "@/web3-check/di/web3CheckModuleFactory"; type MakeContainerArgs = { config: ContextModuleConfig; @@ -26,6 +27,7 @@ export const makeContainer = ({ config }: MakeContainerArgs) => { trustedNameModuleFactory(), typedDataModuleFactory(), nanoPkiModuleFactory(), + web3CheckModuleFactory(), ); return container; diff --git a/packages/signer/context-module/src/index.ts b/packages/signer/context-module/src/index.ts index e747c68ab..0f39d8ced 100644 --- a/packages/signer/context-module/src/index.ts +++ b/packages/signer/context-module/src/index.ts @@ -18,3 +18,5 @@ export * from "./shared/model/TypedDataClearSignContext"; export * from "./shared/model/TypedDataContext"; export * from "./token/domain/TokenContextLoader"; export * from "./trusted-name/domain/TrustedNameContextLoader"; +export * from "./web3-check/domain/Web3CheckContextLoader"; +export * from "./web3-check/domain/web3CheckTypes"; diff --git a/packages/signer/context-module/src/shared/model/ClearSignContext.ts b/packages/signer/context-module/src/shared/model/ClearSignContext.ts index 5245430a4..4b43d25aa 100644 --- a/packages/signer/context-module/src/shared/model/ClearSignContext.ts +++ b/packages/signer/context-module/src/shared/model/ClearSignContext.ts @@ -11,6 +11,7 @@ export enum ClearSignContextType { TRANSACTION_INFO = "transactionInfo", ENUM = "enum", TRANSACTION_FIELD_DESCRIPTION = "transactionFieldDescription", + WEB3_CHECK = "web3Check", ERROR = "error", } diff --git a/packages/signer/context-module/src/shared/model/TransactionSubset.ts b/packages/signer/context-module/src/shared/model/TransactionSubset.ts index 1292a2b27..b8b6335e7 100644 --- a/packages/signer/context-module/src/shared/model/TransactionSubset.ts +++ b/packages/signer/context-module/src/shared/model/TransactionSubset.ts @@ -2,4 +2,6 @@ export type TransactionSubset = { chainId: number; to?: string; data?: string; + from?: string; + rawTx?: string; }; diff --git a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts new file mode 100644 index 000000000..b4d76a492 --- /dev/null +++ b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts @@ -0,0 +1,97 @@ +import axios from "axios"; +import { Left, Right } from "purify-ts"; + +import { type ContextModuleConfig } from "@/config/model/ContextModuleConfig"; +import { HttpWeb3CheckDataSource } from "@/web3-check/data/HttpWeb3CheckDataSource"; +import { type Web3CheckDto } from "@/web3-check/data/Web3CheckDto"; +import { type Web3CheckContext } from "@/web3-check/domain/web3CheckTypes"; + +jest.mock("axios"); + +describe("HttpWeb3CheckDataSource", () => { + const config = { + web3checks: { + url: "web3checksUrl", + }, + } as ContextModuleConfig; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe("getWeb3Checks", () => { + it("should return an object if the request is successful", async () => { + // GIVEN + const params: Web3CheckContext = { + from: "from", + rawTx: "rawTx", + chainId: 1, + }; + const dto: Web3CheckDto = { + block: 1, + public_key_id: "publicKeyId", + descriptor: "descriptor", + }; + jest.spyOn(axios, "request").mockResolvedValue({ data: dto }); + + // WHEN + const dataSource = new HttpWeb3CheckDataSource(config); + const result = await dataSource.getWeb3Checks(params); + + // THEN + expect(result).toEqual( + Right({ + publicKeyId: "publicKeyId", + descriptor: "descriptor", + }), + ); + }); + + it("should return an error if the request fails", async () => { + // GIVEN + const params: Web3CheckContext = { + from: "from", + rawTx: "rawTx", + chainId: 1, + }; + jest.spyOn(axios, "request").mockRejectedValue(new Error("error")); + + // WHEN + const dataSource = new HttpWeb3CheckDataSource(config); + const result = await dataSource.getWeb3Checks(params); + + // THEN + expect(result).toEqual( + Left( + new Error( + "[ContextModule] HttpWeb3CheckDataSource: Failed to fetch web3 checks informations", + ), + ), + ); + }); + + it("should return an error if the response is invalid", async () => { + // GIVEN + const params: Web3CheckContext = { + from: "from", + rawTx: "rawTx", + chainId: 1, + }; + const dto = {}; + jest.spyOn(axios, "request").mockResolvedValue({ data: dto }); + + // WHEN + const dataSource = new HttpWeb3CheckDataSource(config); + const result = await dataSource.getWeb3Checks(params); + + // THEN + expect(result).toEqual( + Left( + new Error( + "[ContextModule] HttpWeb3CheckDataSource: Cannot exploit Web3 checks data received", + ), + ), + ); + }); + }); +}); diff --git a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts new file mode 100644 index 000000000..29be8e187 --- /dev/null +++ b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts @@ -0,0 +1,82 @@ +import axios from "axios"; +import { inject, injectable } from "inversify"; +import { Either, Left, Right } from "purify-ts"; + +import { configTypes } from "@/config/di/configTypes"; +import type { ContextModuleConfig } from "@/config/model/ContextModuleConfig"; +import { + type Web3CheckContext, + type Web3Checks, +} from "@/web3-check/domain/web3CheckTypes"; +import PACKAGE from "@root/package.json"; + +import { Web3CheckDataSource } from "./Web3CheckDataSource"; +import { GetWeb3ChecksRequestDto, Web3CheckDto } from "./Web3CheckDto"; + +@injectable() +export class HttpWeb3CheckDataSource implements Web3CheckDataSource { + constructor( + @inject(configTypes.Config) private readonly config: ContextModuleConfig, + ) {} + + async getWeb3Checks( + params: Web3CheckContext, + ): Promise> { + let web3CheckDto: Web3CheckDto; + + try { + const requestDto: GetWeb3ChecksRequestDto = { + tx: { + from: params.from, + raw: params.rawTx, + }, + chain: params.chainId, + preset: "blockaid", + }; + const response = await axios.request({ + method: "POST", + url: `${this.config.web3checks.url}/ethereum/scan/tx`, + data: requestDto, + headers: { + "X-Ledger-Client-Version": `context-module/${PACKAGE.version}`, + }, + }); + + web3CheckDto = response.data; + } catch (_error) { + return Left( + new Error( + "[ContextModule] HttpWeb3CheckDataSource: Failed to fetch web3 checks informations", + ), + ); + } + + if (!this.isWeb3CheckDto(web3CheckDto)) { + return Left( + new Error( + "[ContextModule] HttpWeb3CheckDataSource: Cannot exploit Web3 checks data received", + ), + ); + } + + const result: Web3Checks = { + publicKeyId: web3CheckDto.public_key_id, + descriptor: web3CheckDto.descriptor, + }; + + return Right(result); + } + + private isWeb3CheckDto(dto: unknown): dto is Web3CheckDto { + return ( + dto != null && + typeof dto == "object" && + "public_key_id" in dto && + dto.public_key_id != null && + typeof dto.public_key_id == "string" && + "descriptor" in dto && + dto.descriptor != null && + typeof dto.descriptor == "string" + ); + } +} diff --git a/packages/signer/context-module/src/web3-check/data/Web3CheckDataSource.ts b/packages/signer/context-module/src/web3-check/data/Web3CheckDataSource.ts new file mode 100644 index 000000000..5960e5de1 --- /dev/null +++ b/packages/signer/context-module/src/web3-check/data/Web3CheckDataSource.ts @@ -0,0 +1,10 @@ +import { type Either } from "purify-ts"; + +import type { + Web3CheckContext, + Web3Checks, +} from "@/web3-check/domain/web3CheckTypes"; + +export interface Web3CheckDataSource { + getWeb3Checks(params: Web3CheckContext): Promise>; +} diff --git a/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts b/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts new file mode 100644 index 000000000..16e10f392 --- /dev/null +++ b/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts @@ -0,0 +1,15 @@ +export type GetWeb3ChecksRequestDto = { + tx: { + from: string; + raw: string; + }; + chain: number; + preset: string; + block?: number; +}; + +export type Web3CheckDto = { + public_key_id: string; + descriptor: string; + block: number; +}; diff --git a/packages/signer/context-module/src/web3-check/di/web3CheckModuleFactory.ts b/packages/signer/context-module/src/web3-check/di/web3CheckModuleFactory.ts new file mode 100644 index 000000000..1d952f6b5 --- /dev/null +++ b/packages/signer/context-module/src/web3-check/di/web3CheckModuleFactory.ts @@ -0,0 +1,14 @@ +import { ContainerModule } from "inversify"; + +import { HttpWeb3CheckDataSource } from "@/web3-check/data/HttpWeb3CheckDataSource"; +import { DefaultWeb3CheckContextLoader } from "@/web3-check/domain/DefaultWeb3CheckLoader"; + +import { web3CheckTypes } from "./web3CheckTypes"; + +export const web3CheckModuleFactory = () => + new ContainerModule((bind, _unbind, _isBound, _rebind) => { + bind(web3CheckTypes.Web3CheckDataSource).to(HttpWeb3CheckDataSource); + bind(web3CheckTypes.Web3CheckContextLoader).to( + DefaultWeb3CheckContextLoader, + ); + }); diff --git a/packages/signer/context-module/src/web3-check/di/web3CheckTypes.ts b/packages/signer/context-module/src/web3-check/di/web3CheckTypes.ts new file mode 100644 index 000000000..8940649ed --- /dev/null +++ b/packages/signer/context-module/src/web3-check/di/web3CheckTypes.ts @@ -0,0 +1,4 @@ +export const web3CheckTypes = { + Web3CheckDataSource: Symbol.for("Web3CheckDataSource"), + Web3CheckContextLoader: Symbol.for("Web3CheckContextLoader"), +}; diff --git a/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.test.ts b/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.test.ts new file mode 100644 index 000000000..ddff8f4b7 --- /dev/null +++ b/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.test.ts @@ -0,0 +1,81 @@ +import { Left } from "purify-ts"; + +import { DefaultWeb3CheckContextLoader } from "@/web3-check/domain/DefaultWeb3CheckLoader"; +import { type Web3CheckContext } from "@/web3-check/domain/web3CheckTypes"; + +describe("DefaultWeb3CheckLoader", () => { + describe("load", () => { + it("should return an error if the rawTx is undefined", async () => { + // GIVEN + const dataSource = { + getWeb3Checks: jest.fn(), + }; + const web3CheckContext = { + from: "from", + rawTx: undefined, + chainId: 1, + } as unknown as Web3CheckContext; + + // WHEN + const loader = new DefaultWeb3CheckContextLoader(dataSource); + const result = await loader.load(web3CheckContext); + + // THEN + expect(result).toEqual( + Left( + new Error( + "[ContextModule] Web3CheckContextLoader: cannot load web checks with undefined `rawTx` field params", + ), + ), + ); + }); + + it("should return an error if the from is undefined", async () => { + // GIVEN + const dataSource = { + getWeb3Checks: jest.fn(), + }; + const web3CheckContext = { + from: undefined, + rawTx: "rawTx", + chainId: 1, + } as unknown as Web3CheckContext; + + // WHEN + const loader = new DefaultWeb3CheckContextLoader(dataSource); + const result = await loader.load(web3CheckContext); + + // THEN + expect(result).toEqual( + Left( + new Error( + "[ContextModule] Web3CheckContextLoader: cannot load web checks with undefined `from` field params", + ), + ), + ); + }); + + it("should call the dataSource with the correct params", async () => { + // GIVEN + const dataSource = { + getWeb3Checks: jest.fn(), + }; + const web3CheckContext = { + from: "from", + rawTx: "rawTx", + chainId: 1, + } as unknown as Web3CheckContext; + + // WHEN + const loader = new DefaultWeb3CheckContextLoader(dataSource); + await loader.load(web3CheckContext); + + // THEN + expect(dataSource.getWeb3Checks).toHaveBeenCalledWith({ + from: "from", + rawTx: "rawTx", + chainId: 1, + }); + }); + }); +}); diff --git a/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.ts b/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.ts new file mode 100644 index 000000000..1832965b7 --- /dev/null +++ b/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.ts @@ -0,0 +1,49 @@ +import { inject, injectable } from "inversify"; +import { Either, Left } from "purify-ts"; + +import { type Web3CheckDataSource } from "@/web3-check/data/Web3CheckDataSource"; +import { web3CheckTypes } from "@/web3-check/di/web3CheckTypes"; + +import { Web3CheckContextLoader } from "./Web3CheckContextLoader"; +import { Web3CheckContext, Web3Checks } from "./web3CheckTypes"; + +@injectable() +export class DefaultWeb3CheckContextLoader implements Web3CheckContextLoader { + private _dataSource: Web3CheckDataSource; + + constructor( + @inject(web3CheckTypes.Web3CheckDataSource) + dataSource: Web3CheckDataSource, + ) { + this._dataSource = dataSource; + } + + async load( + web3CheckContext: Web3CheckContext, + ): Promise> { + const { chainId, rawTx, from } = web3CheckContext; + + if (rawTx == undefined || typeof rawTx != "string") { + return Left( + new Error( + "[ContextModule] Web3CheckContextLoader: cannot load web checks with undefined `rawTx` field params", + ), + ); + } + + if (from == undefined || typeof from != "string") { + return Left( + new Error( + "[ContextModule] Web3CheckContextLoader: cannot load web checks with undefined `from` field params", + ), + ); + } + + // Handle descritor payload + return await this._dataSource.getWeb3Checks({ + chainId: chainId, + rawTx: rawTx, + from: from, + }); + } +} diff --git a/packages/signer/context-module/src/web3-check/domain/Web3CheckContextLoader.ts b/packages/signer/context-module/src/web3-check/domain/Web3CheckContextLoader.ts new file mode 100644 index 000000000..f4aff719a --- /dev/null +++ b/packages/signer/context-module/src/web3-check/domain/Web3CheckContextLoader.ts @@ -0,0 +1,7 @@ +import { type Either } from "purify-ts"; + +import { type Web3CheckContext, type Web3Checks } from "./web3CheckTypes"; + +export interface Web3CheckContextLoader { + load(web3CheckContext: Web3CheckContext): Promise>; +} diff --git a/packages/signer/context-module/src/web3-check/domain/web3CheckTypes.ts b/packages/signer/context-module/src/web3-check/domain/web3CheckTypes.ts new file mode 100644 index 000000000..c85805811 --- /dev/null +++ b/packages/signer/context-module/src/web3-check/domain/web3CheckTypes.ts @@ -0,0 +1,10 @@ +export type Web3CheckContext = { + from: string; + rawTx: string; + chainId: number; +}; + +export type Web3Checks = { + publicKeyId: string; + descriptor: string; +}; From d6d6c2c24fca363a6b37c3e6f79e12bbd6b9a5ec Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 5 Feb 2025 11:59:54 +0100 Subject: [PATCH 02/12] =?UTF-8?q?=E2=9C=85=20(context-module):=20Update=20?= =?UTF-8?q?tests=20to=20vitest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../context-module/src/ContextModuleBuilder.test.ts | 2 +- .../context-module/src/DefaultContextModule.test.ts | 8 ++++---- .../web3-check/data/HttpWeb3CheckDataSource.test.ts | 10 +++++----- .../web3-check/domain/DefaultWeb3CheckLoader.test.ts | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/signer/context-module/src/ContextModuleBuilder.test.ts b/packages/signer/context-module/src/ContextModuleBuilder.test.ts index 103cfa2c2..01aaa5607 100644 --- a/packages/signer/context-module/src/ContextModuleBuilder.test.ts +++ b/packages/signer/context-module/src/ContextModuleBuilder.test.ts @@ -70,7 +70,7 @@ describe("ContextModuleBuilder", () => { it("should return a custom context module with a custom custom web3checks loader", () => { const contextModuleBuilder = new ContextModuleBuilder(); - const customLoader = { load: jest.fn() }; + const customLoader = { load: vi.fn() }; const res = contextModuleBuilder .removeDefaultLoaders() diff --git a/packages/signer/context-module/src/DefaultContextModule.test.ts b/packages/signer/context-module/src/DefaultContextModule.test.ts index ac66402ae..849f1c111 100644 --- a/packages/signer/context-module/src/DefaultContextModule.test.ts +++ b/packages/signer/context-module/src/DefaultContextModule.test.ts @@ -116,9 +116,9 @@ describe("DefaultContextModule", () => { it("should return a web3 check context", async () => { const loader = contextLoaderStubBuilder(); - jest - .spyOn(loader, "load") - .mockResolvedValueOnce(Right({ descriptor: "payload" })); + vi.spyOn(loader, "load").mockResolvedValueOnce( + Right({ descriptor: "payload" }), + ); const contextModule = new DefaultContextModule({ ...defaultContextModuleConfig, customLoaders: [], @@ -137,7 +137,7 @@ describe("DefaultContextModule", () => { it("should return null if no web3 check context", async () => { const loader = contextLoaderStubBuilder(); - jest.spyOn(loader, "load").mockResolvedValue(Left(new Error("error"))); + vi.spyOn(loader, "load").mockResolvedValue(Left(new Error("error"))); const contextModule = new DefaultContextModule({ ...defaultContextModuleConfig, customLoaders: [], diff --git a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts index b4d76a492..06c219e70 100644 --- a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts +++ b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts @@ -6,7 +6,7 @@ import { HttpWeb3CheckDataSource } from "@/web3-check/data/HttpWeb3CheckDataSour import { type Web3CheckDto } from "@/web3-check/data/Web3CheckDto"; import { type Web3CheckContext } from "@/web3-check/domain/web3CheckTypes"; -jest.mock("axios"); +vi.mock("axios"); describe("HttpWeb3CheckDataSource", () => { const config = { @@ -16,7 +16,7 @@ describe("HttpWeb3CheckDataSource", () => { } as ContextModuleConfig; beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); }); describe("getWeb3Checks", () => { @@ -32,7 +32,7 @@ describe("HttpWeb3CheckDataSource", () => { public_key_id: "publicKeyId", descriptor: "descriptor", }; - jest.spyOn(axios, "request").mockResolvedValue({ data: dto }); + vi.spyOn(axios, "request").mockResolvedValue({ data: dto }); // WHEN const dataSource = new HttpWeb3CheckDataSource(config); @@ -54,7 +54,7 @@ describe("HttpWeb3CheckDataSource", () => { rawTx: "rawTx", chainId: 1, }; - jest.spyOn(axios, "request").mockRejectedValue(new Error("error")); + vi.spyOn(axios, "request").mockRejectedValue(new Error("error")); // WHEN const dataSource = new HttpWeb3CheckDataSource(config); @@ -78,7 +78,7 @@ describe("HttpWeb3CheckDataSource", () => { chainId: 1, }; const dto = {}; - jest.spyOn(axios, "request").mockResolvedValue({ data: dto }); + vi.spyOn(axios, "request").mockResolvedValue({ data: dto }); // WHEN const dataSource = new HttpWeb3CheckDataSource(config); diff --git a/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.test.ts b/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.test.ts index ddff8f4b7..f177d3cbd 100644 --- a/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.test.ts +++ b/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.test.ts @@ -8,7 +8,7 @@ describe("DefaultWeb3CheckLoader", () => { it("should return an error if the rawTx is undefined", async () => { // GIVEN const dataSource = { - getWeb3Checks: jest.fn(), + getWeb3Checks: vi.fn(), }; const web3CheckContext = { from: "from", @@ -33,7 +33,7 @@ describe("DefaultWeb3CheckLoader", () => { it("should return an error if the from is undefined", async () => { // GIVEN const dataSource = { - getWeb3Checks: jest.fn(), + getWeb3Checks: vi.fn(), }; const web3CheckContext = { from: undefined, @@ -58,7 +58,7 @@ describe("DefaultWeb3CheckLoader", () => { it("should call the dataSource with the correct params", async () => { // GIVEN const dataSource = { - getWeb3Checks: jest.fn(), + getWeb3Checks: vi.fn(), }; const web3CheckContext = { from: "from", From 75ea40a350709583d1e107eeb26d986fcd8899ba Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 5 Feb 2025 12:04:50 +0100 Subject: [PATCH 03/12] =?UTF-8?q?=E2=9C=A8=20(signer-eth):=20Add=20web3che?= =?UTF-8?q?cks=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/moody-experts-sniff.md | 5 + .../api/app-binder/GetConfigCommandTypes.ts | 5 + .../SignTransactionDeviceActionTypes.ts | 1 + .../GetAppConfigurationCommand.test.ts | 164 ++++++++++++++++ .../command/GetAppConfigurationCommand.ts | 83 +++++++++ .../command/ProvideWeb3CheckCommand.test.ts | 99 ++++++++++ .../command/ProvideWeb3CheckCommand.ts | 54 ++++++ .../SignTransactionDeviceAction.test.ts | 77 ++++++++ .../SignTransactionDeviceAction.ts | 58 +++++- .../app-binder/task/GetWeb3CheckTask.test.ts | 175 ++++++++++++++++++ .../app-binder/task/GetWeb3CheckTask.ts | 101 ++++++++++ .../ProvideTransactionContextTask.test.ts | 1 + .../task/ProvideTransactionContextTask.ts | 24 +++ .../ProvideTransactionFieldDescriptionTask.ts | 5 + 14 files changed, 851 insertions(+), 1 deletion(-) create mode 100644 .changeset/moody-experts-sniff.md create mode 100644 packages/signer/signer-eth/src/api/app-binder/GetConfigCommandTypes.ts create mode 100644 packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts create mode 100644 packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.ts create mode 100644 packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.test.ts create mode 100644 packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.ts create mode 100644 packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts create mode 100644 packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts diff --git a/.changeset/moody-experts-sniff.md b/.changeset/moody-experts-sniff.md new file mode 100644 index 000000000..2925579b0 --- /dev/null +++ b/.changeset/moody-experts-sniff.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-signer-kit-ethereum": minor +--- + +Add web3checks support diff --git a/packages/signer/signer-eth/src/api/app-binder/GetConfigCommandTypes.ts b/packages/signer/signer-eth/src/api/app-binder/GetConfigCommandTypes.ts new file mode 100644 index 000000000..026a7a151 --- /dev/null +++ b/packages/signer/signer-eth/src/api/app-binder/GetConfigCommandTypes.ts @@ -0,0 +1,5 @@ +export type GetConfigCommandResponse = { + readonly blindSigningEnabled: boolean; + readonly web3ChecksEnabled: boolean; + readonly version: string; +}; diff --git a/packages/signer/signer-eth/src/api/app-binder/SignTransactionDeviceActionTypes.ts b/packages/signer/signer-eth/src/api/app-binder/SignTransactionDeviceActionTypes.ts index 80d21b05d..d7c3c482d 100644 --- a/packages/signer/signer-eth/src/api/app-binder/SignTransactionDeviceActionTypes.ts +++ b/packages/signer/signer-eth/src/api/app-binder/SignTransactionDeviceActionTypes.ts @@ -52,6 +52,7 @@ export type SignTransactionDAInternalState = { readonly error: SignTransactionDAError | null; readonly challenge: string | null; readonly clearSignContexts: ClearSignContextSuccess[] | GenericContext | null; + readonly web3Check: ClearSignContextSuccess | null; readonly serializedTransaction: Uint8Array | null; readonly chainId: number | null; readonly transactionType: TransactionType | null; diff --git a/packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts b/packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts new file mode 100644 index 000000000..d75b72d44 --- /dev/null +++ b/packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts @@ -0,0 +1,164 @@ +import { + InvalidStatusWordError, + isSuccessCommandResult, +} from "@ledgerhq/device-management-kit"; + +import { GetAppConfiguration } from "./GetAppConfigurationCommand"; + +describe("GetConfigCommand", () => { + let command: GetAppConfiguration; + + beforeEach(() => { + command = new GetAppConfiguration(); + }); + + describe("getApdu", () => { + it("should return the raw APDU", () => { + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + Uint8Array.from([0xe0, 0x06, 0x00, 0x00, 0x00]), + ); + }); + }); + + describe("parseResponse", () => { + it("should return the app configuration", () => { + // GIVEN + const response = { + statusCode: Uint8Array.from([0x90, 0x00]), + data: new Uint8Array([0x11, 0x01, 0x02, 0x03]), + }; + + // WHEN + const result = command.parseResponse(response); + + // THEN + if (isSuccessCommandResult(result)) { + expect(result.data).toEqual({ + blindSigningEnabled: true, + web3ChecksEnabled: true, + version: "1.2.3", + }); + } else { + fail("Expected a success"); + } + }); + + it("should return the app configuration with flags disabled", () => { + // GIVEN + const response = { + statusCode: Uint8Array.from([0x90, 0x00]), + data: new Uint8Array([0x00, 0x01, 0x02, 0x03]), + }; + + // WHEN + const result = command.parseResponse(response); + + // THEN + if (isSuccessCommandResult(result)) { + expect(result.data).toEqual({ + blindSigningEnabled: false, + web3ChecksEnabled: false, + version: "1.2.3", + }); + } else { + fail("Expected a success"); + } + }); + + it("should return an error if the device is locked", () => { + // GIVEN + const response = { + statusCode: Uint8Array.from([0x55, 0x15]), + data: new Uint8Array(), + }; + + // WHEN + const result = command.parseResponse(response); + + // THEN + if (isSuccessCommandResult(result)) { + fail("Expected an error"); + } else { + expect(result.error).toEqual( + expect.objectContaining({ + errorCode: "5515", + message: "Device is locked.", + }), + ); + } + }); + + it("should return an error if data is invalid", () => { + // GIVEN + const response = { + statusCode: Uint8Array.from([0x6a, 0x80]), + data: new Uint8Array(), + }; + + // WHEN + const result = command.parseResponse(response); + + // THEN + if (isSuccessCommandResult(result)) { + fail("Expected an error"); + } else { + expect(result.error).toEqual( + expect.objectContaining({ + errorCode: "6a80", + message: "Invalid data", + }), + ); + } + }); + + it("should return an error if no flags are extracted", () => { + // GIVEN + const response = { + statusCode: Uint8Array.from([0x90, 0x00]), + data: new Uint8Array(), + }; + + // WHEN + const result = command.parseResponse(response); + + // THEN + if (isSuccessCommandResult(result)) { + fail("Expected an error"); + } else { + expect(result.error).toBeInstanceOf(InvalidStatusWordError); + expect(result.error).toEqual( + expect.objectContaining({ + originalError: new Error("Cannot extract config flags"), + }), + ); + } + }); + + it("should return an error if no version is extracted", () => { + // GIVEN + const response = { + statusCode: Uint8Array.from([0x90, 0x00]), + data: new Uint8Array([0x01]), + }; + + // WHEN + const result = command.parseResponse(response); + + // THEN + if (isSuccessCommandResult(result)) { + fail("Expected an error"); + } else { + expect(result.error).toBeInstanceOf(InvalidStatusWordError); + expect(result.error).toEqual( + expect.objectContaining({ + originalError: new Error("Cannot extract version"), + }), + ); + } + }); + }); +}); diff --git a/packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.ts b/packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.ts new file mode 100644 index 000000000..c3de5a304 --- /dev/null +++ b/packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.ts @@ -0,0 +1,83 @@ +import { + type Apdu, + ApduBuilder, + type ApduBuilderArgs, + ApduParser, + type ApduResponse, + type Command, + type CommandResult, + CommandResultFactory, + InvalidStatusWordError, +} from "@ledgerhq/device-management-kit"; +import { CommandErrorHelper } from "@ledgerhq/signer-utils"; +import { Maybe } from "purify-ts"; + +import { type GetConfigCommandResponse as GetAppConfigurationCommandResponse } from "@api/app-binder/GetConfigCommandTypes"; + +import { + ETH_APP_ERRORS, + EthAppCommandErrorFactory, + type EthErrorCodes, +} from "./utils/ethAppErrors"; + +export class GetAppConfiguration + implements Command +{ + private readonly errorHelper = new CommandErrorHelper< + GetAppConfigurationCommandResponse, + EthErrorCodes + >(ETH_APP_ERRORS, EthAppCommandErrorFactory); + + constructor() {} + + getApdu(): Apdu { + const getEthConfigArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x06, + p1: 0x00, + p2: 0x00, + }; + const builder = new ApduBuilder(getEthConfigArgs); + return builder.build(); + } + + parseResponse( + response: ApduResponse, + ): CommandResult { + return Maybe.fromNullable( + this.errorHelper.getError(response), + ).orDefaultLazy(() => { + const parser = new ApduParser(response); + + const configFlags = parser.extract8BitUInt(); + if (configFlags === undefined) { + return CommandResultFactory({ + error: new InvalidStatusWordError("Cannot extract config flags"), + }); + } + + const major = parser.extract8BitUInt(); + const minor = parser.extract8BitUInt(); + const patch = parser.extract8BitUInt(); + + if (major === undefined || minor === undefined || patch === undefined) { + return CommandResultFactory({ + error: new InvalidStatusWordError("Cannot extract version"), + }); + } + + const blindSigningEnabled = !!(configFlags & 0x00000001); + const web3ChecksEnabled = !!(configFlags & 0x00000010); + + const data: GetAppConfigurationCommandResponse = { + blindSigningEnabled, + web3ChecksEnabled, + version: `${major}.${minor}.${patch}`, + }; + + return CommandResultFactory({ + data, + }); + }); + } +} diff --git a/packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.test.ts b/packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.test.ts new file mode 100644 index 000000000..c942b7527 --- /dev/null +++ b/packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.test.ts @@ -0,0 +1,99 @@ +import { isSuccessCommandResult } from "@ledgerhq/device-management-kit"; + +import { ProvideWeb3CheckCommand } from "@internal/app-binder/command/ProvideWeb3CheckCommand"; + +describe("ProvideWeb3CheckCommand", () => { + describe("getApdu", () => { + it("should return the raw APDU", () => { + // GIVEN + const args = { + payload: "0x010203", + certificate: true, + }; + const command = new ProvideWeb3CheckCommand(args); + + // WHEN + const apdu = command.getApdu(); + + // THEN + expect(apdu.getRawApdu()).toStrictEqual( + Uint8Array.from([0xe0, 0x32, 0x00, 0x00, 0x03, 0x01, 0x02, 0x03]), + ); + }); + }); + + describe("parseResponse", () => { + it("should return undefined", () => { + // GIVEN + const args = { + payload: "0x010203", + }; + const response = { + statusCode: Uint8Array.from([0x90, 0x00]), + data: new Uint8Array([]), + }; + + // WHEN + const result = new ProvideWeb3CheckCommand(args).parseResponse(response); + + // THEN + if (isSuccessCommandResult(result)) { + expect(result.data).toBeUndefined(); + } else { + fail("Expected a success"); + } + }); + + it("should return an error if the device is locked", () => { + // GIVEN + const args = { + payload: "0x010203", + }; + const response = { + statusCode: Uint8Array.from([0x55, 0x15]), + data: new Uint8Array(), + }; + + // WHEN + const result = new ProvideWeb3CheckCommand(args).parseResponse(response); + + // THEN + if (isSuccessCommandResult(result)) { + fail("Expected an error"); + } else { + expect(result.error).toEqual( + expect.objectContaining({ + errorCode: "5515", + message: "Device is locked.", + }), + ); + } + }); + + it("should return an error if data is invalid", () => { + // GIVEN + const args = { + payload: "0x010203", + }; + const response = { + statusCode: Uint8Array.from([0x6a, 0x80]), + data: new Uint8Array(), + }; + + // WHEN + const result = new ProvideWeb3CheckCommand(args).parseResponse(response); + + // THEN + if (isSuccessCommandResult(result)) { + fail("Expected an error"); + } else { + expect(result.error).toEqual( + expect.objectContaining({ + errorCode: "6a80", + message: "Invalid data", + }), + ); + } + }); + }); +}); diff --git a/packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.ts b/packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.ts new file mode 100644 index 000000000..bdceed436 --- /dev/null +++ b/packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.ts @@ -0,0 +1,54 @@ +import { + type Apdu, + ApduBuilder, + type ApduBuilderArgs, + type ApduResponse, + type Command, + type CommandResult, + CommandResultFactory, +} from "@ledgerhq/device-management-kit"; +import { CommandErrorHelper } from "@ledgerhq/signer-utils"; +import { Maybe } from "purify-ts"; + +import { + ETH_APP_ERRORS, + EthAppCommandErrorFactory, + type EthErrorCodes, +} from "@internal/app-binder/command/utils/ethAppErrors"; + +export type ProvideWeb3CheckCommandArgs = { + payload: string; +}; + +/** + * The command that provides a chunk of the trusted name to the device. + */ +export class ProvideWeb3CheckCommand + implements Command +{ + private readonly errorHelper = new CommandErrorHelper( + ETH_APP_ERRORS, + EthAppCommandErrorFactory, + ); + + constructor(private readonly args: ProvideWeb3CheckCommandArgs) {} + + getApdu(): Apdu { + const apduBuilderArgs: ApduBuilderArgs = { + cla: 0xe0, + ins: 0x32, + p1: 0x00, + p2: 0x00, + }; + + return new ApduBuilder(apduBuilderArgs) + .addHexaStringToData(this.args.payload) + .build(); + } + + parseResponse(response: ApduResponse): CommandResult { + return Maybe.fromNullable(this.errorHelper.getError(response)).orDefault( + CommandResultFactory({ data: undefined }), + ); + } +} diff --git a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts index 22f42e321..a122139ce 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts @@ -1,5 +1,9 @@ /* eslint @typescript-eslint/consistent-type-imports: 0 */ import { type ContextModule } from "@ledgerhq/context-module"; +import { + ClearSignContextType, + type ContextModule, +} from "@ledgerhq/context-module"; import { CommandResultFactory, DeviceActionStatus, @@ -57,6 +61,7 @@ describe("SignTransactionDeviceAction", () => { provideContext: provideContextMock, provideGenericContext: provideGenericContextMock, signTransaction: signTransactionMock, + getWeb3Check: getWeb3CheckMock, }; } const defaultOptions = { @@ -1047,6 +1052,78 @@ describe("SignTransactionDeviceAction", () => { })); }); + describe("GetWeb3Checks errors", () => { + it("should fail if GetWeb3Checks throws an error", (done) => { + setupOpenAppDAMock(); + + const deviceAction = new SignTransactionDeviceAction({ + input: { + derivationPath: "44'/60'/0'/0/0", + transaction: defaultTransaction, + options: defaultOptions, + contextModule: contextModuleMock, + mapper: mapperMock, + parser: parserMock, + }, + }); + + getChallengeMock.mockResolvedValueOnce( + CommandResultFactory({ + data: { challenge: "challenge" }, + }), + ); + getWeb3CheckMock.mockRejectedValueOnce( + new InvalidStatusWordError("getWeb3Checks error"), + ); + jest + .spyOn(deviceAction, "extractDependencies") + .mockReturnValue(extractDependenciesMock()); + + const expectedStates: Array = [ + // Initial state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // OpenApp interaction + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.ConfirmOpenApp, + }, + status: DeviceActionStatus.Pending, + }, + // GetChallenge state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // GetWeb3Checks state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // GetWeb3Checks error + { + error: new InvalidStatusWordError("getWeb3Checks error"), + status: DeviceActionStatus.Error, + }, + ]; + + testDeviceActionStates( + deviceAction, + expectedStates, + makeDeviceActionInternalApiMock(), + done, + ); + }); + }); + describe("BuildContext errors", () => { it("should fail if buildContext throws an error", () => new Promise((resolve, reject) => { diff --git a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.ts b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.ts index 69d5e673d..3c253e83f 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.ts @@ -38,6 +38,11 @@ import { type BuildTransactionContextTaskArgs, type BuildTransactionTaskResult, } from "@internal/app-binder/task/BuildTransactionContextTask"; +import { + GetWeb3CheckTask, + type GetWeb3CheckTaskArgs, + type GetWeb3CheckTaskResult, +} from "@internal/app-binder/task/GetWeb3CheckTask"; import { ProvideTransactionContextTask } from "@internal/app-binder/task/ProvideTransactionContextTask"; import { type GenericContext, @@ -61,9 +66,19 @@ export type MachineDependencies = { challenge: string | null; }; }) => Promise; + readonly getWeb3Check: (arg0: { + input: { + contextModule: ContextModule; + mapper: TransactionMapperService; + transaction: Uint8Array; + options: TransactionOptions; + derivationPath: string; + }; + }) => Promise; readonly provideContext: (arg0: { input: { clearSignContexts: ClearSignContextSuccess[]; + web3Check: ClearSignContextSuccess | null; }; }) => Promise>>; readonly provideGenericContext: (arg0: { @@ -115,6 +130,7 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< const { getChallenge, + getWeb3Check, buildContext, provideContext, provideGenericContext, @@ -132,6 +148,7 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< input: { appName: "Ethereum" }, }).makeStateMachine(internalApi), getChallenge: fromPromise(getChallenge), + getWeb3Check: fromPromise(getWeb3Check), buildContext: fromPromise(buildContext), provideContext: fromPromise(provideContext), provideGenericContext: fromPromise(provideGenericContext), @@ -167,6 +184,7 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< clearSignContexts: null, serializedTransaction: null, chainId: null, + web3Check: null, transactionType: null, challenge: null, isLegacy: true, @@ -262,12 +280,42 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< GetChallengeResultCheck: { always: [ { - target: "BuildContext", + target: "GetWeb3Check", guard: "noInternalError", }, "Error", ], }, + GetWeb3Check: { + invoke: { + id: "getWeb3Check", + src: "getWeb3Check", + input: ({ context }) => ({ + contextModule: context.input.contextModule, + mapper: context.input.mapper, + transaction: context.input.transaction, + options: context.input.options, + derivationPath: context.input.derivationPath, + }), + onDone: { + target: "BuildContext", + actions: [ + assign({ + _internalState: ({ event, context }) => { + return { + ...context._internalState, + web3Check: event.output.web3Check, + }; + }, + }), + ], + }, + onError: { + target: "Error", + actions: "assignErrorFromEvent", + }, + }, + }, BuildContext: { invoke: { id: "buildContext", @@ -317,6 +365,7 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< input: ({ context }) => ({ clearSignContexts: context._internalState .clearSignContexts as ClearSignContextSuccess[], + web3Check: context._internalState.web3Check, }), onDone: { target: "SignTransaction", @@ -439,6 +488,10 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< extractDependencies(internalApi: InternalApi): MachineDependencies { const getChallenge = async () => internalApi.sendCommand(new GetChallengeCommand()); + + const getWeb3Check = async (arg0: { input: GetWeb3CheckTaskArgs }) => + new GetWeb3CheckTask(internalApi, arg0.input).run(); + const buildContext = async (arg0: { input: BuildTransactionContextTaskArgs; }) => new BuildTransactionContextTask(internalApi, arg0.input).run(); @@ -446,10 +499,12 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< const provideContext = async (arg0: { input: { clearSignContexts: ClearSignContextSuccess[]; + web3Check: ClearSignContextSuccess | null; }; }) => new ProvideTransactionContextTask(internalApi, { clearSignContexts: arg0.input.clearSignContexts, + web3Check: arg0.input.web3Check, }).run(); const provideGenericContext = async (arg0: { @@ -484,6 +539,7 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< return { getChallenge, buildContext, + getWeb3Check, provideContext, provideGenericContext, signTransaction, diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts new file mode 100644 index 000000000..93b5882ac --- /dev/null +++ b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts @@ -0,0 +1,175 @@ +import { type ContextModule } from "@ledgerhq/context-module"; +import { + CommandResultFactory, + InvalidStatusWordError, +} from "@ledgerhq/device-management-kit"; +import { Left, Right } from "purify-ts"; + +import { makeDeviceActionInternalApiMock } from "@internal/app-binder/device-action/__test-utils__/makeInternalApi"; +import { GetWeb3CheckTask } from "@internal/app-binder/task/GetWeb3CheckTask"; +import { type TransactionMapperService } from "@internal/transaction/service/mapper/TransactionMapperService"; + +describe("GetWeb3CheckTask", () => { + const apiMock = makeDeviceActionInternalApiMock(); + const contextModuleMock = { + getWeb3Checks: jest.fn(), + }; + const mapperMock = { + mapTransactionToSubset: jest.fn(), + }; + const transaction = new Uint8Array([0x01, 0x02, 0x03, 0x04]); + const derivationPath = "44'/60'/0'/0/0"; + + describe("run", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("errors", () => { + it("should throw an error if mapTransactionToSubset fails", async () => { + // GIVEN + const error = new Error("error"); + mapperMock.mapTransactionToSubset.mockReturnValue(Left(error)); + + // WHEN + try { + await new GetWeb3CheckTask(apiMock, { + contextModule: contextModuleMock as unknown as ContextModule, + mapper: mapperMock as unknown as TransactionMapperService, + transaction, + derivationPath, + }).run(); + fail("should throw an error"); + } catch (e) { + // THEN + expect(e).toEqual(error); + } + }); + + it("should return a context error if GetAppConfiguration fails", async () => { + // GIVEN + mapperMock.mapTransactionToSubset.mockReturnValue( + Right({ subset: {}, serializedTransaction: new Uint8Array() }), + ); + apiMock.sendCommand.mockResolvedValue( + CommandResultFactory({ error: new InvalidStatusWordError("error") }), + ); + + // WHEN + const result = await new GetWeb3CheckTask(apiMock, { + contextModule: contextModuleMock as unknown as ContextModule, + mapper: mapperMock as unknown as TransactionMapperService, + transaction, + derivationPath, + }).run(); + + // THEN + expect(result).toEqual({ + web3Check: null, + error: new InvalidStatusWordError("error"), + }); + }); + + it("should return a context error if GetAddressCommand fails", async () => { + // GIVEN + mapperMock.mapTransactionToSubset.mockReturnValue( + Right({ subset: {}, serializedTransaction: new Uint8Array() }), + ); + apiMock.sendCommand.mockResolvedValueOnce( + CommandResultFactory({ data: { web3ChecksEnabled: true } }), + ); + apiMock.sendCommand.mockResolvedValueOnce( + CommandResultFactory({ error: new InvalidStatusWordError("error") }), + ); + + // WHEN + const result = await new GetWeb3CheckTask(apiMock, { + contextModule: contextModuleMock as unknown as ContextModule, + mapper: mapperMock as unknown as TransactionMapperService, + transaction, + derivationPath, + }).run(); + + // THEN + expect(result).toEqual({ + web3Check: null, + error: new InvalidStatusWordError("error"), + }); + }); + }); + + describe("success", () => { + it("should return null if web3ChecksEnabled is false", async () => { + // GIVEN + mapperMock.mapTransactionToSubset.mockReturnValue( + Right({ subset: {}, serializedTransaction: new Uint8Array() }), + ); + apiMock.sendCommand.mockResolvedValue( + CommandResultFactory({ data: { web3ChecksEnabled: false } }), + ); + + // WHEN + const result = await new GetWeb3CheckTask(apiMock, { + contextModule: contextModuleMock as unknown as ContextModule, + mapper: mapperMock as unknown as TransactionMapperService, + transaction, + derivationPath, + }).run(); + + // THEN + expect(result).toEqual({ + web3Check: null, + }); + }); + + it("should return null if the context module does not have a web3 check", async () => { + // GIVEN + mapperMock.mapTransactionToSubset.mockReturnValue( + Right({ subset: {}, serializedTransaction: new Uint8Array() }), + ); + apiMock.sendCommand.mockResolvedValue( + CommandResultFactory({ data: { web3ChecksEnabled: true } }), + ); + contextModuleMock.getWeb3Checks.mockResolvedValue(null); + + // WHEN + const result = await new GetWeb3CheckTask(apiMock, { + contextModule: contextModuleMock as unknown as ContextModule, + mapper: mapperMock as unknown as TransactionMapperService, + transaction, + derivationPath, + }).run(); + + // THEN + expect(result).toEqual({ + web3Check: null, + }); + }); + + it("should return a web3 check", async () => { + // GIVEN + const web3Check = { type: "web3Check", id: 1 }; + mapperMock.mapTransactionToSubset.mockReturnValue( + Right({ subset: {}, serializedTransaction: new Uint8Array() }), + ); + apiMock.sendCommand.mockResolvedValue( + CommandResultFactory({ data: { web3ChecksEnabled: true } }), + ); + contextModuleMock.getWeb3Checks.mockResolvedValue(web3Check); + + // WHEN + const result = await new GetWeb3CheckTask(apiMock, { + contextModule: contextModuleMock as unknown as ContextModule, + mapper: mapperMock as unknown as TransactionMapperService, + transaction, + derivationPath, + }).run(); + + // THEN + expect(result).toEqual({ + web3Check, + }); + }); + }); + }); +}); diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts new file mode 100644 index 000000000..1db3a0f28 --- /dev/null +++ b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts @@ -0,0 +1,101 @@ +import { + type ClearSignContext, + type ClearSignContextSuccess, + ClearSignContextType, + type ContextModule, + type Web3CheckContext, +} from "@ledgerhq/context-module"; +import { + bufferToHexaString, + type DmkError, + type InternalApi, + isSuccessCommandResult, +} from "@ledgerhq/device-management-kit"; + +import { GetAddressCommand } from "@internal/app-binder/command/GetAddressCommand"; +import { GetAppConfiguration } from "@internal/app-binder/command/GetAppConfigurationCommand"; +import { type TransactionMapperService } from "@internal/transaction/service/mapper/TransactionMapperService"; + +export type GetWeb3CheckTaskResult = + | { + readonly web3Check: ClearSignContextSuccess | null; + } + | { + readonly web3Check: null; + error: DmkError; + }; + +export type GetWeb3CheckTaskArgs = { + readonly contextModule: ContextModule; + readonly mapper: TransactionMapperService; + readonly transaction: Uint8Array; + readonly derivationPath: string; +}; + +export class GetWeb3CheckTask { + constructor( + private readonly api: InternalApi, + private readonly args: GetWeb3CheckTaskArgs, + ) {} + + async run(): Promise { + const { contextModule, mapper, transaction } = this.args; + const parsed = mapper.mapTransactionToSubset(transaction); + parsed.ifLeft((err) => { + throw err; + }); + const { subset, serializedTransaction } = parsed.unsafeCoerce(); + + const configResult = await this.api.sendCommand(new GetAppConfiguration()); + //check error + if (!isSuccessCommandResult(configResult)) { + return { + web3Check: null, + error: configResult.error, + }; + } + + //Only do Web3 Check if it is activated + if (!configResult.data.web3ChecksEnabled) { + return { + web3Check: null, + }; + } + + const getAddressResult = await this.api.sendCommand( + new GetAddressCommand({ + derivationPath: this.args.derivationPath, + checkOnDevice: false, + returnChainCode: false, + }), + ); + if (!isSuccessCommandResult(getAddressResult)) { + return { + web3Check: null, + error: getAddressResult.error, + }; + } + + const address = getAddressResult.data.address; + const web3Params: Web3CheckContext = { + from: address, + rawTx: bufferToHexaString(serializedTransaction), + chainId: subset.chainId, + }; + const web3CheckContext: ClearSignContext | null = + await contextModule.getWeb3Checks(web3Params); + + if ( + web3CheckContext === null || + web3CheckContext?.type === ClearSignContextType.ERROR + ) { + return { + web3Check: null, + }; + } + + return { + web3Check: web3CheckContext, + }; + } +} diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.test.ts b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.test.ts index c5c6a50e8..2d9e90e0b 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.test.ts @@ -50,6 +50,7 @@ describe("ProvideTransactionContextTask", () => { payload: "746f6b656e", // "token" }, ], + web3Check: null, }; afterEach(() => { vi.restoreAllMocks(); diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.ts b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.ts index 36e67c59f..9f8874bee 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.ts @@ -9,6 +9,7 @@ import { type InternalApi, InvalidStatusWordError, isSuccessCommandResult, + LoadCertificateCommand, } from "@ledgerhq/device-management-kit"; import { Just, type Maybe, Nothing } from "purify-ts"; @@ -18,6 +19,7 @@ import { type ProvideTokenInformationCommandResponse, } from "@internal/app-binder/command/ProvideTokenInformationCommand"; import { ProvideTrustedNameCommand } from "@internal/app-binder/command/ProvideTrustedNameCommand"; +import { ProvideWeb3CheckCommand } from "@internal/app-binder/command/ProvideWeb3CheckCommand"; import { SetExternalPluginCommand } from "@internal/app-binder/command/SetExternalPluginCommand"; import { SetPluginCommand } from "@internal/app-binder/command/SetPluginCommand"; import { type EthErrorCodes } from "@internal/app-binder/command/utils/ethAppErrors"; @@ -29,6 +31,7 @@ export type ProvideTransactionContextTaskArgs = { * The valid clear sign contexts offerred by the `BuildTrancationContextTask`. */ clearSignContexts: ClearSignContextSuccess[]; + web3Check: ClearSignContextSuccess | null; }; /** @@ -55,6 +58,12 @@ export class ProvideTransactionContextTask { return Just(res); } } + if (this.args.web3Check) { + const res = await this.provideContext(this.args.web3Check); + if (!isSuccessCommandResult(res)) { + return Just(res); + } + } return Nothing; } @@ -68,9 +77,20 @@ export class ProvideTransactionContextTask { async provideContext({ type, payload, + certificate, }: ClearSignContextSuccess): Promise< CommandResult > { + // if a certificate is provided, we load it before sending the command + if (certificate) { + await this.api.sendCommand( + new LoadCertificateCommand({ + keyUsage: certificate.keyUsageNumber, + certificate: certificate.payload, + }), + ); + } + switch (type) { case ClearSignContextType.PLUGIN: { return await this.api.sendCommand(new SetPluginCommand({ payload })); @@ -109,6 +129,10 @@ export class ProvideTransactionContextTask { ), }); } + case ClearSignContextType.WEB3_CHECK: + return await this.api.sendCommand( + new ProvideWeb3CheckCommand({ payload }), + ); default: { const uncoveredType: never = type; return CommandResultFactory({ diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionFieldDescriptionTask.ts b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionFieldDescriptionTask.ts index 8b74f22df..dd1906109 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionFieldDescriptionTask.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionFieldDescriptionTask.ts @@ -29,6 +29,7 @@ import { } from "@internal/app-binder/command/ProvideTokenInformationCommand"; import { ProvideTransactionFieldDescriptionCommand } from "@internal/app-binder/command/ProvideTransactionFieldDescriptionCommand"; import { ProvideTrustedNameCommand } from "@internal/app-binder/command/ProvideTrustedNameCommand"; +import { ProvideWeb3CheckCommand } from "@internal/app-binder/command/ProvideWeb3CheckCommand"; import { type EthErrorCodes } from "@internal/app-binder/command/utils/ethAppErrors"; import { type TransactionParserService } from "@internal/transaction/service/parser/TransactionParserService"; @@ -329,6 +330,10 @@ export class ProvideTransactionFieldDescriptionTask { `The context type [${type}] is not valid as a transaction field or metadata`, ), }); + case ClearSignContextType.WEB3_CHECK: + return await this.api.sendCommand( + new ProvideWeb3CheckCommand({ payload }), + ); default: { const uncoveredType: never = type; return CommandResultFactory({ From c19ca9ae0f21abcb6e1e90864a6086bdbbba5971 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 5 Feb 2025 12:26:09 +0100 Subject: [PATCH 04/12] =?UTF-8?q?=E2=9C=85=20(signer-eth):=20Update=20test?= =?UTF-8?q?s=20to=20vitest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetAppConfigurationCommand.test.ts | 12 +- .../command/ProvideWeb3CheckCommand.test.ts | 6 +- .../SignTransactionDeviceAction.test.ts | 568 +++++++++++++++--- .../SignTypedDataDeviceAction.test.ts | 1 + .../task/BuildEIP712ContextTask.test.ts | 1 + .../task/BuildTransactionContextTask.test.ts | 1 + .../app-binder/task/GetWeb3CheckTask.test.ts | 14 +- 7 files changed, 515 insertions(+), 88 deletions(-) diff --git a/packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts b/packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts index d75b72d44..69863ae02 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/command/GetAppConfigurationCommand.test.ts @@ -43,7 +43,7 @@ describe("GetConfigCommand", () => { version: "1.2.3", }); } else { - fail("Expected a success"); + assert.fail("Expected a success"); } }); @@ -65,7 +65,7 @@ describe("GetConfigCommand", () => { version: "1.2.3", }); } else { - fail("Expected a success"); + assert.fail("Expected a success"); } }); @@ -81,7 +81,7 @@ describe("GetConfigCommand", () => { // THEN if (isSuccessCommandResult(result)) { - fail("Expected an error"); + assert.fail("Expected an error"); } else { expect(result.error).toEqual( expect.objectContaining({ @@ -104,7 +104,7 @@ describe("GetConfigCommand", () => { // THEN if (isSuccessCommandResult(result)) { - fail("Expected an error"); + assert.fail("Expected an error"); } else { expect(result.error).toEqual( expect.objectContaining({ @@ -127,7 +127,7 @@ describe("GetConfigCommand", () => { // THEN if (isSuccessCommandResult(result)) { - fail("Expected an error"); + assert.fail("Expected an error"); } else { expect(result.error).toBeInstanceOf(InvalidStatusWordError); expect(result.error).toEqual( @@ -150,7 +150,7 @@ describe("GetConfigCommand", () => { // THEN if (isSuccessCommandResult(result)) { - fail("Expected an error"); + assert.fail("Expected an error"); } else { expect(result.error).toBeInstanceOf(InvalidStatusWordError); expect(result.error).toEqual( diff --git a/packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.test.ts b/packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.test.ts index c942b7527..2f86cb10f 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/command/ProvideWeb3CheckCommand.test.ts @@ -40,7 +40,7 @@ describe("ProvideWeb3CheckCommand", () => { if (isSuccessCommandResult(result)) { expect(result.data).toBeUndefined(); } else { - fail("Expected a success"); + assert.fail("Expected a success"); } }); @@ -59,7 +59,7 @@ describe("ProvideWeb3CheckCommand", () => { // THEN if (isSuccessCommandResult(result)) { - fail("Expected an error"); + assert.fail("Expected an error"); } else { expect(result.error).toEqual( expect.objectContaining({ @@ -85,7 +85,7 @@ describe("ProvideWeb3CheckCommand", () => { // THEN if (isSuccessCommandResult(result)) { - fail("Expected an error"); + assert.fail("Expected an error"); } else { expect(result.error).toEqual( expect.objectContaining({ diff --git a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts index a122139ce..c00f89796 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts @@ -1,5 +1,4 @@ /* eslint @typescript-eslint/consistent-type-imports: 0 */ -import { type ContextModule } from "@ledgerhq/context-module"; import { ClearSignContextType, type ContextModule, @@ -42,6 +41,7 @@ describe("SignTransactionDeviceAction", () => { getContext: vi.fn(), getContexts: vi.fn(), getTypedDataFilters: vi.fn(), + getWeb3Checks: vi.fn(), }; const mapperMock: TransactionMapperService = { mapTransactionToSubset: vi.fn(), @@ -54,6 +54,7 @@ describe("SignTransactionDeviceAction", () => { const provideContextMock = vi.fn(); const provideGenericContextMock = vi.fn(); const signTransactionMock = vi.fn(); + const getWeb3CheckMock = vi.fn(); function extractDependenciesMock() { return { getChallenge: getChallengeMock, @@ -101,6 +102,9 @@ describe("SignTransactionDeviceAction", () => { data: { challenge: "challenge" }, }), ); + getWeb3CheckMock.mockResolvedValueOnce({ + web3Check: null, + }); buildContextMock.mockResolvedValueOnce({ clearSignContexts: [ { @@ -150,6 +154,13 @@ describe("SignTransactionDeviceAction", () => { }, status: DeviceActionStatus.Pending, }, + // GetWeb3Check state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, // BuildContext state { intermediateValue: { @@ -211,6 +222,7 @@ describe("SignTransactionDeviceAction", () => { payload: "payload-1", }, ], + web3Check: null, }, }), ); @@ -254,6 +266,9 @@ describe("SignTransactionDeviceAction", () => { data: { challenge: "challenge" }, }), ); + getWeb3CheckMock.mockResolvedValueOnce({ + web3Check: null, + }); buildContextMock.mockResolvedValueOnce({ clearSignContexts: { transactionInfo: "payload-1", @@ -306,6 +321,13 @@ describe("SignTransactionDeviceAction", () => { }, status: DeviceActionStatus.Pending, }, + // GetWeb3Checks state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, // BuildContext state { intermediateValue: { @@ -417,6 +439,9 @@ describe("SignTransactionDeviceAction", () => { data: { challenge: "challenge" }, }), ); + getWeb3CheckMock.mockResolvedValueOnce({ + web3Check: null, + }); buildContextMock.mockResolvedValueOnce({ clearSignContexts: [ { @@ -472,6 +497,13 @@ describe("SignTransactionDeviceAction", () => { }, status: DeviceActionStatus.Pending, }, + // GetWeb3Checks state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, // BuildContext state { intermediateValue: { @@ -533,6 +565,7 @@ describe("SignTransactionDeviceAction", () => { payload: "payload-1", }, ], + web3Check: null, }, }), ); @@ -574,7 +607,9 @@ describe("SignTransactionDeviceAction", () => { data: { challenge: "challenge" }, }), ); - + getWeb3CheckMock.mockResolvedValueOnce({ + web3Check: null, + }); buildContextMock.mockResolvedValueOnce({ clearSignContexts: { transactionInfo: "payload-1", @@ -636,6 +671,13 @@ describe("SignTransactionDeviceAction", () => { }, status: DeviceActionStatus.Pending, }, + // GetWeb3Checks state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, // BuildContext state { intermediateValue: { @@ -750,7 +792,9 @@ describe("SignTransactionDeviceAction", () => { }), }), ); - + getWeb3CheckMock.mockResolvedValueOnce({ + web3Check: null, + }); buildContextMock.mockResolvedValueOnce({ clearSignContexts: [], serializedTransaction: new Uint8Array([0x01, 0x02, 0x03]), @@ -798,6 +842,13 @@ describe("SignTransactionDeviceAction", () => { }, status: DeviceActionStatus.Pending, }, + // GetWeb3Checks state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, // BuildContext state { intermediateValue: { @@ -866,6 +917,343 @@ describe("SignTransactionDeviceAction", () => { }, ); })); + + it("should provide web3checks context if getWeb3Check return a value", () => + new Promise((resolve, reject) => { + setupOpenAppDAMock(); + + const deviceAction = new SignTransactionDeviceAction({ + input: { + derivationPath: "44'/60'/0'/0/0", + transaction: defaultTransaction, + options: defaultOptions, + contextModule: contextModuleMock, + mapper: mapperMock, + parser: parserMock, + }, + }); + + // Mock the dependencies to return some sample data + getChallengeMock.mockResolvedValueOnce( + CommandResultFactory({ + data: { challenge: "challenge" }, + }), + ); + getWeb3CheckMock.mockResolvedValueOnce({ + web3Check: { + type: ClearSignContextType.ENUM, + id: 1, + payload: "0x01020304", + value: 1, + certificate: undefined, + }, + }); + buildContextMock.mockResolvedValueOnce({ + clearSignContexts: [ + { + type: "token", + payload: "payload-1", + }, + ], + serializedTransaction: new Uint8Array([0x01, 0x02, 0x03]), + chainId: 1, + transactionType: TransactionType.LEGACY, + }); + provideContextMock.mockResolvedValueOnce(Nothing); + signTransactionMock.mockResolvedValueOnce( + CommandResultFactory({ + data: { + v: 0x1c, + r: "0x8a540510e13b0f2b11a451275716d29e08caad07e89a1c84964782fb5e1ad788", + s: "0x64a0de235b270fbe81e8e40688f4a9f9ad9d283d690552c9331d7773ceafa513", + }, + }), + ); + vi.spyOn(deviceAction, "extractDependencies").mockReturnValue( + extractDependenciesMock(), + ); + + // Expected intermediate values for the following state sequence: + // Initial -> OpenApp -> GetChallenge -> BuildContext -> ProvideContext -> SignTransaction + const expectedStates: Array = [ + // Initial state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // OpenApp interaction + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.ConfirmOpenApp, + }, + status: DeviceActionStatus.Pending, + }, + // GetChallenge state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // GetWeb3Checks state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // BuildContext state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // ProvideContext state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // SignTransaction state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.SignTransaction, + }, + status: DeviceActionStatus.Pending, + }, + // Final state + { + output: { + v: 0x1c, + r: "0x8a540510e13b0f2b11a451275716d29e08caad07e89a1c84964782fb5e1ad788", + s: "0x64a0de235b270fbe81e8e40688f4a9f9ad9d283d690552c9331d7773ceafa513", + }, + status: DeviceActionStatus.Completed, + }, + ]; + + testDeviceActionStates( + deviceAction, + expectedStates, + makeDeviceActionInternalApiMock(), + { + onError: reject, + onDone: () => { + // Verify mocks calls parameters + expect(getChallengeMock).toHaveBeenCalled(); + expect(buildContextMock).toHaveBeenCalledWith( + expect.objectContaining({ + input: { + challenge: "challenge", + contextModule: contextModuleMock, + mapper: mapperMock, + options: defaultOptions, + transaction: defaultTransaction, + }, + }), + ); + expect(provideContextMock).toHaveBeenCalledWith( + expect.objectContaining({ + input: { + clearSignContexts: [ + { + type: "token", + payload: "payload-1", + }, + ], + web3Check: { + type: ClearSignContextType.ENUM, + id: 1, + payload: "0x01020304", + value: 1, + certificate: undefined, + }, + }, + }), + ); + expect(signTransactionMock).toHaveBeenCalledWith( + expect.objectContaining({ + input: { + derivationPath: "44'/60'/0'/0/0", + serializedTransaction: new Uint8Array([0x01, 0x02, 0x03]), + isLegacy: true, + chainId: 1, + transactionType: TransactionType.LEGACY, + }, + }), + ); + resolve(); + }, + }, + ); + })); + + it("should ignore web3checks errors", () => + new Promise((resolve, reject) => { + setupOpenAppDAMock(); + + const deviceAction = new SignTransactionDeviceAction({ + input: { + derivationPath: "44'/60'/0'/0/0", + transaction: defaultTransaction, + options: defaultOptions, + contextModule: contextModuleMock, + mapper: mapperMock, + parser: parserMock, + }, + }); + + // Mock the dependencies to return some sample data + getChallengeMock.mockResolvedValueOnce( + CommandResultFactory({ + data: { challenge: "challenge" }, + }), + ); + getWeb3CheckMock.mockResolvedValueOnce({ + web3Check: null, + error: new InvalidStatusWordError("getWeb3Check error"), + }); + buildContextMock.mockResolvedValueOnce({ + clearSignContexts: [ + { + type: "token", + payload: "payload-1", + }, + ], + serializedTransaction: new Uint8Array([0x01, 0x02, 0x03]), + chainId: 1, + transactionType: TransactionType.LEGACY, + }); + provideContextMock.mockResolvedValueOnce(Nothing); + signTransactionMock.mockResolvedValueOnce( + CommandResultFactory({ + data: { + v: 0x1c, + r: "0x8a540510e13b0f2b11a451275716d29e08caad07e89a1c84964782fb5e1ad788", + s: "0x64a0de235b270fbe81e8e40688f4a9f9ad9d283d690552c9331d7773ceafa513", + }, + }), + ); + vi.spyOn(deviceAction, "extractDependencies").mockReturnValue( + extractDependenciesMock(), + ); + + // Expected intermediate values for the following state sequence: + // Initial -> OpenApp -> GetChallenge -> BuildContext -> ProvideContext -> SignTransaction + const expectedStates: Array = [ + // Initial state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // OpenApp interaction + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.ConfirmOpenApp, + }, + status: DeviceActionStatus.Pending, + }, + // GetChallenge state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // GetWeb3Checks state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // BuildContext state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // ProvideContext state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // SignTransaction state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.SignTransaction, + }, + status: DeviceActionStatus.Pending, + }, + // Final state + { + output: { + v: 0x1c, + r: "0x8a540510e13b0f2b11a451275716d29e08caad07e89a1c84964782fb5e1ad788", + s: "0x64a0de235b270fbe81e8e40688f4a9f9ad9d283d690552c9331d7773ceafa513", + }, + status: DeviceActionStatus.Completed, + }, + ]; + + testDeviceActionStates( + deviceAction, + expectedStates, + makeDeviceActionInternalApiMock(), + { + onError: reject, + onDone: () => { + // Verify mocks calls parameters + expect(getChallengeMock).toHaveBeenCalled(); + expect(buildContextMock).toHaveBeenCalledWith( + expect.objectContaining({ + input: { + challenge: "challenge", + contextModule: contextModuleMock, + mapper: mapperMock, + options: defaultOptions, + transaction: defaultTransaction, + }, + }), + ); + expect(provideContextMock).toHaveBeenCalledWith( + expect.objectContaining({ + input: { + clearSignContexts: [ + { + type: "token", + payload: "payload-1", + }, + ], + web3Check: null, + }, + }), + ); + expect(signTransactionMock).toHaveBeenCalledWith( + expect.objectContaining({ + input: { + derivationPath: "44'/60'/0'/0/0", + serializedTransaction: new Uint8Array([0x01, 0x02, 0x03]), + isLegacy: true, + chainId: 1, + transactionType: TransactionType.LEGACY, + }, + }), + ); + resolve(); + }, + }, + ); + })); }); describe("OpenApp errors", () => { @@ -1053,75 +1441,81 @@ describe("SignTransactionDeviceAction", () => { }); describe("GetWeb3Checks errors", () => { - it("should fail if GetWeb3Checks throws an error", (done) => { - setupOpenAppDAMock(); - - const deviceAction = new SignTransactionDeviceAction({ - input: { - derivationPath: "44'/60'/0'/0/0", - transaction: defaultTransaction, - options: defaultOptions, - contextModule: contextModuleMock, - mapper: mapperMock, - parser: parserMock, - }, - }); - - getChallengeMock.mockResolvedValueOnce( - CommandResultFactory({ - data: { challenge: "challenge" }, - }), - ); - getWeb3CheckMock.mockRejectedValueOnce( - new InvalidStatusWordError("getWeb3Checks error"), - ); - jest - .spyOn(deviceAction, "extractDependencies") - .mockReturnValue(extractDependenciesMock()); - - const expectedStates: Array = [ - // Initial state - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - }, - status: DeviceActionStatus.Pending, - }, - // OpenApp interaction - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.ConfirmOpenApp, - }, - status: DeviceActionStatus.Pending, - }, - // GetChallenge state - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - }, - status: DeviceActionStatus.Pending, - }, - // GetWeb3Checks state - { - intermediateValue: { - requiredUserInteraction: UserInteractionRequired.None, - }, - status: DeviceActionStatus.Pending, - }, - // GetWeb3Checks error - { - error: new InvalidStatusWordError("getWeb3Checks error"), - status: DeviceActionStatus.Error, - }, - ]; - - testDeviceActionStates( - deviceAction, - expectedStates, - makeDeviceActionInternalApiMock(), - done, - ); - }); + it("should fail if GetWeb3Checks throws an error", () => + new Promise((resolve, reject) => { + setupOpenAppDAMock(); + + const deviceAction = new SignTransactionDeviceAction({ + input: { + derivationPath: "44'/60'/0'/0/0", + transaction: defaultTransaction, + options: defaultOptions, + contextModule: contextModuleMock, + mapper: mapperMock, + parser: parserMock, + }, + }); + + getChallengeMock.mockResolvedValueOnce( + CommandResultFactory({ + data: { challenge: "challenge" }, + }), + ); + getWeb3CheckMock.mockRejectedValueOnce( + new InvalidStatusWordError("getWeb3Checks error"), + ); + vi.spyOn(deviceAction, "extractDependencies").mockReturnValue( + extractDependenciesMock(), + ); + + const expectedStates: Array = [ + // Initial state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // OpenApp interaction + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.ConfirmOpenApp, + }, + status: DeviceActionStatus.Pending, + }, + // GetChallenge state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // GetWeb3Checks state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, + // GetWeb3Checks error + { + error: new InvalidStatusWordError("getWeb3Checks error"), + status: DeviceActionStatus.Error, + }, + ]; + + testDeviceActionStates( + deviceAction, + expectedStates, + makeDeviceActionInternalApiMock(), + { + onError: reject, + onDone: () => { + resolve(); + }, + }, + ); + })); }); describe("BuildContext errors", () => { @@ -1145,6 +1539,9 @@ describe("SignTransactionDeviceAction", () => { data: { challenge: "challenge" }, }), ); + getWeb3CheckMock.mockResolvedValueOnce({ + web3Check: null, + }); buildContextMock.mockRejectedValueOnce( new InvalidStatusWordError("buildContext error"), ); @@ -1174,6 +1571,13 @@ describe("SignTransactionDeviceAction", () => { }, status: DeviceActionStatus.Pending, }, + // GetWeb3Check state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, // BuildContext state { intermediateValue: { @@ -1221,6 +1625,9 @@ describe("SignTransactionDeviceAction", () => { data: { challenge: "challenge" }, }), ); + getWeb3CheckMock.mockResolvedValueOnce({ + web3Check: null, + }); buildContextMock.mockResolvedValueOnce({ clearSignContexts: [ { @@ -1259,6 +1666,13 @@ describe("SignTransactionDeviceAction", () => { }, status: DeviceActionStatus.Pending, }, + // GetWeb3Check state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, // BuildContext state { intermediateValue: { @@ -1313,6 +1727,9 @@ describe("SignTransactionDeviceAction", () => { data: { challenge: "challenge" }, }), ); + getWeb3CheckMock.mockResolvedValueOnce({ + web3Check: null, + }); buildContextMock.mockResolvedValueOnce({ clearSignContexts: [ { @@ -1354,6 +1771,13 @@ describe("SignTransactionDeviceAction", () => { }, status: DeviceActionStatus.Pending, }, + // GetWeb3Check state + { + intermediateValue: { + requiredUserInteraction: UserInteractionRequired.None, + }, + status: DeviceActionStatus.Pending, + }, // BuildContext state { intermediateValue: { diff --git a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTypedData/SignTypedDataDeviceAction.test.ts b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTypedData/SignTypedDataDeviceAction.test.ts index f4b94bfdb..ad0b0750b 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTypedData/SignTypedDataDeviceAction.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTypedData/SignTypedDataDeviceAction.test.ts @@ -96,6 +96,7 @@ describe("SignTypedDataDeviceAction", () => { getContext: vi.fn(), getContexts: vi.fn(), getTypedDataFilters: vi.fn(), + getWeb3Checks: vi.fn(), }; const buildContextMock = vi.fn(); const provideContextMock = vi.fn(); diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/BuildEIP712ContextTask.test.ts b/packages/signer/signer-eth/src/internal/app-binder/task/BuildEIP712ContextTask.test.ts index 0aab058b5..105b686cf 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/BuildEIP712ContextTask.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/BuildEIP712ContextTask.test.ts @@ -22,6 +22,7 @@ describe("BuildEIP712ContextTask", () => { getContext: vi.fn(), getContexts: vi.fn(), getTypedDataFilters: vi.fn(), + getWeb3Checks: vi.fn(), }; const parserMock = { parse: vi.fn(), diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/BuildTransactionContextTask.test.ts b/packages/signer/signer-eth/src/internal/app-binder/task/BuildTransactionContextTask.test.ts index 9e75bcb28..f84cb39b4 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/BuildTransactionContextTask.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/BuildTransactionContextTask.test.ts @@ -27,6 +27,7 @@ describe("BuildTransactionContextTask", () => { getContext: vi.fn(), getContexts: vi.fn(), getTypedDataFilters: vi.fn(), + getWeb3Checks: vi.fn(), }; const mapperMock = { mapTransactionToSubset: vi.fn(), diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts index 93b5882ac..8b4b1ddf6 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts @@ -12,21 +12,21 @@ import { type TransactionMapperService } from "@internal/transaction/service/map describe("GetWeb3CheckTask", () => { const apiMock = makeDeviceActionInternalApiMock(); const contextModuleMock = { - getWeb3Checks: jest.fn(), + getWeb3Checks: vi.fn(), }; const mapperMock = { - mapTransactionToSubset: jest.fn(), + mapTransactionToSubset: vi.fn(), }; const transaction = new Uint8Array([0x01, 0x02, 0x03, 0x04]); const derivationPath = "44'/60'/0'/0/0"; describe("run", () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe("errors", () => { - it("should throw an error if mapTransactionToSubset fails", async () => { + it("should throw an error if mapTransactionToSubset assert.fails", async () => { // GIVEN const error = new Error("error"); mapperMock.mapTransactionToSubset.mockReturnValue(Left(error)); @@ -39,14 +39,14 @@ describe("GetWeb3CheckTask", () => { transaction, derivationPath, }).run(); - fail("should throw an error"); + assert.fail("should throw an error"); } catch (e) { // THEN expect(e).toEqual(error); } }); - it("should return a context error if GetAppConfiguration fails", async () => { + it("should return a context error if GetAppConfiguration assert.fails", async () => { // GIVEN mapperMock.mapTransactionToSubset.mockReturnValue( Right({ subset: {}, serializedTransaction: new Uint8Array() }), @@ -70,7 +70,7 @@ describe("GetWeb3CheckTask", () => { }); }); - it("should return a context error if GetAddressCommand fails", async () => { + it("should return a context error if GetAddressCommand assert.fails", async () => { // GIVEN mapperMock.mapTransactionToSubset.mockReturnValue( Right({ subset: {}, serializedTransaction: new Uint8Array() }), From dc3a4384d69c253df4ac4c2164a4ec452bcfc4cf Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Mon, 3 Feb 2025 10:31:43 +0100 Subject: [PATCH 05/12] =?UTF-8?q?=E2=9C=A8=20(sample):=20Add=20web3checks?= =?UTF-8?q?=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/odd-elephants-hope.md | 5 ++ .../components/CalView/Web3ChecksDrawer.tsx | 56 +++++++++++++++++++ apps/sample/src/components/CalView/index.tsx | 28 +++++++--- .../src/providers/SignerEthProvider/index.tsx | 28 +++++++++- 4 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 .changeset/odd-elephants-hope.md create mode 100644 apps/sample/src/components/CalView/Web3ChecksDrawer.tsx diff --git a/.changeset/odd-elephants-hope.md b/.changeset/odd-elephants-hope.md new file mode 100644 index 000000000..f7ab7586c --- /dev/null +++ b/.changeset/odd-elephants-hope.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-management-kit-sample": patch +--- + +Add web3checks url configuration diff --git a/apps/sample/src/components/CalView/Web3ChecksDrawer.tsx b/apps/sample/src/components/CalView/Web3ChecksDrawer.tsx new file mode 100644 index 000000000..8196b4da1 --- /dev/null +++ b/apps/sample/src/components/CalView/Web3ChecksDrawer.tsx @@ -0,0 +1,56 @@ +import React, { useCallback, useState } from "react"; +import { type ContextModuleWeb3ChecksConfig } from "@ledgerhq/context-module"; +import { Button, Divider, Flex } from "@ledgerhq/react-ui"; + +import { Block } from "@/components/Block"; +import { CommandForm } from "@/components/CommandsView/CommandForm"; +import { type FieldType } from "@/hooks/useForm"; +import { useWeb3ChecksConfig } from "@/providers/SignerEthProvider"; + +type Web3ChecksDrawerProps = { + onClose: () => void; +}; + +export function Web3ChecksDrawer({ onClose }: Web3ChecksDrawerProps) { + const { web3ChecksConfig, setWeb3ChecksConfig } = useWeb3ChecksConfig(); + const [values, setValues] = + useState>(web3ChecksConfig); + const labelSelector: Record = { + url: "Web3checks provider URL", + }; + + const onSettingsUpdate = useCallback(() => { + const { url } = values; + + console.log("Updating settings", values); + if (!url || typeof url !== "string" || !url.startsWith("http")) { + console.error("Invalid Web3Checks provider URL", url); + return; + } + + const newSettings: ContextModuleWeb3ChecksConfig = { + url, + }; + + setWeb3ChecksConfig(newSettings); + onClose(); + }, [onClose, setWeb3ChecksConfig, values]); + + return ( + + + + + + + + + + ); +} diff --git a/apps/sample/src/components/CalView/index.tsx b/apps/sample/src/components/CalView/index.tsx index 6875670f1..7ccb57b42 100644 --- a/apps/sample/src/components/CalView/index.tsx +++ b/apps/sample/src/components/CalView/index.tsx @@ -7,33 +7,34 @@ import { StyledDrawer } from "@/components/StyledDrawer"; import { CalCheckDappDrawer } from "./CalCheckDappDrawer"; import { CalSettingsDrawer } from "./CalSettingsDrawer"; +import { Web3ChecksDrawer } from "./Web3ChecksDrawer"; export const CalView = () => { const [isCheckDappOpen, setIsCheckDappOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); - const openCheckDapp = useCallback(() => { - setIsCheckDappOpen(true); - }, []); - - const openSettings = useCallback(() => { - setIsSettingsOpen(true); - }, []); + const [isWeb3ChecksOpen, setIsWeb3ChecksOpen] = useState(false); const closeDrawers = useCallback(() => { setIsCheckDappOpen(false); setIsSettingsOpen(false); + setIsWeb3ChecksOpen(false); }, []); const entries = [ { title: "Settings", description: "Settings for the Crypto Asset List", - onClick: openSettings, + onClick: () => setIsSettingsOpen(true), }, { title: "Check dApp availability", description: "Check dApp availability in Crypto Asset List", - onClick: openCheckDapp, + onClick: () => setIsCheckDappOpen(true), + }, + { + title: "Web3Checks Settings", + description: "Settings for the Web3Checks provider", + onClick: () => setIsWeb3ChecksOpen(true), }, ]; @@ -79,6 +80,15 @@ export const CalView = () => { > + + + ); }; diff --git a/apps/sample/src/providers/SignerEthProvider/index.tsx b/apps/sample/src/providers/SignerEthProvider/index.tsx index 76df959d2..22e1e857b 100644 --- a/apps/sample/src/providers/SignerEthProvider/index.tsx +++ b/apps/sample/src/providers/SignerEthProvider/index.tsx @@ -10,6 +10,7 @@ import React, { import { ContextModuleBuilder, type ContextModuleCalConfig, + type ContextModuleWeb3ChecksConfig, } from "@ledgerhq/context-module"; import { type SignerEth, @@ -22,7 +23,9 @@ import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider"; type SignerEthContextType = { signer: SignerEth | null; calConfig: ContextModuleCalConfig; + web3ChecksConfig: ContextModuleWeb3ChecksConfig; setCalConfig: (cal: ContextModuleCalConfig) => void; + setWeb3ChecksConfig: (web3Checks: ContextModuleWeb3ChecksConfig) => void; }; const initialState: SignerEthContextType = { @@ -32,7 +35,11 @@ const initialState: SignerEthContextType = { mode: "prod", branch: "main", }, + web3ChecksConfig: { + url: "https://web3checks-backend.api.aws.prd.ldg-tech.com/v3", + }, setCalConfig: () => {}, + setWeb3ChecksConfig: () => {}, }; const SignerEthContext = createContext(initialState); @@ -49,6 +56,8 @@ export const SignerEthProvider: React.FC = ({ const [calConfig, setCalConfig] = useState( initialState.calConfig, ); + const [web3ChecksConfig, setWeb3ChecksConfig] = + useState(initialState.web3ChecksConfig); useEffect(() => { if (!sessionId || !dmk) { @@ -58,15 +67,24 @@ export const SignerEthProvider: React.FC = ({ const contextModule = new ContextModuleBuilder() .addCalConfig(calConfig) + .addWeb3ChecksConfig(web3ChecksConfig) .build(); const newSigner = new SignerEthBuilder({ dmk, sessionId }) .withContextModule(contextModule) .build(); setSigner(newSigner); - }, [calConfig, dmk, sessionId]); + }, [calConfig, dmk, sessionId, web3ChecksConfig]); return ( - + {children} ); @@ -80,3 +98,9 @@ export const useCalConfig = () => { const { calConfig, setCalConfig } = useContext(SignerEthContext); return { calConfig, setCalConfig }; }; + +export const useWeb3ChecksConfig = () => { + const { web3ChecksConfig, setWeb3ChecksConfig } = + useContext(SignerEthContext); + return { web3ChecksConfig, setWeb3ChecksConfig }; +}; From 8d12f3b4db881033373a99212cb8ad7ee4115430 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 5 Feb 2025 12:31:42 +0100 Subject: [PATCH 06/12] =?UTF-8?q?=F0=9F=A9=B9=20(signer-eth):=20Send=20web?= =?UTF-8?q?3checks=20partner=20certificate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/DefaultContextModule.test.ts | 3 + .../src/DefaultContextModule.ts | 3 +- .../data/HttpPkiCertificateDataSource.test.ts | 9 ++- .../DefaultPkiCertificateLoader.test.ts | 3 +- .../context-module/src/pki/model/KeyId.ts | 1 + .../src/pki/model/PkiCertificateInfo.ts | 4 +- .../data/HttpWeb3CheckDataSource.test.ts | 76 +++++++++++++++++-- .../data/HttpWeb3CheckDataSource.ts | 27 +++++-- .../src/web3-check/data/Web3CheckDto.ts | 4 +- .../domain/DefaultWeb3CheckLoader.ts | 8 +- .../src/web3-check/domain/web3CheckTypes.ts | 6 ++ .../app-binder/task/GetWeb3CheckTask.test.ts | 46 ++++++++++- .../app-binder/task/GetWeb3CheckTask.ts | 3 + 13 files changed, 164 insertions(+), 29 deletions(-) diff --git a/packages/signer/context-module/src/DefaultContextModule.test.ts b/packages/signer/context-module/src/DefaultContextModule.test.ts index 849f1c111..5a2586d03 100644 --- a/packages/signer/context-module/src/DefaultContextModule.test.ts +++ b/packages/signer/context-module/src/DefaultContextModule.test.ts @@ -1,3 +1,4 @@ +import { DeviceModelId } from "@ledgerhq/device-management-kit"; import { Left, Right } from "purify-ts"; import { type ContextModuleConfig } from "./config/model/ContextModuleConfig"; @@ -126,6 +127,7 @@ describe("DefaultContextModule", () => { }); const res = await contextModule.getWeb3Checks({ + deviceModelId: DeviceModelId.FLEX, from: "from", rawTx: "rawTx", chainId: 1, @@ -145,6 +147,7 @@ describe("DefaultContextModule", () => { }); const res = await contextModule.getWeb3Checks({ + deviceModelId: DeviceModelId.FLEX, from: "from", rawTx: "rawTx", chainId: 1, diff --git a/packages/signer/context-module/src/DefaultContextModule.ts b/packages/signer/context-module/src/DefaultContextModule.ts index 0ff284a45..7e44aec8b 100644 --- a/packages/signer/context-module/src/DefaultContextModule.ts +++ b/packages/signer/context-module/src/DefaultContextModule.ts @@ -113,11 +113,10 @@ export class DefaultContextModule implements ContextModule { return null; } else { const web3ChecksValue = web3Checks.unsafeCoerce(); - // add Nano PKI fetch here should looks like => - // const web3CheckCertificate = await this._pkiCertificateLoader.fetchCertificate(...) return { type: ClearSignContextType.WEB3_CHECK, payload: web3ChecksValue.descriptor, + certificate: web3ChecksValue.certificate, }; } } diff --git a/packages/signer/context-module/src/pki/data/HttpPkiCertificateDataSource.test.ts b/packages/signer/context-module/src/pki/data/HttpPkiCertificateDataSource.test.ts index 2b92d4b45..a296f32d6 100644 --- a/packages/signer/context-module/src/pki/data/HttpPkiCertificateDataSource.test.ts +++ b/packages/signer/context-module/src/pki/data/HttpPkiCertificateDataSource.test.ts @@ -3,6 +3,7 @@ import { Left, Right } from "purify-ts"; import { type ContextModuleConfig } from "@/config/model/ContextModuleConfig"; import { HttpPkiCertificateDataSource } from "@/pki/data/HttpPkiCertificateDataSource"; +import { type KeyId } from "@/pki/model/KeyId"; import { KeyUsage } from "@/pki/model/KeyUsage"; import { type PkiCertificateInfo } from "@/pki/model/PkiCertificateInfo"; @@ -23,7 +24,7 @@ describe("HttpPkiCertificateDataSource", () => { const pkiCertificateInfo: PkiCertificateInfo = { targetDevice: "targetDevice", keyUsage: KeyUsage.Calldata, - keyId: "keyId", + keyId: "keyId" as KeyId, }; vi.spyOn(axios, "request").mockResolvedValue({ status: 200, @@ -60,7 +61,7 @@ describe("HttpPkiCertificateDataSource", () => { const pkiCertificateInfo: PkiCertificateInfo = { targetDevice: "targetDevice", keyUsage: KeyUsage.Calldata, - keyId: "keyId", + keyId: "keyId" as KeyId, }; vi.spyOn(axios, "request").mockResolvedValue({ status: 200, @@ -87,7 +88,7 @@ describe("HttpPkiCertificateDataSource", () => { const pkiCertificateInfo: PkiCertificateInfo = { targetDevice: "targetDevice", keyUsage: KeyUsage.Calldata, - keyId: "keyId", + keyId: "keyId" as KeyId, }; vi.spyOn(axios, "request").mockRejectedValue(new Error("error")); @@ -111,7 +112,7 @@ describe("HttpPkiCertificateDataSource", () => { const pkiCertificateInfo: PkiCertificateInfo = { targetDevice: "targetDevice", keyUsage: KeyUsage.Calldata, - keyId: "keyId", + keyId: "keyId" as KeyId, }; vi.spyOn(axios, "request").mockResolvedValue({ status: 200, diff --git a/packages/signer/context-module/src/pki/domain/DefaultPkiCertificateLoader.test.ts b/packages/signer/context-module/src/pki/domain/DefaultPkiCertificateLoader.test.ts index 960b76fbf..3af12453e 100644 --- a/packages/signer/context-module/src/pki/domain/DefaultPkiCertificateLoader.test.ts +++ b/packages/signer/context-module/src/pki/domain/DefaultPkiCertificateLoader.test.ts @@ -1,6 +1,7 @@ import { Right } from "purify-ts"; import { DefaultPkiCertificateLoader } from "@/pki/domain/DefaultPkiCertificateLoader"; +import { KeyId } from "@/pki/model/KeyId"; import { KeyUsage } from "@/pki/model/KeyUsage"; import { type PkiCertificateInfo } from "@/pki/model/PkiCertificateInfo"; @@ -11,7 +12,7 @@ describe("DefaultPkiCertificateLoader", () => { const certificateInfos: PkiCertificateInfo = { targetDevice: "targetDevice", keyUsage: KeyUsage.Calldata, - keyId: "keyId", + keyId: KeyId.CalNetwork, }; const certificate = { keyUsageNumber: 11, diff --git a/packages/signer/context-module/src/pki/model/KeyId.ts b/packages/signer/context-module/src/pki/model/KeyId.ts index 5b7bb6ea5..623650d90 100644 --- a/packages/signer/context-module/src/pki/model/KeyId.ts +++ b/packages/signer/context-module/src/pki/model/KeyId.ts @@ -10,4 +10,5 @@ export enum KeyId { CalCalldataKey = "cal_calldata_key", CalTrustedNameKey = "cal_trusted_name_key", CalNetwork = "cal_network", + Blockaid = "blockaid", } diff --git a/packages/signer/context-module/src/pki/model/PkiCertificateInfo.ts b/packages/signer/context-module/src/pki/model/PkiCertificateInfo.ts index 08fb40da3..004c97ab3 100644 --- a/packages/signer/context-module/src/pki/model/PkiCertificateInfo.ts +++ b/packages/signer/context-module/src/pki/model/PkiCertificateInfo.ts @@ -1,7 +1,9 @@ import type { KeyUsage } from "@/pki/model/KeyUsage"; +import { type KeyId } from "./KeyId"; + export type PkiCertificateInfo = { targetDevice: string; keyUsage: KeyUsage; - keyId?: string | undefined; + keyId: KeyId; }; diff --git a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts index 06c219e70..10e988bd4 100644 --- a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts +++ b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts @@ -1,7 +1,10 @@ +import { DeviceModelId } from "@ledgerhq/device-management-kit"; import axios from "axios"; import { Left, Right } from "purify-ts"; import { type ContextModuleConfig } from "@/config/model/ContextModuleConfig"; +import { type PkiCertificateLoader } from "@/pki/domain/PkiCertificateLoader"; +import { KeyId } from "@/pki/model/KeyId"; import { HttpWeb3CheckDataSource } from "@/web3-check/data/HttpWeb3CheckDataSource"; import { type Web3CheckDto } from "@/web3-check/data/Web3CheckDto"; import { type Web3CheckContext } from "@/web3-check/domain/web3CheckTypes"; @@ -14,6 +17,9 @@ describe("HttpWeb3CheckDataSource", () => { url: "web3checksUrl", }, } as ContextModuleConfig; + const certificateLoaderMock = { + loadCertificate: jest.fn(), + }; beforeEach(() => { vi.resetAllMocks(); @@ -23,26 +29,71 @@ describe("HttpWeb3CheckDataSource", () => { it("should return an object if the request is successful", async () => { // GIVEN const params: Web3CheckContext = { + deviceModelId: DeviceModelId.FLEX, from: "from", rawTx: "rawTx", chainId: 1, }; const dto: Web3CheckDto = { block: 1, - public_key_id: "publicKeyId", + public_key_id: KeyId.Blockaid, descriptor: "descriptor", }; - vi.spyOn(axios, "request").mockResolvedValue({ data: dto }); + jest.spyOn(axios, "request").mockResolvedValueOnce({ data: dto }); + jest + .spyOn(certificateLoaderMock, "loadCertificate") + .mockResolvedValueOnce(undefined); // WHEN - const dataSource = new HttpWeb3CheckDataSource(config); + const dataSource = new HttpWeb3CheckDataSource( + config, + certificateLoaderMock as unknown as PkiCertificateLoader, + ); + const result = await dataSource.getWeb3Checks(params); + + // THEN + expect(result).toEqual( + Right({ + publicKeyId: "blockaid", + descriptor: "descriptor", + }), + ); + }); + + it("should return an object with a certificate if the request is successful", async () => { + // GIVEN + const params: Web3CheckContext = { + deviceModelId: DeviceModelId.FLEX, + from: "from", + rawTx: "rawTx", + chainId: 1, + }; + const dto: Web3CheckDto = { + block: 1, + public_key_id: KeyId.Blockaid, + descriptor: "descriptor", + }; + jest.spyOn(axios, "request").mockResolvedValueOnce({ data: dto }); + jest + .spyOn(certificateLoaderMock, "loadCertificate") + .mockResolvedValueOnce({ + keyUsageNumber: 11, + payload: new Uint8Array([0x01]), + }); + + // WHEN + const dataSource = new HttpWeb3CheckDataSource( + config, + certificateLoaderMock as unknown as PkiCertificateLoader, + ); const result = await dataSource.getWeb3Checks(params); // THEN expect(result).toEqual( Right({ - publicKeyId: "publicKeyId", + publicKeyId: "blockaid", descriptor: "descriptor", + certificate: { keyUsageNumber: 11, payload: new Uint8Array([0x01]) }, }), ); }); @@ -50,6 +101,7 @@ describe("HttpWeb3CheckDataSource", () => { it("should return an error if the request fails", async () => { // GIVEN const params: Web3CheckContext = { + deviceModelId: DeviceModelId.FLEX, from: "from", rawTx: "rawTx", chainId: 1, @@ -57,7 +109,10 @@ describe("HttpWeb3CheckDataSource", () => { vi.spyOn(axios, "request").mockRejectedValue(new Error("error")); // WHEN - const dataSource = new HttpWeb3CheckDataSource(config); + const dataSource = new HttpWeb3CheckDataSource( + config, + certificateLoaderMock as unknown as PkiCertificateLoader, + ); const result = await dataSource.getWeb3Checks(params); // THEN @@ -73,15 +128,22 @@ describe("HttpWeb3CheckDataSource", () => { it("should return an error if the response is invalid", async () => { // GIVEN const params: Web3CheckContext = { + deviceModelId: DeviceModelId.FLEX, from: "from", rawTx: "rawTx", chainId: 1, }; const dto = {}; - vi.spyOn(axios, "request").mockResolvedValue({ data: dto }); + jest.spyOn(axios, "request").mockResolvedValue({ data: dto }); + jest + .spyOn(certificateLoaderMock, "loadCertificate") + .mockResolvedValue(undefined); // WHEN - const dataSource = new HttpWeb3CheckDataSource(config); + const dataSource = new HttpWeb3CheckDataSource( + config, + certificateLoaderMock as unknown as PkiCertificateLoader, + ); const result = await dataSource.getWeb3Checks(params); // THEN diff --git a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts index 29be8e187..eaaf8c0e8 100644 --- a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts +++ b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts @@ -4,6 +4,9 @@ import { Either, Left, Right } from "purify-ts"; import { configTypes } from "@/config/di/configTypes"; import type { ContextModuleConfig } from "@/config/model/ContextModuleConfig"; +import { pkiTypes } from "@/pki/di/pkiTypes"; +import { type PkiCertificateLoader } from "@/pki/domain/PkiCertificateLoader"; +import { KeyUsage } from "@/pki/model/KeyUsage"; import { type Web3CheckContext, type Web3Checks, @@ -17,20 +20,25 @@ import { GetWeb3ChecksRequestDto, Web3CheckDto } from "./Web3CheckDto"; export class HttpWeb3CheckDataSource implements Web3CheckDataSource { constructor( @inject(configTypes.Config) private readonly config: ContextModuleConfig, + @inject(pkiTypes.PkiCertificateLoader) + private readonly _certificateLoader: PkiCertificateLoader, ) {} - async getWeb3Checks( - params: Web3CheckContext, - ): Promise> { + async getWeb3Checks({ + chainId, + deviceModelId, + from, + rawTx, + }: Web3CheckContext): Promise> { let web3CheckDto: Web3CheckDto; try { const requestDto: GetWeb3ChecksRequestDto = { tx: { - from: params.from, - raw: params.rawTx, + from, + raw: rawTx, }, - chain: params.chainId, + chain: chainId, preset: "blockaid", }; const response = await axios.request({ @@ -59,9 +67,16 @@ export class HttpWeb3CheckDataSource implements Web3CheckDataSource { ); } + const certificate = await this._certificateLoader.loadCertificate({ + keyId: web3CheckDto.public_key_id, + keyUsage: "replace-me" as KeyUsage, // TODO: replace with the keyUsage given by the API + targetDevice: deviceModelId, + }); + const result: Web3Checks = { publicKeyId: web3CheckDto.public_key_id, descriptor: web3CheckDto.descriptor, + certificate, }; return Right(result); diff --git a/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts b/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts index 16e10f392..1c7e1874a 100644 --- a/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts +++ b/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts @@ -1,3 +1,5 @@ +import { type KeyId } from "@/pki/model/KeyId"; + export type GetWeb3ChecksRequestDto = { tx: { from: string; @@ -9,7 +11,7 @@ export type GetWeb3ChecksRequestDto = { }; export type Web3CheckDto = { - public_key_id: string; + public_key_id: KeyId; descriptor: string; block: number; }; diff --git a/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.ts b/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.ts index 1832965b7..d5e56c9cb 100644 --- a/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.ts +++ b/packages/signer/context-module/src/web3-check/domain/DefaultWeb3CheckLoader.ts @@ -21,7 +21,7 @@ export class DefaultWeb3CheckContextLoader implements Web3CheckContextLoader { async load( web3CheckContext: Web3CheckContext, ): Promise> { - const { chainId, rawTx, from } = web3CheckContext; + const { rawTx, from } = web3CheckContext; if (rawTx == undefined || typeof rawTx != "string") { return Left( @@ -40,10 +40,6 @@ export class DefaultWeb3CheckContextLoader implements Web3CheckContextLoader { } // Handle descritor payload - return await this._dataSource.getWeb3Checks({ - chainId: chainId, - rawTx: rawTx, - from: from, - }); + return await this._dataSource.getWeb3Checks(web3CheckContext); } } diff --git a/packages/signer/context-module/src/web3-check/domain/web3CheckTypes.ts b/packages/signer/context-module/src/web3-check/domain/web3CheckTypes.ts index c85805811..5a2271cdc 100644 --- a/packages/signer/context-module/src/web3-check/domain/web3CheckTypes.ts +++ b/packages/signer/context-module/src/web3-check/domain/web3CheckTypes.ts @@ -1,10 +1,16 @@ +import { type DeviceModelId } from "@ledgerhq/device-management-kit"; + +import { type PkiCertificate } from "@/pki/model/PkiCertificate"; + export type Web3CheckContext = { from: string; rawTx: string; chainId: number; + deviceModelId: DeviceModelId; }; export type Web3Checks = { publicKeyId: string; descriptor: string; + certificate?: PkiCertificate; }; diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts index 8b4b1ddf6..a2a2fa627 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts @@ -1,6 +1,9 @@ import { type ContextModule } from "@ledgerhq/context-module"; import { CommandResultFactory, + DeviceModelId, + DeviceSessionStateType, + DeviceStatus, InvalidStatusWordError, } from "@ledgerhq/device-management-kit"; import { Left, Right } from "purify-ts"; @@ -22,7 +25,15 @@ describe("GetWeb3CheckTask", () => { describe("run", () => { beforeEach(() => { - vi.clearAllMocks(); + jest.clearAllMocks(); + + apiMock.getDeviceSessionState.mockReturnValueOnce({ + sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, + deviceStatus: DeviceStatus.CONNECTED, + installedApps: [], + currentApp: { name: "Ethereum", version: "1.15.0" }, + deviceModelId: DeviceModelId.FLEX, + }); }); describe("errors", () => { @@ -170,6 +181,39 @@ describe("GetWeb3CheckTask", () => { web3Check, }); }); + + it("should call the context module with the right parameters", async () => { + // GIVEN + mapperMock.mapTransactionToSubset.mockReturnValue( + Right({ + subset: { chainId: 15, from: "from" }, + serializedTransaction: new Uint8Array([0x01, 0x02, 0x03]), + }), + ); + apiMock.sendCommand.mockResolvedValueOnce( + CommandResultFactory({ data: { web3ChecksEnabled: true } }), + ); + apiMock.sendCommand.mockResolvedValueOnce( + CommandResultFactory({ data: { address: "address" } }), + ); + contextModuleMock.getWeb3Checks.mockResolvedValue(null); + + // WHEN + await new GetWeb3CheckTask(apiMock, { + contextModule: contextModuleMock as unknown as ContextModule, + mapper: mapperMock as unknown as TransactionMapperService, + transaction, + derivationPath, + }).run(); + + // THEN + expect(contextModuleMock.getWeb3Checks).toHaveBeenCalledWith({ + deviceModelId: DeviceModelId.FLEX, + from: "address", + rawTx: "0x010203", + chainId: 15, + }); + }); }); }); }); diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts index 1db3a0f28..14b3ee645 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts @@ -40,6 +40,8 @@ export class GetWeb3CheckTask { async run(): Promise { const { contextModule, mapper, transaction } = this.args; + const { deviceModelId } = this.api.getDeviceSessionState(); + const parsed = mapper.mapTransactionToSubset(transaction); parsed.ifLeft((err) => { throw err; @@ -78,6 +80,7 @@ export class GetWeb3CheckTask { const address = getAddressResult.data.address; const web3Params: Web3CheckContext = { + deviceModelId, from: address, rawTx: bufferToHexaString(serializedTransaction), chainId: subset.chainId, From 0d77b2ad4dda1f12112a9ec47eacd656f1b4c0a2 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 5 Feb 2025 12:32:00 +0100 Subject: [PATCH 07/12] =?UTF-8?q?=E2=9C=85=20(signer-eth):=20Update=20test?= =?UTF-8?q?s=20to=20vitest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/HttpWeb3CheckDataSource.test.ts | 30 +++++++++---------- .../app-binder/task/GetWeb3CheckTask.test.ts | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts index 10e988bd4..5470de433 100644 --- a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts +++ b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts @@ -18,7 +18,7 @@ describe("HttpWeb3CheckDataSource", () => { }, } as ContextModuleConfig; const certificateLoaderMock = { - loadCertificate: jest.fn(), + loadCertificate: vi.fn(), }; beforeEach(() => { @@ -39,10 +39,10 @@ describe("HttpWeb3CheckDataSource", () => { public_key_id: KeyId.Blockaid, descriptor: "descriptor", }; - jest.spyOn(axios, "request").mockResolvedValueOnce({ data: dto }); - jest - .spyOn(certificateLoaderMock, "loadCertificate") - .mockResolvedValueOnce(undefined); + vi.spyOn(axios, "request").mockResolvedValueOnce({ data: dto }); + vi.spyOn(certificateLoaderMock, "loadCertificate").mockResolvedValueOnce( + undefined, + ); // WHEN const dataSource = new HttpWeb3CheckDataSource( @@ -73,13 +73,11 @@ describe("HttpWeb3CheckDataSource", () => { public_key_id: KeyId.Blockaid, descriptor: "descriptor", }; - jest.spyOn(axios, "request").mockResolvedValueOnce({ data: dto }); - jest - .spyOn(certificateLoaderMock, "loadCertificate") - .mockResolvedValueOnce({ - keyUsageNumber: 11, - payload: new Uint8Array([0x01]), - }); + vi.spyOn(axios, "request").mockResolvedValueOnce({ data: dto }); + vi.spyOn(certificateLoaderMock, "loadCertificate").mockResolvedValueOnce({ + keyUsageNumber: 11, + payload: new Uint8Array([0x01]), + }); // WHEN const dataSource = new HttpWeb3CheckDataSource( @@ -134,10 +132,10 @@ describe("HttpWeb3CheckDataSource", () => { chainId: 1, }; const dto = {}; - jest.spyOn(axios, "request").mockResolvedValue({ data: dto }); - jest - .spyOn(certificateLoaderMock, "loadCertificate") - .mockResolvedValue(undefined); + vi.spyOn(axios, "request").mockResolvedValue({ data: dto }); + vi.spyOn(certificateLoaderMock, "loadCertificate").mockResolvedValue( + undefined, + ); // WHEN const dataSource = new HttpWeb3CheckDataSource( diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts index a2a2fa627..3392c0700 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts @@ -25,7 +25,7 @@ describe("GetWeb3CheckTask", () => { describe("run", () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); apiMock.getDeviceSessionState.mockReturnValueOnce({ sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel, From c50aacef00504d14830c8d4754dc19a7eb52189a Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 5 Feb 2025 12:33:58 +0100 Subject: [PATCH 08/12] =?UTF-8?q?=F0=9F=A9=B9=20(signer-eth):=20Provide=20?= =?UTF-8?q?web3checks=20with=20generic=20parser=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/DefaultContextModule.ts | 3 +- .../SignTransactionDeviceActionTypes.ts | 3 +- .../SignTransactionDeviceAction.test.ts | 2 ++ .../SignTransactionDeviceAction.ts | 9 ++++-- .../app-binder/task/GetWeb3CheckTask.test.ts | 30 +++++++++++++++++++ .../app-binder/task/GetWeb3CheckTask.ts | 4 +-- .../task/ProvideTransactionContextTask.ts | 2 +- ...ovideTransactionGenericContextTask.test.ts | 1 + .../ProvideTransactionGenericContextTask.ts | 8 ++++- 9 files changed, 54 insertions(+), 8 deletions(-) diff --git a/packages/signer/context-module/src/DefaultContextModule.ts b/packages/signer/context-module/src/DefaultContextModule.ts index 7e44aec8b..801e28d90 100644 --- a/packages/signer/context-module/src/DefaultContextModule.ts +++ b/packages/signer/context-module/src/DefaultContextModule.ts @@ -13,6 +13,7 @@ import { type NftContextLoader } from "./nft/domain/NftContextLoader"; import { type ContextLoader } from "./shared/domain/ContextLoader"; import { type ClearSignContext, + type ClearSignContextSuccess, ClearSignContextType, } from "./shared/model/ClearSignContext"; import { @@ -106,7 +107,7 @@ export class DefaultContextModule implements ContextModule { public async getWeb3Checks( transactionContext: Web3CheckContext, - ): Promise { + ): Promise | null> { const web3Checks = await this._web3CheckLoader.load(transactionContext); if (web3Checks.isLeft()) { diff --git a/packages/signer/signer-eth/src/api/app-binder/SignTransactionDeviceActionTypes.ts b/packages/signer/signer-eth/src/api/app-binder/SignTransactionDeviceActionTypes.ts index d7c3c482d..cc637fba6 100644 --- a/packages/signer/signer-eth/src/api/app-binder/SignTransactionDeviceActionTypes.ts +++ b/packages/signer/signer-eth/src/api/app-binder/SignTransactionDeviceActionTypes.ts @@ -1,5 +1,6 @@ import { type ClearSignContextSuccess, + type ClearSignContextType, type ContextModule, } from "@ledgerhq/context-module"; import { @@ -52,7 +53,7 @@ export type SignTransactionDAInternalState = { readonly error: SignTransactionDAError | null; readonly challenge: string | null; readonly clearSignContexts: ClearSignContextSuccess[] | GenericContext | null; - readonly web3Check: ClearSignContextSuccess | null; + readonly web3Check: ClearSignContextSuccess | null; readonly serializedTransaction: Uint8Array | null; readonly chainId: number | null; readonly transactionType: TransactionType | null; diff --git a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts index c00f89796..dfb310e8d 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.test.ts @@ -398,6 +398,7 @@ describe("SignTransactionDeviceAction", () => { derivationPath: "44'/60'/0'/0/0", serializedTransaction: new Uint8Array([0x01, 0x02, 0x03]), transactionParser: parserMock, + web3Check: null, }, }), ); @@ -748,6 +749,7 @@ describe("SignTransactionDeviceAction", () => { derivationPath: "44'/60'/0'/0/0", serializedTransaction: new Uint8Array([0x01, 0x02, 0x03]), transactionParser: parserMock, + web3Check: null, }, }), ); diff --git a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.ts b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.ts index 3c253e83f..0cf0d963c 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/device-action/SignTransaction/SignTransactionDeviceAction.ts @@ -1,5 +1,6 @@ import { type ClearSignContextSuccess, + type ClearSignContextType, type ContextModule, } from "@ledgerhq/context-module"; import { @@ -78,7 +79,7 @@ export type MachineDependencies = { readonly provideContext: (arg0: { input: { clearSignContexts: ClearSignContextSuccess[]; - web3Check: ClearSignContextSuccess | null; + web3Check: ClearSignContextSuccess | null; }; }) => Promise>>; readonly provideGenericContext: (arg0: { @@ -89,6 +90,7 @@ export type MachineDependencies = { derivationPath: string; serializedTransaction: Uint8Array; context: GenericContext; + web3Check: ClearSignContextSuccess | null; }; }) => Promise< Maybe> @@ -389,6 +391,7 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< context._internalState.serializedTransaction!, context: context._internalState .clearSignContexts as GenericContext, + web3Check: context._internalState.web3Check, }), onDone: { actions: assign({ @@ -499,7 +502,7 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< const provideContext = async (arg0: { input: { clearSignContexts: ClearSignContextSuccess[]; - web3Check: ClearSignContextSuccess | null; + web3Check: ClearSignContextSuccess | null; }; }) => new ProvideTransactionContextTask(internalApi, { @@ -515,6 +518,7 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< derivationPath: string; serializedTransaction: Uint8Array; context: GenericContext; + web3Check: ClearSignContextSuccess | null; }; }) => new ProvideTransactionGenericContextTask(internalApi, { @@ -524,6 +528,7 @@ export class SignTransactionDeviceAction extends XStateDeviceAction< derivationPath: arg0.input.derivationPath, serializedTransaction: arg0.input.serializedTransaction, context: arg0.input.context, + web3Check: arg0.input.web3Check, }).run(); const signTransaction = async (arg0: { diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts index 3392c0700..fdbf5cc8c 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.test.ts @@ -107,6 +107,36 @@ describe("GetWeb3CheckTask", () => { error: new InvalidStatusWordError("error"), }); }); + + it("should return null if the type is not a ClearSignContextSuccess web3check", async () => { + // GIVEN + mapperMock.mapTransactionToSubset.mockReturnValue( + Right({ subset: {}, serializedTransaction: new Uint8Array() }), + ); + apiMock.sendCommand.mockResolvedValueOnce( + CommandResultFactory({ data: { web3ChecksEnabled: true } }), + ); + apiMock.sendCommand.mockResolvedValueOnce( + CommandResultFactory({ data: { address: "address" } }), + ); + contextModuleMock.getWeb3Checks.mockResolvedValue({ + type: "invalid-type", + id: 1, + }); + + // WHEN + const result = await new GetWeb3CheckTask(apiMock, { + contextModule: contextModuleMock as unknown as ContextModule, + mapper: mapperMock as unknown as TransactionMapperService, + transaction, + derivationPath, + }).run(); + + // THEN + expect(result).toEqual({ + web3Check: null, + }); + }); }); describe("success", () => { diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts index 14b3ee645..1e973db03 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/GetWeb3CheckTask.ts @@ -18,7 +18,7 @@ import { type TransactionMapperService } from "@internal/transaction/service/map export type GetWeb3CheckTaskResult = | { - readonly web3Check: ClearSignContextSuccess | null; + readonly web3Check: ClearSignContextSuccess | null; } | { readonly web3Check: null; @@ -90,7 +90,7 @@ export class GetWeb3CheckTask { if ( web3CheckContext === null || - web3CheckContext?.type === ClearSignContextType.ERROR + web3CheckContext?.type !== ClearSignContextType.WEB3_CHECK ) { return { web3Check: null, diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.ts b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.ts index 9f8874bee..f247fe792 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionContextTask.ts @@ -31,7 +31,7 @@ export type ProvideTransactionContextTaskArgs = { * The valid clear sign contexts offerred by the `BuildTrancationContextTask`. */ clearSignContexts: ClearSignContextSuccess[]; - web3Check: ClearSignContextSuccess | null; + web3Check: ClearSignContextSuccess | null; }; /** diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionGenericContextTask.test.ts b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionGenericContextTask.test.ts index 06980f8fa..81e0b0a6f 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionGenericContextTask.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionGenericContextTask.test.ts @@ -52,6 +52,7 @@ describe("ProvideTransactionGenericContextTask", () => { chainId, transactionParser, contextModule, + web3Check: null, }; const apiMock = makeDeviceActionInternalApiMock(); diff --git a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionGenericContextTask.ts b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionGenericContextTask.ts index 5dcfe9827..78b215b4e 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionGenericContextTask.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/task/ProvideTransactionGenericContextTask.ts @@ -40,6 +40,7 @@ export type ProvideTransactionGenericContextTaskArgs = { readonly derivationPath: string; readonly serializedTransaction: Uint8Array; readonly context: GenericContext; + readonly web3Check: ClearSignContextSuccess | null; }; export type ProvideTransactionGenericContextTaskErrorCodes = @@ -100,8 +101,13 @@ export class ProvideTransactionGenericContextTask { return Just(transactionInfoResult); } + // If there is a web3 check, add it to the transactionField array + const fields = this.args.web3Check + ? [...this.args.context.transactionFields, this.args.web3Check] + : this.args.context.transactionFields; + // Provide the transaction field description and according metadata reference - for (const field of this.args.context.transactionFields) { + for (const field of fields) { const result = await new ProvideTransactionFieldDescriptionTask( this.api, { From 9a8c9cece1b4181239411d8d3ab8f22368b645a1 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 5 Feb 2025 12:40:03 +0100 Subject: [PATCH 09/12] =?UTF-8?q?=F0=9F=A9=B9=20(signer-eth):=20Fix=20buil?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../signer-eth/src/internal/app-binder/EthAppBinder.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/signer/signer-eth/src/internal/app-binder/EthAppBinder.test.ts b/packages/signer/signer-eth/src/internal/app-binder/EthAppBinder.test.ts index a99c6924d..63b1b5ec2 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/EthAppBinder.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/EthAppBinder.test.ts @@ -49,6 +49,7 @@ describe("EthAppBinder", () => { getContext: vi.fn(), getContexts: vi.fn(), getTypedDataFilters: vi.fn(), + getWeb3Checks: vi.fn(), }; const mockedMapper: TransactionMapperService = { mapTransactionToSubset: vi.fn(), From 935c5a08a8ea57435c6f538cb81faea0574f2559 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 5 Feb 2025 14:56:25 +0100 Subject: [PATCH 10/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(context-module):=20?= =?UTF-8?q?Use=20string=20for=20key=20id=20type=20with=20PkiCertificateInf?= =?UTF-8?q?o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pki/data/HttpPkiCertificateDataSource.test.ts | 9 ++++----- packages/signer/context-module/src/pki/model/KeyId.ts | 1 - .../context-module/src/pki/model/PkiCertificateInfo.ts | 4 +--- .../src/web3-check/data/HttpWeb3CheckDataSource.test.ts | 9 ++++----- .../context-module/src/web3-check/data/Web3CheckDto.ts | 2 +- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/signer/context-module/src/pki/data/HttpPkiCertificateDataSource.test.ts b/packages/signer/context-module/src/pki/data/HttpPkiCertificateDataSource.test.ts index a296f32d6..2b92d4b45 100644 --- a/packages/signer/context-module/src/pki/data/HttpPkiCertificateDataSource.test.ts +++ b/packages/signer/context-module/src/pki/data/HttpPkiCertificateDataSource.test.ts @@ -3,7 +3,6 @@ import { Left, Right } from "purify-ts"; import { type ContextModuleConfig } from "@/config/model/ContextModuleConfig"; import { HttpPkiCertificateDataSource } from "@/pki/data/HttpPkiCertificateDataSource"; -import { type KeyId } from "@/pki/model/KeyId"; import { KeyUsage } from "@/pki/model/KeyUsage"; import { type PkiCertificateInfo } from "@/pki/model/PkiCertificateInfo"; @@ -24,7 +23,7 @@ describe("HttpPkiCertificateDataSource", () => { const pkiCertificateInfo: PkiCertificateInfo = { targetDevice: "targetDevice", keyUsage: KeyUsage.Calldata, - keyId: "keyId" as KeyId, + keyId: "keyId", }; vi.spyOn(axios, "request").mockResolvedValue({ status: 200, @@ -61,7 +60,7 @@ describe("HttpPkiCertificateDataSource", () => { const pkiCertificateInfo: PkiCertificateInfo = { targetDevice: "targetDevice", keyUsage: KeyUsage.Calldata, - keyId: "keyId" as KeyId, + keyId: "keyId", }; vi.spyOn(axios, "request").mockResolvedValue({ status: 200, @@ -88,7 +87,7 @@ describe("HttpPkiCertificateDataSource", () => { const pkiCertificateInfo: PkiCertificateInfo = { targetDevice: "targetDevice", keyUsage: KeyUsage.Calldata, - keyId: "keyId" as KeyId, + keyId: "keyId", }; vi.spyOn(axios, "request").mockRejectedValue(new Error("error")); @@ -112,7 +111,7 @@ describe("HttpPkiCertificateDataSource", () => { const pkiCertificateInfo: PkiCertificateInfo = { targetDevice: "targetDevice", keyUsage: KeyUsage.Calldata, - keyId: "keyId" as KeyId, + keyId: "keyId", }; vi.spyOn(axios, "request").mockResolvedValue({ status: 200, diff --git a/packages/signer/context-module/src/pki/model/KeyId.ts b/packages/signer/context-module/src/pki/model/KeyId.ts index 623650d90..5b7bb6ea5 100644 --- a/packages/signer/context-module/src/pki/model/KeyId.ts +++ b/packages/signer/context-module/src/pki/model/KeyId.ts @@ -10,5 +10,4 @@ export enum KeyId { CalCalldataKey = "cal_calldata_key", CalTrustedNameKey = "cal_trusted_name_key", CalNetwork = "cal_network", - Blockaid = "blockaid", } diff --git a/packages/signer/context-module/src/pki/model/PkiCertificateInfo.ts b/packages/signer/context-module/src/pki/model/PkiCertificateInfo.ts index 004c97ab3..a1ed306d6 100644 --- a/packages/signer/context-module/src/pki/model/PkiCertificateInfo.ts +++ b/packages/signer/context-module/src/pki/model/PkiCertificateInfo.ts @@ -1,9 +1,7 @@ import type { KeyUsage } from "@/pki/model/KeyUsage"; -import { type KeyId } from "./KeyId"; - export type PkiCertificateInfo = { targetDevice: string; keyUsage: KeyUsage; - keyId: KeyId; + keyId: string; }; diff --git a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts index 5470de433..2ceb6c6f9 100644 --- a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts +++ b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.test.ts @@ -4,7 +4,6 @@ import { Left, Right } from "purify-ts"; import { type ContextModuleConfig } from "@/config/model/ContextModuleConfig"; import { type PkiCertificateLoader } from "@/pki/domain/PkiCertificateLoader"; -import { KeyId } from "@/pki/model/KeyId"; import { HttpWeb3CheckDataSource } from "@/web3-check/data/HttpWeb3CheckDataSource"; import { type Web3CheckDto } from "@/web3-check/data/Web3CheckDto"; import { type Web3CheckContext } from "@/web3-check/domain/web3CheckTypes"; @@ -36,7 +35,7 @@ describe("HttpWeb3CheckDataSource", () => { }; const dto: Web3CheckDto = { block: 1, - public_key_id: KeyId.Blockaid, + public_key_id: "partner", descriptor: "descriptor", }; vi.spyOn(axios, "request").mockResolvedValueOnce({ data: dto }); @@ -54,7 +53,7 @@ describe("HttpWeb3CheckDataSource", () => { // THEN expect(result).toEqual( Right({ - publicKeyId: "blockaid", + publicKeyId: "partner", descriptor: "descriptor", }), ); @@ -70,7 +69,7 @@ describe("HttpWeb3CheckDataSource", () => { }; const dto: Web3CheckDto = { block: 1, - public_key_id: KeyId.Blockaid, + public_key_id: "partner", descriptor: "descriptor", }; vi.spyOn(axios, "request").mockResolvedValueOnce({ data: dto }); @@ -89,7 +88,7 @@ describe("HttpWeb3CheckDataSource", () => { // THEN expect(result).toEqual( Right({ - publicKeyId: "blockaid", + publicKeyId: "partner", descriptor: "descriptor", certificate: { keyUsageNumber: 11, payload: new Uint8Array([0x01]) }, }), diff --git a/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts b/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts index 1c7e1874a..fca9ed4f3 100644 --- a/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts +++ b/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts @@ -11,7 +11,7 @@ export type GetWeb3ChecksRequestDto = { }; export type Web3CheckDto = { - public_key_id: KeyId; + public_key_id: string; descriptor: string; block: number; }; From 48e5efcc08cd725ac5e7342a165019f881f2d5af Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 5 Feb 2025 14:57:09 +0100 Subject: [PATCH 11/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(context-module):=20?= =?UTF-8?q?Remove=20block=20and=20preset=20from=20web3check=20dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/web3-check/data/HttpWeb3CheckDataSource.ts | 1 - .../signer/context-module/src/web3-check/data/Web3CheckDto.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts index eaaf8c0e8..b8c8b8e03 100644 --- a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts +++ b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts @@ -39,7 +39,6 @@ export class HttpWeb3CheckDataSource implements Web3CheckDataSource { raw: rawTx, }, chain: chainId, - preset: "blockaid", }; const response = await axios.request({ method: "POST", diff --git a/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts b/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts index fca9ed4f3..41cc21d4c 100644 --- a/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts +++ b/packages/signer/context-module/src/web3-check/data/Web3CheckDto.ts @@ -1,13 +1,9 @@ -import { type KeyId } from "@/pki/model/KeyId"; - export type GetWeb3ChecksRequestDto = { tx: { from: string; raw: string; }; chain: number; - preset: string; - block?: number; }; export type Web3CheckDto = { From 85c3a5e78e8d50381053e5cb26023b2feb39c189 Mon Sep 17 00:00:00 2001 From: Louis Aussedat Date: Wed, 5 Feb 2025 15:04:23 +0100 Subject: [PATCH 12/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(context-module):=20?= =?UTF-8?q?Use=20tx=5Fsimu=5Fsigner=20with=20web3check=20cert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/web3-check/data/HttpWeb3CheckDataSource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts index b8c8b8e03..098348aa8 100644 --- a/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts +++ b/packages/signer/context-module/src/web3-check/data/HttpWeb3CheckDataSource.ts @@ -68,7 +68,7 @@ export class HttpWeb3CheckDataSource implements Web3CheckDataSource { const certificate = await this._certificateLoader.loadCertificate({ keyId: web3CheckDto.public_key_id, - keyUsage: "replace-me" as KeyUsage, // TODO: replace with the keyUsage given by the API + keyUsage: KeyUsage.TxSimulationSigner, targetDevice: deviceModelId, });