From 0b0d50934bc86f61510eeb2bafd5d248c1c42a23 Mon Sep 17 00:00:00 2001 From: Pierre Aoun Date: Tue, 19 Nov 2024 19:40:21 +0100 Subject: [PATCH 1/2] :sparkles: (context-module): Implement transaction fields loader --- .../context-module/src/ContextModule.ts | 6 +- .../src/DefaultContextModule.test.ts | 66 ++++++++++++++++++- .../src/DefaultContextModule.ts | 25 ++++++- .../src/nft/domain/NftContextLoader.test.ts | 57 +++++++++++++++- .../src/nft/domain/NftContextLoader.ts | 27 +++++++- .../src/shared/domain/ContextLoader.ts | 8 ++- .../src/shared/model/TransactionContext.ts | 16 +++++ .../token/domain/TokenContextLoader.test.ts | 61 ++++++++++++++++- .../src/token/domain/TokenContextLoader.ts | 27 +++++++- .../internal/app-binder/EthAppBinder.test.ts | 1 + .../SignTransactionDeviceAction.test.ts | 1 + .../SignTypedDataDeviceAction.test.ts | 1 + .../task/BuildEIP712ContextTask.test.ts | 1 + .../task/BuildTransactionContextTask.test.ts | 1 + 14 files changed, 288 insertions(+), 10 deletions(-) diff --git a/packages/signer/context-module/src/ContextModule.ts b/packages/signer/context-module/src/ContextModule.ts index 8a8b03f80..7a5df37d4 100644 --- a/packages/signer/context-module/src/ContextModule.ts +++ b/packages/signer/context-module/src/ContextModule.ts @@ -1,10 +1,14 @@ import { type ClearSignContext } from "@/shared/model/ClearSignContext"; -import { type TransactionContext } from "./shared/model/TransactionContext"; +import { + type TransactionContext, + type TransactionFieldContext, +} from "./shared/model/TransactionContext"; import { type TypedDataClearSignContext } from "./shared/model/TypedDataClearSignContext"; import { type TypedDataContext } from "./shared/model/TypedDataContext"; export interface ContextModule { + getContext(field: TransactionFieldContext): Promise; getContexts(transaction: TransactionContext): Promise; getTypedDataFilters( typedData: TypedDataContext, diff --git a/packages/signer/context-module/src/DefaultContextModule.test.ts b/packages/signer/context-module/src/DefaultContextModule.test.ts index 0a8469977..698368b1b 100644 --- a/packages/signer/context-module/src/DefaultContextModule.test.ts +++ b/packages/signer/context-module/src/DefaultContextModule.test.ts @@ -1,11 +1,14 @@ import { type ContextModuleConfig } from "./config/model/ContextModuleConfig"; -import { type TransactionContext } from "./shared/model/TransactionContext"; +import { + type TransactionContext, + type TransactionFieldContext, +} from "./shared/model/TransactionContext"; import { type TypedDataContext } from "./shared/model/TypedDataContext"; import type { TypedDataContextLoader } from "./typed-data/domain/TypedDataContextLoader"; import { DefaultContextModule } from "./DefaultContextModule"; const contextLoaderStubBuilder = () => { - return { load: jest.fn() }; + return { load: jest.fn(), loadField: jest.fn() }; }; describe("DefaultContextModule", () => { @@ -87,4 +90,63 @@ describe("DefaultContextModule", () => { expect(typedDataLoader.load).toHaveBeenCalledTimes(1); }); + + it("should return a single context", async () => { + const loader = contextLoaderStubBuilder(); + const responses = [null, { type: "token", payload: "payload" }]; + jest + .spyOn(loader, "loadField") + .mockResolvedValueOnce(responses[0]) + .mockResolvedValueOnce(responses[1]); + const contextModule = new DefaultContextModule({ + loaders: [loader, { load: jest.fn() }, loader], + typedDataLoader, + }); + + const res = await contextModule.getContext({ + type: "token", + } as TransactionFieldContext); + + expect(loader.loadField).toHaveBeenCalledTimes(2); + expect(res).toEqual({ type: "token", payload: "payload" }); + }); + + it("context field not supported", async () => { + const loader = contextLoaderStubBuilder(); + const responses = [null, null]; + jest + .spyOn(loader, "loadField") + .mockResolvedValueOnce(responses[0]) + .mockResolvedValueOnce(responses[1]); + const contextModule = new DefaultContextModule({ + loaders: [loader, { load: jest.fn() }, loader], + typedDataLoader, + }); + + const res = await contextModule.getContext({ + type: "token", + } as TransactionFieldContext); + + expect(loader.loadField).toHaveBeenCalledTimes(2); + expect(res).toEqual({ + type: "error", + error: new Error("Field type not supported: token"), + }); + }); + + it("getField not implemented", async () => { + const contextModule = new DefaultContextModule({ + loaders: [{ load: jest.fn() }], + typedDataLoader, + }); + + const res = await contextModule.getContext({ + type: "token", + } as TransactionFieldContext); + + expect(res).toEqual({ + type: "error", + error: new Error("Field type not supported: token"), + }); + }); }); diff --git a/packages/signer/context-module/src/DefaultContextModule.ts b/packages/signer/context-module/src/DefaultContextModule.ts index e8827ab37..c45749600 100644 --- a/packages/signer/context-module/src/DefaultContextModule.ts +++ b/packages/signer/context-module/src/DefaultContextModule.ts @@ -12,8 +12,14 @@ import { type ForwardDomainContextLoader } from "./forward-domain/domain/Forward import { nftTypes } from "./nft/di/nftTypes"; import { type NftContextLoader } from "./nft/domain/NftContextLoader"; import { type ContextLoader } from "./shared/domain/ContextLoader"; -import { type ClearSignContext } from "./shared/model/ClearSignContext"; -import { type TransactionContext } from "./shared/model/TransactionContext"; +import { + type ClearSignContext, + ClearSignContextType, +} from "./shared/model/ClearSignContext"; +import { + type TransactionContext, + type TransactionFieldContext, +} from "./shared/model/TransactionContext"; import { tokenTypes } from "./token/di/tokenTypes"; import { type TokenContextLoader } from "./token/domain/TokenContextLoader"; import { type TransactionContextLoader } from "./transaction/domain/TransactionContextLoader"; @@ -65,6 +71,21 @@ export class DefaultContextModule implements ContextModule { return responses.flat(); } + public async getContext( + field: TransactionFieldContext, + ): Promise { + const promises = this._loaders + .filter((fetcher) => fetcher.loadField) + .map((fetcher) => fetcher.loadField!(field)); + const responses = await Promise.all(promises); + return ( + responses.find((resp) => resp !== null) || { + type: ClearSignContextType.ERROR, + error: new Error(`Field type not supported: ${field.type}`), + } + ); + } + public async getTypedDataFilters( typedData: TypedDataContext, ): Promise { diff --git a/packages/signer/context-module/src/nft/domain/NftContextLoader.test.ts b/packages/signer/context-module/src/nft/domain/NftContextLoader.test.ts index 0a43c8f54..5d20240f1 100644 --- a/packages/signer/context-module/src/nft/domain/NftContextLoader.test.ts +++ b/packages/signer/context-module/src/nft/domain/NftContextLoader.test.ts @@ -3,7 +3,10 @@ import { Left, Right } from "purify-ts"; import { type NftDataSource } from "@/nft/data/NftDataSource"; import { NftContextLoader } from "@/nft/domain/NftContextLoader"; import { ClearSignContextType } from "@/shared/model/ClearSignContext"; -import { type TransactionContext } from "@/shared/model/TransactionContext"; +import { + type TransactionContext, + type TransactionFieldContext, +} from "@/shared/model/TransactionContext"; describe("NftContextLoader", () => { const spyGetNftInfosPayload = jest.fn(); @@ -135,4 +138,56 @@ describe("NftContextLoader", () => { ]); }); }); + + describe("loadField function", () => { + it("should return an error when field type if not supported", async () => { + const field: TransactionFieldContext = { + type: ClearSignContextType.TOKEN, + chainId: 7, + address: "0x1234", + }; + + const result = await loader.loadField(field); + + expect(result).toEqual(null); + }); + + it("should return a payload", async () => { + // GIVEN + const field: TransactionFieldContext = { + type: ClearSignContextType.NFT, + chainId: 7, + address: "0x1234", + }; + + // WHEN + spyGetNftInfosPayload.mockResolvedValueOnce(Right("payload")); + const result = await loader.loadField(field); + + // THEN + expect(result).toEqual({ + type: ClearSignContextType.NFT, + payload: "payload", + }); + }); + + it("should return an error when unable to fetch the datasource", async () => { + // GIVEN + const field: TransactionFieldContext = { + type: ClearSignContextType.NFT, + chainId: 7, + address: "0x1234", + }; + + // WHEN + spyGetNftInfosPayload.mockResolvedValueOnce(Left(new Error("error"))); + const result = await loader.loadField(field); + + // THEN + expect(result).toEqual({ + type: ClearSignContextType.ERROR, + error: new Error("error"), + }); + }); + }); }); diff --git a/packages/signer/context-module/src/nft/domain/NftContextLoader.ts b/packages/signer/context-module/src/nft/domain/NftContextLoader.ts index 037d852a6..a9a7427ca 100644 --- a/packages/signer/context-module/src/nft/domain/NftContextLoader.ts +++ b/packages/signer/context-module/src/nft/domain/NftContextLoader.ts @@ -8,7 +8,10 @@ import { ClearSignContext, ClearSignContextType, } from "@/shared/model/ClearSignContext"; -import { TransactionContext } from "@/shared/model/TransactionContext"; +import { + TransactionContext, + TransactionFieldContext, +} from "@/shared/model/TransactionContext"; enum ERC721_SUPPORTED_SELECTOR { Approve = "0x095ea7b3", @@ -112,6 +115,28 @@ export class NftContextLoader implements ContextLoader { return responses; } + async loadField( + field: TransactionFieldContext, + ): Promise { + if (field.type !== ClearSignContextType.NFT) { + return null; + } + const payload = await this._dataSource.getNftInfosPayload({ + address: field.address, + chainId: field.chainId, + }); + return payload.caseOf({ + Left: (error): ClearSignContext => ({ + type: ClearSignContextType.ERROR, + error, + }), + Right: (value): ClearSignContext => ({ + type: ClearSignContextType.NFT, + payload: value, + }), + }); + } + private isSelectorSupported(selector: HexaString) { return Object.values(SUPPORTED_SELECTORS).includes(selector); } diff --git a/packages/signer/context-module/src/shared/domain/ContextLoader.ts b/packages/signer/context-module/src/shared/domain/ContextLoader.ts index 6f6a2ef81..6c17133fc 100644 --- a/packages/signer/context-module/src/shared/domain/ContextLoader.ts +++ b/packages/signer/context-module/src/shared/domain/ContextLoader.ts @@ -1,6 +1,12 @@ import { type ClearSignContext } from "@/shared/model/ClearSignContext"; -import { type TransactionContext } from "@/shared/model/TransactionContext"; +import { + type TransactionContext, + type TransactionFieldContext, +} from "@/shared/model/TransactionContext"; export type ContextLoader = { load: (transaction: TransactionContext) => Promise; + loadField?: ( + field: TransactionFieldContext, + ) => Promise; }; diff --git a/packages/signer/context-module/src/shared/model/TransactionContext.ts b/packages/signer/context-module/src/shared/model/TransactionContext.ts index a4268c8f2..1271380a1 100644 --- a/packages/signer/context-module/src/shared/model/TransactionContext.ts +++ b/packages/signer/context-module/src/shared/model/TransactionContext.ts @@ -1,5 +1,21 @@ +import { type ClearSignContextType } from "@/shared/model/ClearSignContext"; import { type TransactionSubset } from "@/shared/model/TransactionSubset"; +export type TransactionFieldContext = + | { + type: ClearSignContextType.TOKEN | ClearSignContextType.NFT; + chainId: number; + address: string; + } + | { + type: ClearSignContextType.TRUSTED_NAME; + chainId: number; + address: string; + challenge: string; + types: string[]; + sources: string[]; + }; + export type TransactionContext = TransactionSubset & { challenge: string; domain?: string; diff --git a/packages/signer/context-module/src/token/domain/TokenContextLoader.test.ts b/packages/signer/context-module/src/token/domain/TokenContextLoader.test.ts index d88c6ba06..9030e68b2 100644 --- a/packages/signer/context-module/src/token/domain/TokenContextLoader.test.ts +++ b/packages/signer/context-module/src/token/domain/TokenContextLoader.test.ts @@ -1,7 +1,10 @@ import { Left, Right } from "purify-ts"; import { ClearSignContextType } from "@/shared/model/ClearSignContext"; -import { type TransactionContext } from "@/shared/model/TransactionContext"; +import { + type TransactionContext, + type TransactionFieldContext, +} from "@/shared/model/TransactionContext"; import { type TokenDataSource } from "@/token/data/TokenDataSource"; import { TokenContextLoader } from "@/token/domain/TokenContextLoader"; @@ -133,4 +136,60 @@ describe("TokenContextLoader", () => { ]); }); }); + + describe("loadField function", () => { + it("should return an error when field type if not supported", async () => { + const field: TransactionFieldContext = { + type: ClearSignContextType.NFT, + chainId: 7, + address: "0x1234", + }; + + const result = await loader.loadField(field); + + expect(result).toEqual(null); + }); + + it("should return a payload", async () => { + // GIVEN + const field: TransactionFieldContext = { + type: ClearSignContextType.TOKEN, + chainId: 7, + address: "0x1234", + }; + + // WHEN + jest + .spyOn(mockTokenDataSource, "getTokenInfosPayload") + .mockResolvedValue(Right("payload")); + const result = await loader.loadField(field); + + // THEN + expect(result).toEqual({ + type: ClearSignContextType.TOKEN, + payload: "payload", + }); + }); + + it("should return an error when unable to fetch the datasource", async () => { + // GIVEN + const field: TransactionFieldContext = { + type: ClearSignContextType.TOKEN, + chainId: 7, + address: "0x1234", + }; + + // WHEN + jest + .spyOn(mockTokenDataSource, "getTokenInfosPayload") + .mockResolvedValue(Left(new Error("error"))); + const result = await loader.loadField(field); + + // THEN + expect(result).toEqual({ + type: ClearSignContextType.ERROR, + error: new Error("error"), + }); + }); + }); }); diff --git a/packages/signer/context-module/src/token/domain/TokenContextLoader.ts b/packages/signer/context-module/src/token/domain/TokenContextLoader.ts index e9fa034a8..73aa56558 100644 --- a/packages/signer/context-module/src/token/domain/TokenContextLoader.ts +++ b/packages/signer/context-module/src/token/domain/TokenContextLoader.ts @@ -6,7 +6,10 @@ import { ClearSignContext, ClearSignContextType, } from "@/shared/model/ClearSignContext"; -import { TransactionContext } from "@/shared/model/TransactionContext"; +import { + TransactionContext, + TransactionFieldContext, +} from "@/shared/model/TransactionContext"; import type { TokenDataSource } from "@/token/data/TokenDataSource"; import { tokenTypes } from "@/token/di/tokenTypes"; @@ -66,6 +69,28 @@ export class TokenContextLoader implements ContextLoader { ]; } + async loadField( + field: TransactionFieldContext, + ): Promise { + if (field.type !== ClearSignContextType.TOKEN) { + return null; + } + const payload = await this._dataSource.getTokenInfosPayload({ + address: field.address, + chainId: field.chainId, + }); + return payload.caseOf({ + Left: (error): ClearSignContext => ({ + type: ClearSignContextType.ERROR, + error, + }), + Right: (value): ClearSignContext => ({ + type: ClearSignContextType.TOKEN, + payload: value, + }), + }); + } + private isSelectorSupported(selector: HexaString) { return Object.values(SUPPORTED_SELECTORS).includes(selector); } 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 154dc7ca4..02b294118 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 @@ -43,6 +43,7 @@ describe("EthAppBinder", () => { executeDeviceAction: jest.fn(), } as unknown as DeviceManagementKit; const mockedContextModule: ContextModule = { + getContext: jest.fn(), getContexts: jest.fn(), getTypedDataFilters: jest.fn(), }; 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 d6c2b374e..b58b8c465 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 @@ -30,6 +30,7 @@ jest.mock( describe("SignTransactionDeviceAction", () => { const contextModuleMock: ContextModule = { + getContext: jest.fn(), getContexts: jest.fn(), getTypedDataFilters: jest.fn(), }; 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 72c112af0..75f708b38 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 @@ -88,6 +88,7 @@ describe("SignTypedDataDeviceAction", () => { parse: jest.fn(), }; const mockContextModule: ContextModule = { + getContext: jest.fn(), getContexts: jest.fn(), getTypedDataFilters: jest.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 655ffc6cb..2dd2c1b81 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 @@ -18,6 +18,7 @@ import { BuildEIP712ContextTask } from "./BuildEIP712ContextTask"; describe("BuildEIP712ContextTask", () => { const apiMock = makeDeviceActionInternalApiMock(); const contextMouleMock = { + getContext: jest.fn(), getContexts: jest.fn(), getTypedDataFilters: jest.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 52e19ac59..2b8cc2644 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 @@ -15,6 +15,7 @@ import { describe("BuildTransactionContextTask", () => { const contextModuleMock = { + getContext: jest.fn(), getContexts: jest.fn(), getTypedDataFilters: jest.fn(), }; From fa9c3d61735f7c2aaf2d00a878eeba7ece16078a Mon Sep 17 00:00:00 2001 From: Pierre Aoun Date: Wed, 20 Nov 2024 11:52:58 +0100 Subject: [PATCH 2/2] :sparkles: (context-module): Implement trusted names provider --- .changeset/fifty-taxis-kiss.md | 5 + .../src/DefaultContextModule.test.ts | 12 +- .../src/DefaultContextModule.ts | 8 +- packages/signer/context-module/src/di.ts | 4 +- .../data/ForwardDomainDataSource.ts | 12 -- .../data/HttpForwardDomainDataSource.test.ts | 93 --------- .../data/HttpForwardDomainDataSource.ts | 41 ---- .../di/forwardDomainModuleFactory.ts | 15 -- .../forward-domain/di/forwardDomainTypes.ts | 4 - .../domain/ForwardDomainContextLoader.test.ts | 112 ---------- packages/signer/context-module/src/index.ts | 2 +- .../src/shared/model/ClearSignContext.ts | 1 - .../data/HttpTrustedNameDataSource.test.ts | 193 ++++++++++++++++++ .../data/HttpTrustedNameDataSource.ts | 98 +++++++++ .../data/TrustedNameDataSource.ts | 23 +++ .../src/trusted-name/data/TrustedNameDto.ts | 18 ++ .../di/trustedNameModuleFactory.ts | 13 ++ .../src/trusted-name/di/trustedNameTypes.ts | 4 + .../domain/TrustedNameContextLoader.test.ts | 167 +++++++++++++++ .../domain/TrustedNameContextLoader.ts} | 47 ++++- ...t.ts => ProvideTrustedNameCommand.test.ts} | 16 +- ...ommand.ts => ProvideTrustedNameCommand.ts} | 12 +- .../ProvideTransactionContextTask.test.ts | 2 +- .../task/ProvideTransactionContextTask.ts | 15 +- .../ProvideTransactionGenericContextTask.ts | 13 +- 25 files changed, 595 insertions(+), 335 deletions(-) create mode 100644 .changeset/fifty-taxis-kiss.md delete mode 100644 packages/signer/context-module/src/forward-domain/data/ForwardDomainDataSource.ts delete mode 100644 packages/signer/context-module/src/forward-domain/data/HttpForwardDomainDataSource.test.ts delete mode 100644 packages/signer/context-module/src/forward-domain/data/HttpForwardDomainDataSource.ts delete mode 100644 packages/signer/context-module/src/forward-domain/di/forwardDomainModuleFactory.ts delete mode 100644 packages/signer/context-module/src/forward-domain/di/forwardDomainTypes.ts delete mode 100644 packages/signer/context-module/src/forward-domain/domain/ForwardDomainContextLoader.test.ts create mode 100644 packages/signer/context-module/src/trusted-name/data/HttpTrustedNameDataSource.test.ts create mode 100644 packages/signer/context-module/src/trusted-name/data/HttpTrustedNameDataSource.ts create mode 100644 packages/signer/context-module/src/trusted-name/data/TrustedNameDataSource.ts create mode 100644 packages/signer/context-module/src/trusted-name/data/TrustedNameDto.ts create mode 100644 packages/signer/context-module/src/trusted-name/di/trustedNameModuleFactory.ts create mode 100644 packages/signer/context-module/src/trusted-name/di/trustedNameTypes.ts create mode 100644 packages/signer/context-module/src/trusted-name/domain/TrustedNameContextLoader.test.ts rename packages/signer/context-module/src/{forward-domain/domain/ForwardDomainContextLoader.ts => trusted-name/domain/TrustedNameContextLoader.ts} (50%) rename packages/signer/signer-eth/src/internal/app-binder/command/{ProvideDomainNameCommand.test.ts => ProvideTrustedNameCommand.test.ts} (80%) rename packages/signer/signer-eth/src/internal/app-binder/command/{ProvideDomainNameCommand.ts => ProvideTrustedNameCommand.ts} (76%) diff --git a/.changeset/fifty-taxis-kiss.md b/.changeset/fifty-taxis-kiss.md new file mode 100644 index 000000000..12c64b576 --- /dev/null +++ b/.changeset/fifty-taxis-kiss.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/context-module": minor +--- + +Implement transaction fields loader diff --git a/packages/signer/context-module/src/DefaultContextModule.test.ts b/packages/signer/context-module/src/DefaultContextModule.test.ts index 698368b1b..1d77cc87d 100644 --- a/packages/signer/context-module/src/DefaultContextModule.test.ts +++ b/packages/signer/context-module/src/DefaultContextModule.test.ts @@ -99,8 +99,8 @@ describe("DefaultContextModule", () => { .mockResolvedValueOnce(responses[0]) .mockResolvedValueOnce(responses[1]); const contextModule = new DefaultContextModule({ - loaders: [loader, { load: jest.fn() }, loader], - typedDataLoader, + ...defaultContextModuleConfig, + customLoaders: [loader, { load: jest.fn() }, loader], }); const res = await contextModule.getContext({ @@ -119,8 +119,8 @@ describe("DefaultContextModule", () => { .mockResolvedValueOnce(responses[0]) .mockResolvedValueOnce(responses[1]); const contextModule = new DefaultContextModule({ - loaders: [loader, { load: jest.fn() }, loader], - typedDataLoader, + ...defaultContextModuleConfig, + customLoaders: [loader, { load: jest.fn() }, loader], }); const res = await contextModule.getContext({ @@ -136,8 +136,8 @@ describe("DefaultContextModule", () => { it("getField not implemented", async () => { const contextModule = new DefaultContextModule({ - loaders: [{ load: jest.fn() }], - typedDataLoader, + ...defaultContextModuleConfig, + customLoaders: [{ load: jest.fn() }], }); const res = await contextModule.getContext({ diff --git a/packages/signer/context-module/src/DefaultContextModule.ts b/packages/signer/context-module/src/DefaultContextModule.ts index c45749600..11a1ba1ed 100644 --- a/packages/signer/context-module/src/DefaultContextModule.ts +++ b/packages/signer/context-module/src/DefaultContextModule.ts @@ -3,12 +3,11 @@ import { type Container } from "inversify"; import type { TypedDataClearSignContext } from "@/shared/model/TypedDataClearSignContext"; import type { TypedDataContext } from "@/shared/model/TypedDataContext"; import { transactionTypes } from "@/transaction/di/transactionTypes"; +import { trustedNameTypes } from "@/trusted-name/di/trustedNameTypes"; import { type ContextModuleConfig } from "./config/model/ContextModuleConfig"; import { externalPluginTypes } from "./external-plugin/di/externalPluginTypes"; import { type ExternalPluginContextLoader } from "./external-plugin/domain/ExternalPluginContextLoader"; -import { forwardDomainTypes } from "./forward-domain/di/forwardDomainTypes"; -import { type ForwardDomainContextLoader } from "./forward-domain/domain/ForwardDomainContextLoader"; import { nftTypes } from "./nft/di/nftTypes"; import { type NftContextLoader } from "./nft/domain/NftContextLoader"; import { type ContextLoader } from "./shared/domain/ContextLoader"; @@ -23,6 +22,7 @@ import { import { tokenTypes } from "./token/di/tokenTypes"; import { type TokenContextLoader } from "./token/domain/TokenContextLoader"; import { type TransactionContextLoader } from "./transaction/domain/TransactionContextLoader"; +import { type TrustedNameContextLoader } from "./trusted-name/domain/TrustedNameContextLoader"; import { typedDataTypes } from "./typed-data/di/typedDataTypes"; import type { TypedDataContextLoader } from "./typed-data/domain/TypedDataContextLoader"; import { type ContextModule } from "./ContextModule"; @@ -46,8 +46,8 @@ export class DefaultContextModule implements ContextModule { this._container.get( externalPluginTypes.ExternalPluginContextLoader, ), - this._container.get( - forwardDomainTypes.ForwardDomainContextLoader, + this._container.get( + trustedNameTypes.TrustedNameContextLoader, ), this._container.get(nftTypes.NftContextLoader), this._container.get(tokenTypes.TokenContextLoader), diff --git a/packages/signer/context-module/src/di.ts b/packages/signer/context-module/src/di.ts index 85bb93265..99823246f 100644 --- a/packages/signer/context-module/src/di.ts +++ b/packages/signer/context-module/src/di.ts @@ -3,10 +3,10 @@ import { Container } from "inversify"; import { configModuleFactory } from "@/config/di/configModuleFactory"; import { type ContextModuleConfig } from "@/config/model/ContextModuleConfig"; import { externalPluginModuleFactory } from "@/external-plugin/di/externalPluginModuleFactory"; -import { forwardDomainModuleFactory } from "@/forward-domain/di/forwardDomainModuleFactory"; import { nftModuleFactory } from "@/nft/di/nftModuleFactory"; 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"; type MakeContainerArgs = { @@ -19,10 +19,10 @@ export const makeContainer = ({ config }: MakeContainerArgs) => { container.load( configModuleFactory(config), externalPluginModuleFactory(), - forwardDomainModuleFactory(), nftModuleFactory(), tokenModuleFactory(), transactionModuleFactory(), + trustedNameModuleFactory(), typedDataModuleFactory(), ); diff --git a/packages/signer/context-module/src/forward-domain/data/ForwardDomainDataSource.ts b/packages/signer/context-module/src/forward-domain/data/ForwardDomainDataSource.ts deleted file mode 100644 index 38cc220da..000000000 --- a/packages/signer/context-module/src/forward-domain/data/ForwardDomainDataSource.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type Either } from "purify-ts/Either"; - -export type GetForwardDomainInfosParams = { - domain: string; - challenge: string; -}; - -export interface ForwardDomainDataSource { - getDomainNamePayload( - params: GetForwardDomainInfosParams, - ): Promise>; -} diff --git a/packages/signer/context-module/src/forward-domain/data/HttpForwardDomainDataSource.test.ts b/packages/signer/context-module/src/forward-domain/data/HttpForwardDomainDataSource.test.ts deleted file mode 100644 index 85d37d91a..000000000 --- a/packages/signer/context-module/src/forward-domain/data/HttpForwardDomainDataSource.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import axios from "axios"; -import { Left, Right } from "purify-ts"; - -import { type ForwardDomainDataSource } from "@/forward-domain/data/ForwardDomainDataSource"; -import { HttpForwardDomainDataSource } from "@/forward-domain/data/HttpForwardDomainDataSource"; -import PACKAGE from "@root/package.json"; - -jest.mock("axios"); - -describe("HttpForwardDomainDataSource", () => { - let datasource: ForwardDomainDataSource; - - beforeAll(() => { - datasource = new HttpForwardDomainDataSource(); - jest.clearAllMocks(); - }); - - it("should call axios with the ledger client version header", async () => { - // GIVEN - const version = `context-module/${PACKAGE.version}`; - const requestSpy = jest.fn(() => Promise.resolve({ data: [] })); - jest.spyOn(axios, "request").mockImplementation(requestSpy); - - // WHEN - await datasource.getDomainNamePayload({ - challenge: "", - domain: "hello.eth", - }); - - // THEN - expect(requestSpy).toHaveBeenCalledWith( - expect.objectContaining({ - headers: { "X-Ledger-Client-Version": version }, - }), - ); - }); - - it("should return an error when no payload is returned", async () => { - // GIVEN - const response = { data: { test: "" } }; - jest.spyOn(axios, "request").mockResolvedValue(response); - - // WHEN - const result = await datasource.getDomainNamePayload({ - challenge: "", - domain: "hello.eth", - }); - - // THEN - expect(result).toEqual( - Left( - new Error( - "[ContextModule] HttpForwardDomainDataSource: error getting domain payload", - ), - ), - ); - }); - - it("should throw an error when axios throws an error", async () => { - // GIVEN - jest.spyOn(axios, "request").mockRejectedValue(new Error()); - - // WHEN - const result = await datasource.getDomainNamePayload({ - challenge: "", - domain: "hello.eth", - }); - - // THEN - expect(result).toEqual( - Left( - new Error( - "[ContextModule] HttpForwardDomainDataSource: Failed to fetch domain name", - ), - ), - ); - }); - - it("should return a payload", async () => { - // GIVEN - const response = { data: { payload: "payload" } }; - jest.spyOn(axios, "request").mockResolvedValue(response); - - // WHEN - const result = await datasource.getDomainNamePayload({ - challenge: "challenge", - domain: "hello.eth", - }); - - // THEN - expect(result).toEqual(Right("payload")); - }); -}); diff --git a/packages/signer/context-module/src/forward-domain/data/HttpForwardDomainDataSource.ts b/packages/signer/context-module/src/forward-domain/data/HttpForwardDomainDataSource.ts deleted file mode 100644 index 2232552e0..000000000 --- a/packages/signer/context-module/src/forward-domain/data/HttpForwardDomainDataSource.ts +++ /dev/null @@ -1,41 +0,0 @@ -import axios from "axios"; -import { injectable } from "inversify"; -import { Either, Left, Right } from "purify-ts"; - -import { - ForwardDomainDataSource, - GetForwardDomainInfosParams, -} from "@/forward-domain/data/ForwardDomainDataSource"; -import PACKAGE from "@root/package.json"; - -@injectable() -export class HttpForwardDomainDataSource implements ForwardDomainDataSource { - public async getDomainNamePayload({ - domain, - challenge, - }: GetForwardDomainInfosParams): Promise> { - try { - const response = await axios.request<{ payload: string }>({ - method: "GET", - url: `https://nft.api.live.ledger.com/v1/names/ens/forward/${domain}?challenge=${challenge}`, - headers: { - "X-Ledger-Client-Version": `context-module/${PACKAGE.version}`, - }, - }); - - return response.data.payload - ? Right(response.data.payload) - : Left( - new Error( - "[ContextModule] HttpForwardDomainDataSource: error getting domain payload", - ), - ); - } catch (_error) { - return Left( - new Error( - "[ContextModule] HttpForwardDomainDataSource: Failed to fetch domain name", - ), - ); - } - } -} diff --git a/packages/signer/context-module/src/forward-domain/di/forwardDomainModuleFactory.ts b/packages/signer/context-module/src/forward-domain/di/forwardDomainModuleFactory.ts deleted file mode 100644 index 8e1b545e3..000000000 --- a/packages/signer/context-module/src/forward-domain/di/forwardDomainModuleFactory.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ContainerModule } from "inversify"; - -import { HttpForwardDomainDataSource } from "@/forward-domain/data/HttpForwardDomainDataSource"; -import { forwardDomainTypes } from "@/forward-domain/di/forwardDomainTypes"; -import { ForwardDomainContextLoader } from "@/forward-domain/domain/ForwardDomainContextLoader"; - -export const forwardDomainModuleFactory = () => - new ContainerModule((bind, _unbind, _isBound, _rebind) => { - bind(forwardDomainTypes.ForwardDomainDataSource).to( - HttpForwardDomainDataSource, - ); - bind(forwardDomainTypes.ForwardDomainContextLoader).to( - ForwardDomainContextLoader, - ); - }); diff --git a/packages/signer/context-module/src/forward-domain/di/forwardDomainTypes.ts b/packages/signer/context-module/src/forward-domain/di/forwardDomainTypes.ts deleted file mode 100644 index 372d4c9a8..000000000 --- a/packages/signer/context-module/src/forward-domain/di/forwardDomainTypes.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const forwardDomainTypes = { - ForwardDomainDataSource: Symbol.for("forwardDomainDataSource"), - ForwardDomainContextLoader: Symbol.for("forwardDomainContextLoader"), -}; diff --git a/packages/signer/context-module/src/forward-domain/domain/ForwardDomainContextLoader.test.ts b/packages/signer/context-module/src/forward-domain/domain/ForwardDomainContextLoader.test.ts deleted file mode 100644 index 23c95d159..000000000 --- a/packages/signer/context-module/src/forward-domain/domain/ForwardDomainContextLoader.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Left, Right } from "purify-ts"; - -import { type ForwardDomainDataSource } from "@/forward-domain/data/ForwardDomainDataSource"; -import { ForwardDomainContextLoader } from "@/forward-domain/domain/ForwardDomainContextLoader"; -import { ClearSignContextType } from "@/shared/model/ClearSignContext"; -import { type TransactionContext } from "@/shared/model/TransactionContext"; - -describe("ForwardDomainContextLoader", () => { - const mockForwardDomainDataSource: ForwardDomainDataSource = { - getDomainNamePayload: jest.fn(), - }; - - beforeEach(() => { - jest.restoreAllMocks(); - jest - .spyOn(mockForwardDomainDataSource, "getDomainNamePayload") - .mockResolvedValue(Right("payload")); - }); - - describe("load function", () => { - it("should return an empty array when no domain or registry", () => { - const transaction = {} as TransactionContext; - const loader = new ForwardDomainContextLoader( - mockForwardDomainDataSource, - ); - const promise = () => loader.load(transaction); - - expect(promise()).resolves.toEqual([]); - }); - - it("should return an error when domain > max length", async () => { - const transaction = { - domain: "maxlength-maxlength-maxlength-maxlength-maxlength-maxlength", - } as TransactionContext; - - const loader = new ForwardDomainContextLoader( - mockForwardDomainDataSource, - ); - const result = await loader.load(transaction); - - expect(result).toEqual([ - { - type: ClearSignContextType.ERROR, - error: new Error( - "[ContextModule] ForwardDomainLoader: invalid domain", - ), - }, - ]); - }); - - it("should return an error when domain is not valid", async () => { - const transaction = { - domain: "hellođź‘‹", - } as TransactionContext; - - const loader = new ForwardDomainContextLoader( - mockForwardDomainDataSource, - ); - const result = await loader.load(transaction); - - expect(result).toEqual([ - { - type: ClearSignContextType.ERROR, - error: new Error( - "[ContextModule] ForwardDomainLoader: invalid domain", - ), - }, - ]); - }); - - it("should return a payload", async () => { - const transaction = { - domain: "hello.eth", - challenge: "challenge", - } as TransactionContext; - - const loader = new ForwardDomainContextLoader( - mockForwardDomainDataSource, - ); - const result = await loader.load(transaction); - - expect(result).toEqual([ - { - type: ClearSignContextType.DOMAIN_NAME, - payload: "payload", - }, - ]); - }); - - it("should return an error when unable to fetch the datasource", async () => { - // GIVEN - const transaction = { - domain: "hello.eth", - challenge: "challenge", - } as TransactionContext; - - // WHEN - jest - .spyOn(mockForwardDomainDataSource, "getDomainNamePayload") - .mockResolvedValue(Left(new Error("error"))); - const loader = new ForwardDomainContextLoader( - mockForwardDomainDataSource, - ); - const result = await loader.load(transaction); - - // THEN - expect(result).toEqual([ - { type: ClearSignContextType.ERROR, error: new Error("error") }, - ]); - }); - }); -}); diff --git a/packages/signer/context-module/src/index.ts b/packages/signer/context-module/src/index.ts index e94314c2f..a1e9ce2f9 100644 --- a/packages/signer/context-module/src/index.ts +++ b/packages/signer/context-module/src/index.ts @@ -3,7 +3,6 @@ export * from "./ContextModule"; export * from "./ContextModuleBuilder"; export * from "./DefaultContextModule"; export * from "./external-plugin/domain/ExternalPluginContextLoader"; -export * from "./forward-domain/domain/ForwardDomainContextLoader"; export * from "./nft/domain/NftContextLoader"; export * from "./shared/domain/ContextLoader"; export * from "./shared/model/ClearSignContext"; @@ -13,3 +12,4 @@ export * from "./shared/model/TransactionSubset"; export * from "./shared/model/TypedDataClearSignContext"; export * from "./shared/model/TypedDataContext"; export * from "./token/domain/TokenContextLoader"; +export * from "./trusted-name/domain/TrustedNameContextLoader"; diff --git a/packages/signer/context-module/src/shared/model/ClearSignContext.ts b/packages/signer/context-module/src/shared/model/ClearSignContext.ts index 8b90c0dea..b4b3dad42 100644 --- a/packages/signer/context-module/src/shared/model/ClearSignContext.ts +++ b/packages/signer/context-module/src/shared/model/ClearSignContext.ts @@ -3,7 +3,6 @@ import { type GenericPath } from "./GenericPath"; export enum ClearSignContextType { TOKEN = "token", NFT = "nft", - DOMAIN_NAME = "domainName", TRUSTED_NAME = "trustedName", PLUGIN = "plugin", EXTERNAL_PLUGIN = "externalPlugin", diff --git a/packages/signer/context-module/src/trusted-name/data/HttpTrustedNameDataSource.test.ts b/packages/signer/context-module/src/trusted-name/data/HttpTrustedNameDataSource.test.ts new file mode 100644 index 000000000..f30b085b8 --- /dev/null +++ b/packages/signer/context-module/src/trusted-name/data/HttpTrustedNameDataSource.test.ts @@ -0,0 +1,193 @@ +import axios from "axios"; +import { Left, Right } from "purify-ts"; + +import { type ContextModuleConfig } from "@/config/model/ContextModuleConfig"; +import { HttpTrustedNameDataSource } from "@/trusted-name/data/HttpTrustedNameDataSource"; +import { type TrustedNameDataSource } from "@/trusted-name/data/TrustedNameDataSource"; +import PACKAGE from "@root/package.json"; + +jest.mock("axios"); + +describe("HttpTrustedNameDataSource", () => { + let datasource: TrustedNameDataSource; + + beforeAll(() => { + const config = { + cal: { + url: "https://crypto-assets-service.api.ledger.com/v1", + mode: "prod", + branch: "main", + }, + } as ContextModuleConfig; + datasource = new HttpTrustedNameDataSource(config); + jest.clearAllMocks(); + }); + + describe("getDomainNamePayload", () => { + it("should call axios with the ledger client version header", async () => { + // GIVEN + const version = `context-module/${PACKAGE.version}`; + const requestSpy = jest.fn(() => Promise.resolve({ data: [] })); + jest.spyOn(axios, "request").mockImplementation(requestSpy); + + // WHEN + await datasource.getDomainNamePayload({ + challenge: "", + domain: "hello.eth", + }); + + // THEN + expect(requestSpy).toHaveBeenCalledWith( + expect.objectContaining({ + headers: { "X-Ledger-Client-Version": version }, + }), + ); + }); + + it("should throw an error when axios throws an error", async () => { + // GIVEN + jest.spyOn(axios, "request").mockRejectedValue(new Error()); + + // WHEN + const result = await datasource.getDomainNamePayload({ + challenge: "", + domain: "hello.eth", + }); + + // THEN + expect(result).toEqual( + Left( + new Error( + "[ContextModule] HttpTrustedNameDataSource: Failed to fetch domain name", + ), + ), + ); + }); + + it("should return an error when no payload is returned", async () => { + // GIVEN + const response = { data: { test: "" } }; + jest.spyOn(axios, "request").mockResolvedValue(response); + + // WHEN + const result = await datasource.getDomainNamePayload({ + challenge: "", + domain: "hello.eth", + }); + + // THEN + expect(result).toEqual( + Left( + new Error( + "[ContextModule] HttpTrustedNameDataSource: error getting domain payload", + ), + ), + ); + }); + + it("should return a payload", async () => { + // GIVEN + const response = { data: { signedDescriptor: { data: "payload" } } }; + jest.spyOn(axios, "request").mockResolvedValue(response); + + // WHEN + const result = await datasource.getDomainNamePayload({ + challenge: "challenge", + domain: "hello.eth", + }); + + // THEN + expect(result).toEqual(Right("payload")); + }); + }); + + describe("getTrustedNamePayload", () => { + it("should call axios with the ledger client version header", async () => { + // GIVEN + const version = `context-module/${PACKAGE.version}`; + const requestSpy = jest.fn(() => Promise.resolve({ data: [] })); + jest.spyOn(axios, "request").mockImplementation(requestSpy); + + // WHEN + await datasource.getTrustedNamePayload({ + address: "0x1234", + challenge: "", + sources: ["ens"], + types: ["eoa"], + }); + + // THEN + expect(requestSpy).toHaveBeenCalledWith( + expect.objectContaining({ + headers: { "X-Ledger-Client-Version": version }, + }), + ); + }); + + it("should throw an error when axios throws an error", async () => { + // GIVEN + jest.spyOn(axios, "request").mockRejectedValue(new Error()); + + // WHEN + const result = await datasource.getTrustedNamePayload({ + address: "0x1234", + challenge: "", + sources: ["ens"], + types: ["eoa"], + }); + + // THEN + expect(result).toEqual( + Left( + new Error( + "[ContextModule] HttpTrustedNameDataSource: Failed to fetch trusted name", + ), + ), + ); + }); + + it("should return an error when no payload is returned", async () => { + // GIVEN + const response = { data: { test: "" } }; + jest.spyOn(axios, "request").mockResolvedValue(response); + + // WHEN + const result = await datasource.getTrustedNamePayload({ + address: "0x1234", + challenge: "", + sources: ["ens"], + types: ["eoa"], + }); + + // THEN + expect(result).toEqual( + Left( + new Error( + "[ContextModule] HttpTrustedNameDataSource: no trusted name metadata for address 0x1234", + ), + ), + ); + }); + + it("should return a payload", async () => { + // GIVEN + const response = { + data: { + signedDescriptor: { data: "payload", signatures: { prod: "sig" } }, + }, + }; + jest.spyOn(axios, "request").mockResolvedValue(response); + + // WHEN + const result = await datasource.getTrustedNamePayload({ + address: "0x1234", + challenge: "", + sources: ["ens"], + types: ["eoa"], + }); + + // THEN + expect(result).toEqual(Right("payloadsig")); + }); + }); +}); diff --git a/packages/signer/context-module/src/trusted-name/data/HttpTrustedNameDataSource.ts b/packages/signer/context-module/src/trusted-name/data/HttpTrustedNameDataSource.ts new file mode 100644 index 000000000..b5c4841d1 --- /dev/null +++ b/packages/signer/context-module/src/trusted-name/data/HttpTrustedNameDataSource.ts @@ -0,0 +1,98 @@ +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 { + GetDomainNameInfosParams, + GetTrustedNameInfosParams, + TrustedNameDataSource, +} from "@/trusted-name/data/TrustedNameDataSource"; +import PACKAGE from "@root/package.json"; + +import { TrustedNameDto } from "./TrustedNameDto"; + +@injectable() +export class HttpTrustedNameDataSource implements TrustedNameDataSource { + constructor( + @inject(configTypes.Config) private readonly config: ContextModuleConfig, + ) {} + + public async getDomainNamePayload({ + domain, + challenge, + }: GetDomainNameInfosParams): Promise> { + try { + const type = "eoa"; // Externally owned account + const source = "ens"; // Ethereum name service + const response = await axios.request({ + method: "GET", + url: `https://nft.api.live.ledger.com/v2/names/ethereum/1/forward/${domain}?types=${type}&sources=${source}&challenge=${challenge}`, + headers: { + "X-Ledger-Client-Version": `context-module/${PACKAGE.version}`, + }, + }); + + return response.data.signedDescriptor?.data + ? Right(response.data.signedDescriptor.data) + : Left( + new Error( + "[ContextModule] HttpTrustedNameDataSource: error getting domain payload", + ), + ); + } catch (_error) { + return Left( + new Error( + "[ContextModule] HttpTrustedNameDataSource: Failed to fetch domain name", + ), + ); + } + } + + public async getTrustedNamePayload({ + address, + challenge, + sources, + types, + }: GetTrustedNameInfosParams): Promise> { + try { + const response = await axios.request({ + method: "GET", + url: `https://nft.api.live.ledger.com/v2/names/ethereum/1/reverse/${address}?types=${types.join(",")}&sources=${sources.join(",")}&challenge=${challenge}`, + headers: { + "X-Ledger-Client-Version": `context-module/${PACKAGE.version}`, + }, + }); + const trustedName = response.data; + + if ( + !trustedName || + !trustedName.signedDescriptor || + !trustedName.signedDescriptor.data || + !trustedName.signedDescriptor.signatures || + typeof trustedName.signedDescriptor.signatures[this.config.cal.mode] !== + "string" + ) { + return Left( + new Error( + `[ContextModule] HttpTrustedNameDataSource: no trusted name metadata for address ${address}`, + ), + ); + } + + return Right( + [ + trustedName.signedDescriptor.data, + trustedName.signedDescriptor.signatures[this.config.cal.mode], + ].join(""), + ); + } catch (_error) { + return Left( + new Error( + "[ContextModule] HttpTrustedNameDataSource: Failed to fetch trusted name", + ), + ); + } + } +} diff --git a/packages/signer/context-module/src/trusted-name/data/TrustedNameDataSource.ts b/packages/signer/context-module/src/trusted-name/data/TrustedNameDataSource.ts new file mode 100644 index 000000000..d1a92de9b --- /dev/null +++ b/packages/signer/context-module/src/trusted-name/data/TrustedNameDataSource.ts @@ -0,0 +1,23 @@ +import { type Either } from "purify-ts/Either"; + +export type GetDomainNameInfosParams = { + domain: string; + challenge: string; +}; + +export type GetTrustedNameInfosParams = { + address: string; + challenge: string; + types: string[]; + sources: string[]; +}; + +export interface TrustedNameDataSource { + getDomainNamePayload( + params: GetDomainNameInfosParams, + ): Promise>; + + getTrustedNamePayload( + params: GetTrustedNameInfosParams, + ): Promise>; +} diff --git a/packages/signer/context-module/src/trusted-name/data/TrustedNameDto.ts b/packages/signer/context-module/src/trusted-name/data/TrustedNameDto.ts new file mode 100644 index 000000000..8571837d4 --- /dev/null +++ b/packages/signer/context-module/src/trusted-name/data/TrustedNameDto.ts @@ -0,0 +1,18 @@ +export type TrustedNameSignatures = + | { + prod: string; + test?: string; + } + | { + prod?: string; + test: string; + }; + +export type TrustedNameDescriptor = { + data: string; + signatures?: TrustedNameSignatures; +}; + +export type TrustedNameDto = { + signedDescriptor: TrustedNameDescriptor; +}; diff --git a/packages/signer/context-module/src/trusted-name/di/trustedNameModuleFactory.ts b/packages/signer/context-module/src/trusted-name/di/trustedNameModuleFactory.ts new file mode 100644 index 000000000..9a077eca8 --- /dev/null +++ b/packages/signer/context-module/src/trusted-name/di/trustedNameModuleFactory.ts @@ -0,0 +1,13 @@ +import { ContainerModule } from "inversify"; + +import { HttpTrustedNameDataSource } from "@/trusted-name/data/HttpTrustedNameDataSource"; +import { trustedNameTypes } from "@/trusted-name/di/trustedNameTypes"; +import { TrustedNameContextLoader } from "@/trusted-name/domain/TrustedNameContextLoader"; + +export const trustedNameModuleFactory = () => + new ContainerModule((bind, _unbind, _isBound, _rebind) => { + bind(trustedNameTypes.TrustedNameDataSource).to(HttpTrustedNameDataSource); + bind(trustedNameTypes.TrustedNameContextLoader).to( + TrustedNameContextLoader, + ); + }); diff --git a/packages/signer/context-module/src/trusted-name/di/trustedNameTypes.ts b/packages/signer/context-module/src/trusted-name/di/trustedNameTypes.ts new file mode 100644 index 000000000..49f3ee615 --- /dev/null +++ b/packages/signer/context-module/src/trusted-name/di/trustedNameTypes.ts @@ -0,0 +1,4 @@ +export const trustedNameTypes = { + TrustedNameDataSource: Symbol.for("TrustedNameDataSource"), + TrustedNameContextLoader: Symbol.for("TrustedNameContextLoader"), +}; diff --git a/packages/signer/context-module/src/trusted-name/domain/TrustedNameContextLoader.test.ts b/packages/signer/context-module/src/trusted-name/domain/TrustedNameContextLoader.test.ts new file mode 100644 index 000000000..661723ba6 --- /dev/null +++ b/packages/signer/context-module/src/trusted-name/domain/TrustedNameContextLoader.test.ts @@ -0,0 +1,167 @@ +import { Left, Right } from "purify-ts"; + +import { ClearSignContextType } from "@/shared/model/ClearSignContext"; +import { + type TransactionContext, + type TransactionFieldContext, +} from "@/shared/model/TransactionContext"; +import { type TrustedNameDataSource } from "@/trusted-name/data/TrustedNameDataSource"; +import { TrustedNameContextLoader } from "@/trusted-name/domain/TrustedNameContextLoader"; + +describe("TrustedNameContextLoader", () => { + const mockTrustedNameDataSource: TrustedNameDataSource = { + getDomainNamePayload: jest.fn(), + getTrustedNamePayload: jest.fn(), + }; + + beforeEach(() => { + jest.restoreAllMocks(); + jest + .spyOn(mockTrustedNameDataSource, "getDomainNamePayload") + .mockResolvedValue(Right("payload")); + }); + + describe("load function", () => { + it("should return an empty array when no domain or registry", () => { + const transaction = {} as TransactionContext; + const loader = new TrustedNameContextLoader(mockTrustedNameDataSource); + const promise = () => loader.load(transaction); + + expect(promise()).resolves.toEqual([]); + }); + + it("should return an error when domain > max length", async () => { + const transaction = { + domain: "maxlength-maxlength-maxlength-maxlength-maxlength-maxlength", + } as TransactionContext; + + const loader = new TrustedNameContextLoader(mockTrustedNameDataSource); + const result = await loader.load(transaction); + + expect(result).toEqual([ + { + type: ClearSignContextType.ERROR, + error: new Error("[ContextModule] TrustedNameLoader: invalid domain"), + }, + ]); + }); + + it("should return an error when domain is not valid", async () => { + const transaction = { + domain: "hellođź‘‹", + } as TransactionContext; + + const loader = new TrustedNameContextLoader(mockTrustedNameDataSource); + const result = await loader.load(transaction); + + expect(result).toEqual([ + { + type: ClearSignContextType.ERROR, + error: new Error("[ContextModule] TrustedNameLoader: invalid domain"), + }, + ]); + }); + + it("should return a payload", async () => { + const transaction = { + domain: "hello.eth", + challenge: "challenge", + } as TransactionContext; + + const loader = new TrustedNameContextLoader(mockTrustedNameDataSource); + const result = await loader.load(transaction); + + expect(result).toEqual([ + { + type: ClearSignContextType.TRUSTED_NAME, + payload: "payload", + }, + ]); + }); + + it("should return an error when unable to fetch the datasource", async () => { + // GIVEN + const transaction = { + domain: "hello.eth", + challenge: "challenge", + } as TransactionContext; + + // WHEN + jest + .spyOn(mockTrustedNameDataSource, "getDomainNamePayload") + .mockResolvedValue(Left(new Error("error"))); + const loader = new TrustedNameContextLoader(mockTrustedNameDataSource); + const result = await loader.load(transaction); + + // THEN + expect(result).toEqual([ + { type: ClearSignContextType.ERROR, error: new Error("error") }, + ]); + }); + }); + + describe("loadField function", () => { + it("should return an error when field type if not supported", async () => { + const field: TransactionFieldContext = { + type: ClearSignContextType.TOKEN, + chainId: 7, + address: "0x1234", + }; + + const loader = new TrustedNameContextLoader(mockTrustedNameDataSource); + const result = await loader.loadField(field); + + expect(result).toEqual(null); + }); + + it("should return a payload", async () => { + // GIVEN + const field: TransactionFieldContext = { + type: ClearSignContextType.TRUSTED_NAME, + chainId: 7, + address: "0x1234", + challenge: "17", + sources: ["ens"], + types: ["eoa"], + }; + + // WHEN + jest + .spyOn(mockTrustedNameDataSource, "getTrustedNamePayload") + .mockResolvedValue(Right("payload")); + const loader = new TrustedNameContextLoader(mockTrustedNameDataSource); + const result = await loader.loadField(field); + + // THEN + expect(result).toEqual({ + type: ClearSignContextType.TRUSTED_NAME, + payload: "payload", + }); + }); + + it("should return an error when unable to fetch the datasource", async () => { + // GIVEN + const field: TransactionFieldContext = { + type: ClearSignContextType.TRUSTED_NAME, + chainId: 7, + address: "0x1234", + challenge: "17", + sources: ["ens"], + types: ["eoa"], + }; + + // WHEN + jest + .spyOn(mockTrustedNameDataSource, "getTrustedNamePayload") + .mockResolvedValue(Left(new Error("error"))); + const loader = new TrustedNameContextLoader(mockTrustedNameDataSource); + const result = await loader.loadField(field); + + // THEN + expect(result).toEqual({ + type: ClearSignContextType.ERROR, + error: new Error("error"), + }); + }); + }); +}); diff --git a/packages/signer/context-module/src/forward-domain/domain/ForwardDomainContextLoader.ts b/packages/signer/context-module/src/trusted-name/domain/TrustedNameContextLoader.ts similarity index 50% rename from packages/signer/context-module/src/forward-domain/domain/ForwardDomainContextLoader.ts rename to packages/signer/context-module/src/trusted-name/domain/TrustedNameContextLoader.ts index 769f548c6..f09e425fb 100644 --- a/packages/signer/context-module/src/forward-domain/domain/ForwardDomainContextLoader.ts +++ b/packages/signer/context-module/src/trusted-name/domain/TrustedNameContextLoader.ts @@ -1,21 +1,24 @@ import { inject, injectable } from "inversify"; -import type { ForwardDomainDataSource } from "@/forward-domain/data/ForwardDomainDataSource"; -import { forwardDomainTypes } from "@/forward-domain/di/forwardDomainTypes"; import { ContextLoader } from "@/shared/domain/ContextLoader"; import { ClearSignContext, ClearSignContextType, } from "@/shared/model/ClearSignContext"; -import { TransactionContext } from "@/shared/model/TransactionContext"; +import { + TransactionContext, + TransactionFieldContext, +} from "@/shared/model/TransactionContext"; +import type { TrustedNameDataSource } from "@/trusted-name/data/TrustedNameDataSource"; +import { trustedNameTypes } from "@/trusted-name/di/trustedNameTypes"; @injectable() -export class ForwardDomainContextLoader implements ContextLoader { - private _dataSource: ForwardDomainDataSource; +export class TrustedNameContextLoader implements ContextLoader { + private _dataSource: TrustedNameDataSource; constructor( - @inject(forwardDomainTypes.ForwardDomainDataSource) - dataSource: ForwardDomainDataSource, + @inject(trustedNameTypes.TrustedNameDataSource) + dataSource: TrustedNameDataSource, ) { this._dataSource = dataSource; } @@ -33,9 +36,7 @@ export class ForwardDomainContextLoader implements ContextLoader { return [ { type: ClearSignContextType.ERROR, - error: new Error( - "[ContextModule] ForwardDomainLoader: invalid domain", - ), + error: new Error("[ContextModule] TrustedNameLoader: invalid domain"), }, ]; } @@ -52,13 +53,37 @@ export class ForwardDomainContextLoader implements ContextLoader { error: error, }), Right: (value): ClearSignContext => ({ - type: ClearSignContextType.DOMAIN_NAME, + type: ClearSignContextType.TRUSTED_NAME, payload: value, }), }), ]; } + async loadField( + field: TransactionFieldContext, + ): Promise { + if (field.type !== ClearSignContextType.TRUSTED_NAME) { + return null; + } + const payload = await this._dataSource.getTrustedNamePayload({ + address: field.address, + challenge: field.challenge, + types: field.types, + sources: field.sources, + }); + return payload.caseOf({ + Left: (error): ClearSignContext => ({ + type: ClearSignContextType.ERROR, + error, + }), + Right: (value): ClearSignContext => ({ + type: ClearSignContextType.TRUSTED_NAME, + payload: value, + }), + }); + } + private isDomainValid(domain: string) { const lengthIsValid = domain.length > 0 && Number(domain.length) < 30; const containsOnlyValidChars = new RegExp("^[a-zA-Z0-9\\-\\_\\.]+$").test( diff --git a/packages/signer/signer-eth/src/internal/app-binder/command/ProvideDomainNameCommand.test.ts b/packages/signer/signer-eth/src/internal/app-binder/command/ProvideTrustedNameCommand.test.ts similarity index 80% rename from packages/signer/signer-eth/src/internal/app-binder/command/ProvideDomainNameCommand.test.ts rename to packages/signer/signer-eth/src/internal/app-binder/command/ProvideTrustedNameCommand.test.ts index a7c992138..4ccf5305c 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/command/ProvideDomainNameCommand.test.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/command/ProvideTrustedNameCommand.test.ts @@ -4,24 +4,24 @@ import { } from "@ledgerhq/device-management-kit"; import { - ProvideDomainNameCommand, - type ProvideDomainNameCommandArgs, -} from "./ProvideDomainNameCommand"; + ProvideTrustedNameCommand, + type ProvideTrustedNameCommandArgs, +} from "./ProvideTrustedNameCommand"; const FIRST_CHUNK_APDU = Uint8Array.from([ 0xe0, 0x22, 0x01, 0x00, 0x08, 0x00, 0x06, 0x4c, 0x65, 0x64, 0x67, 0x65, 0x72, ]); -describe("ProvideDomainNameCommand", () => { +describe("ProvideTrustedNameCommand", () => { describe("getApdu", () => { it("should return the raw APDU", () => { // GIVEN - const args: ProvideDomainNameCommandArgs = { + const args: ProvideTrustedNameCommandArgs = { data: FIRST_CHUNK_APDU.slice(5), isFirstChunk: true, }; // WHEN - const command = new ProvideDomainNameCommand(args); + const command = new ProvideTrustedNameCommand(args); const apdu = command.getApdu(); // THEN expect(apdu.getRawApdu()).toStrictEqual(FIRST_CHUNK_APDU); @@ -36,7 +36,7 @@ describe("ProvideDomainNameCommand", () => { statusCode: Buffer.from([0x6a, 0x80]), // Invalid status code }; // WHEN - const command = new ProvideDomainNameCommand({ + const command = new ProvideTrustedNameCommand({ data: Uint8Array.from([]), isFirstChunk: true, }); @@ -52,7 +52,7 @@ describe("ProvideDomainNameCommand", () => { statusCode: Buffer.from([0x90, 0x00]), // Success status code }; // WHEN - const command = new ProvideDomainNameCommand({ + const command = new ProvideTrustedNameCommand({ data: Uint8Array.from([]), isFirstChunk: true, }); diff --git a/packages/signer/signer-eth/src/internal/app-binder/command/ProvideDomainNameCommand.ts b/packages/signer/signer-eth/src/internal/app-binder/command/ProvideTrustedNameCommand.ts similarity index 76% rename from packages/signer/signer-eth/src/internal/app-binder/command/ProvideDomainNameCommand.ts rename to packages/signer/signer-eth/src/internal/app-binder/command/ProvideTrustedNameCommand.ts index 850605a9b..7ee2b4a16 100644 --- a/packages/signer/signer-eth/src/internal/app-binder/command/ProvideDomainNameCommand.ts +++ b/packages/signer/signer-eth/src/internal/app-binder/command/ProvideTrustedNameCommand.ts @@ -1,4 +1,4 @@ -// https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#provide-domain-name +// https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#provide-trusted-name import { type Apdu, ApduBuilder, @@ -11,7 +11,7 @@ import { GlobalCommandErrorHandler, } from "@ledgerhq/device-management-kit"; -export type ProvideDomainNameCommandArgs = { +export type ProvideTrustedNameCommandArgs = { data: Uint8Array; isFirstChunk: boolean; }; @@ -22,12 +22,12 @@ export type ProvideDomainNameCommandArgs = { export const PAYLOAD_LENGTH_BYTES = 2; /** - * The command that provides a chunk of the domain name to the device. + * The command that provides a chunk of the trusted name to the device. */ -export class ProvideDomainNameCommand - implements Command +export class ProvideTrustedNameCommand + implements Command { - constructor(private readonly args: ProvideDomainNameCommandArgs) {} + constructor(private readonly args: ProvideTrustedNameCommandArgs) {} getApdu(): Apdu { const apduBuilderArgs: ApduBuilderArgs = { 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 f3565510f..e55ad13bd 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 @@ -60,7 +60,7 @@ describe("ProvideTransactionContextTask", () => { afterEach(() => { jest.restoreAllMocks(); }); - it("should send relative commands when receiving ClearSignContexts of type not domainName", async () => { + it("should send relative commands when receiving ClearSignContexts of type not trustedName", async () => { api.sendCommand.mockResolvedValue(successResult); // GIVEN const task = new ProvideTransactionContextTask(api, args); 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 07a4c405a..a4366f591 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 @@ -12,7 +12,6 @@ import { } from "@ledgerhq/device-management-kit"; import { Just, type Maybe, Nothing } from "purify-ts"; -import { ProvideDomainNameCommand } from "@internal/app-binder/command/ProvideDomainNameCommand"; import { ProvideNFTInformationCommand, type ProvideNFTInformationCommandErrorCodes, @@ -21,6 +20,7 @@ import { ProvideTokenInformationCommand, type ProvideTokenInformationCommandResponse, } from "@internal/app-binder/command/ProvideTokenInformationCommand"; +import { ProvideTrustedNameCommand } from "@internal/app-binder/command/ProvideTrustedNameCommand"; import { SetExternalPluginCommand, type SetExternalPluginCommandErrorCodes, @@ -56,9 +56,9 @@ export type ProvideTransactionContextTaskErrorCodes = * - `SetExternalPluginCommand` (single command) * - `ProvideNFTInformationCommand` (single command) * - `ProvideTokenInformationCommand` (single command) - * - `ProvideDomainNameCommand` (__mulpitle commands__) + * - `ProvideTrustedNameCommand` (__mulpitle commands__) * - * The method `provideDomainNameTask` is dedicated to send the multiple `ProvideDomainNameCommand`. + * The method `provideTrustedNameTask` is dedicated to send the multiple `ProvideTrustedNameCommand`. */ export class ProvideTransactionContextTask { constructor( @@ -80,10 +80,10 @@ export class ProvideTransactionContextTask { /** * This method will send a command according to the clear sign context type and return the command result if only one command - * is sent, otherwise it will return the result of the `provideDomainNameTask`. + * is sent, otherwise it will return the result of the `provideTrustedNameTask`. * * @param context The clear sign context to provide. - * @returns A promise that resolves when the command is sent or result of the `provideDomainNameTask`. + * @returns A promise that resolves when the command is sent or result of the `provideTrustedNameTask`. */ async provideContext({ type, @@ -113,18 +113,17 @@ export class ProvideTransactionContextTask { new ProvideTokenInformationCommand({ payload }), ); } - case ClearSignContextType.DOMAIN_NAME: { + case ClearSignContextType.TRUSTED_NAME: { return this.sendInChunks( payload, (args) => - new ProvideDomainNameCommand({ + new ProvideTrustedNameCommand({ data: args.chunkedData, isFirstChunk: args.isFirstChunk, }), ); } case ClearSignContextType.ENUM: - case ClearSignContextType.TRUSTED_NAME: case ClearSignContextType.TRANSACTION_FIELD_DESCRIPTION: case ClearSignContextType.TRANSACTION_INFO: { return CommandResultFactory({ 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 5c70d2892..b124e4116 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 @@ -12,7 +12,6 @@ import { } from "@ledgerhq/device-management-kit"; import { Just, type Maybe, Nothing } from "purify-ts"; -import { ProvideDomainNameCommand } from "@internal/app-binder/command/ProvideDomainNameCommand"; import { ProvideEnumCommand } from "@internal/app-binder/command/ProvideEnumCommand"; import { ProvideNFTInformationCommand, @@ -24,6 +23,7 @@ import { } from "@internal/app-binder/command/ProvideTokenInformationCommand"; import { ProvideTransactionFieldDescriptionCommand } from "@internal/app-binder/command/ProvideTransactionFieldDescriptionCommand"; import { ProvideTransactionInformationCommand } from "@internal/app-binder/command/ProvideTransactionInformationCommand"; +import { ProvideTrustedNameCommand } from "@internal/app-binder/command/ProvideTrustedNameCommand"; import { SetPluginCommand, type SetPluginCommandErrorCodes, @@ -143,11 +143,11 @@ export class ProvideTransactionGenericContextTask { new ProvideTokenInformationCommand({ payload }), ); } - case ClearSignContextType.DOMAIN_NAME: { + case ClearSignContextType.TRUSTED_NAME: { return this.sendInChunks( payload, (args) => - new ProvideDomainNameCommand({ + new ProvideTrustedNameCommand({ data: args.chunkedData, isFirstChunk: args.isFirstChunk, }), @@ -183,13 +183,6 @@ export class ProvideTransactionGenericContextTask { }), ); } - case ClearSignContextType.TRUSTED_NAME: { - return CommandResultFactory({ - error: new InvalidStatusWordError( - "The context type [TRUSTED_NAME] is not implemented yet", - ), - }); - } case ClearSignContextType.EXTERNAL_PLUGIN: { return CommandResultFactory({ error: new InvalidStatusWordError(