Skip to content

Commit

Permalink
Merge pull request #6789 from LedgerHQ/feat/dsdk-285-context-module-nft
Browse files Browse the repository at this point in the history
Feat/dsdk 285 create context module nft loaders
  • Loading branch information
aussedatlo committed May 16, 2024
2 parents 0d0564e + 550ff5f commit e541abf
Show file tree
Hide file tree
Showing 8 changed files with 433 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type GetForwardDomainInfosParams = {
domain: string;
challenge: string;
};

export interface ForwardDomainDataSource {
getDomainNamePayload(params: GetForwardDomainInfosParams): Promise<string>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import axios from "axios";
import { ForwardDomainDataSource, GetForwardDomainInfosParams } from "./ForwardDomainDataSource";

export class HttpForwardDomainDataSource implements ForwardDomainDataSource {
public async getDomainNamePayload({
domain,
challenge,
}: GetForwardDomainInfosParams): Promise<string> {
const response = await axios.request<{ payload: string }>({
method: "GET",
url: `https://nft.api.live.ledger.com/v1/names/ens/forward/${domain}?challenge=${challenge}`,
});

return response.data.payload;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { LoaderOptions } from "../../shared/model/LoaderOptions";
import { Transaction } from "../../shared/model/Transaction";
import { ForwardDomainContextLoader } from "./ForwardDomainContextLoader";

describe("ForwardDomainContextLoader", () => {
const transaction = {} as Transaction;

beforeEach(() => {
jest.restoreAllMocks();
});

describe("load function", () => {
it("should return an empty array when no domain or registry", async () => {
const options = {} as LoaderOptions;

const loader = new ForwardDomainContextLoader({ getDomainNamePayload: jest.fn() });
const promise = () => loader.load(transaction, options);

expect(promise()).resolves.toEqual([]);
});

it("should throw an error when no registry", async () => {
const options = { options: { forwardDomain: { domain: "test.eth" } } } as LoaderOptions;

const loader = new ForwardDomainContextLoader({ getDomainNamePayload: jest.fn() });
const promise = () => loader.load(transaction, options);

expect(promise()).rejects.toThrow(
new Error(
"[ContextModule] ForwardDomainLoader: Invalid combination of domain and registry. Either both domain and registry should exist",
),
);
});

it("should throw an error when no domain", async () => {
const options = { options: { forwardDomain: { registry: "ens" } } } as LoaderOptions;

const loader = new ForwardDomainContextLoader({ getDomainNamePayload: jest.fn() });
const promise = () => loader.load(transaction, options);

expect(promise()).rejects.toThrow(
new Error(
"[ContextModule] ForwardDomainLoader: Invalid combination of domain and registry. Either both domain and registry should exist",
),
);
});

it("should return an error when domain > max length", async () => {
const options = {
options: {
forwardDomain: {
domain: "maxlength-maxlength-maxlength-maxlength-maxlength",
registry: "ens",
},
},
} as LoaderOptions;

const loader = new ForwardDomainContextLoader({ getDomainNamePayload: jest.fn() });
const result = await loader.load(transaction, options);

expect(result).toEqual([
{
type: "error" as const,
error: new Error("[ContextModule] ForwardDomainLoader: invalid domain"),
},
]);
});

it("should return an error when domain is not valid", async () => {
const options = {
options: { forwardDomain: { domain: "hello👋", registry: "ens" } },
} as LoaderOptions;

const loader = new ForwardDomainContextLoader({ getDomainNamePayload: jest.fn() });
const result = await loader.load(transaction, options);

expect(result).toEqual([
{
type: "error" as const,
error: new Error("[ContextModule] ForwardDomainLoader: invalid domain"),
},
]);
});

it("should return a payload", async () => {
const options = {
options: { forwardDomain: { domain: "hello.eth", registry: "ens" } },
} as LoaderOptions;

const loader = new ForwardDomainContextLoader({
getDomainNamePayload: () => Promise.resolve("payload"),
});
const result = await loader.load(transaction, options);

expect(result).toEqual([
{
type: "provideDomainName" as const,
payload: "payload",
},
]);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ContextLoader } from "../../shared/domain/ContextLoader";
import { ContextResponse } from "../../shared/model/ContextResponse";
import { LoaderOptions } from "../../shared/model/LoaderOptions";
import { Transaction } from "../../shared/model/Transaction";
import { ForwardDomainDataSource } from "../data/ForwardDomainDataSource";

export class ForwardDomainContextLoader implements ContextLoader {
private _dataSource: ForwardDomainDataSource;

constructor(dataSource: ForwardDomainDataSource) {
this._dataSource = dataSource;
}

async load(_transaction: Transaction, options: LoaderOptions): Promise<ContextResponse[]> {
const { domain, registry } = options.options?.forwardDomain || {};
if (!domain && !registry) {
return [];
}

if ((domain && !registry) || (!domain && registry)) {
throw new Error(
"[ContextModule] ForwardDomainLoader: Invalid combination of domain and registry. Either both domain and registry should exist",
);
}

if (!this.isDomainValid(domain as string)) {
return [
{
type: "error" as const,
error: new Error("[ContextModule] ForwardDomainLoader: invalid domain"),
},
];
}

const payload = await this._dataSource.getDomainNamePayload({
domain: domain!,
challenge: options.challenge,
});

return [{ type: "provideDomainName" as const, payload }];
}

// NOTE: duplicata of libs/domain-service/src/utils/index.ts
private isDomainValid(domain: string) {
const lengthIsValid = domain.length > 0 && Number(domain.length) < 30;
const containsOnlyValidChars = new RegExp("^[a-zA-Z0-9\\-\\_\\.]+$").test(domain);

return lengthIsValid && containsOnlyValidChars;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import axios from "axios";
import {
GetNftInformationsParams,
GetSetPluginPayloadParams,
NftDataSource,
} from "./NftDataSource";

export class HttpNftDataSource implements NftDataSource {
public async getSetPluginPayload({
chainId,
address,
selector,
}: GetSetPluginPayloadParams): Promise<string> {
const response = await axios.request<{ payload: string }>({
method: "GET",
url: `https://nft.api.live.ledger.com/v1/ethereum/${chainId}/contracts/${address}/plugin-selector/${selector}`,
});

return response.data.payload;
}

public async getNftInfosPayload({ chainId, address }: GetNftInformationsParams) {
const response = await axios.request<{ payload: string }>({
method: "GET",
url: `https://nft.api.live.ledger.com/v1/ethereum/${chainId}/contracts/${address}`,
});

return response.data.payload;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type GetSetPluginPayloadParams = {
chainId: number;
address: string;
selector: string;
};

export type GetNftInformationsParams = {
chainId: number;
address: string;
};

export interface NftDataSource {
getNftInfosPayload(params: GetNftInformationsParams): Promise<string>;
getSetPluginPayload(params: GetSetPluginPayloadParams): Promise<string>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { LoaderOptions } from "../../shared/model/LoaderOptions";
import { Transaction } from "../../shared/model/Transaction";
import { NftDataSource } from "../data/NftDataSource";
import { NftContextLoader } from "./NftContextLoader";

describe("NftContextLoader", () => {
const spyGetNftInfosPayload = jest.fn();
const spyGetPluginPayload = jest.fn();
let mockDataSource: NftDataSource;
let loader: NftContextLoader;

beforeEach(() => {
jest.restoreAllMocks();
mockDataSource = {
getNftInfosPayload: spyGetNftInfosPayload,
getSetPluginPayload: spyGetPluginPayload,
};
loader = new NftContextLoader(mockDataSource);
});

describe("load function", () => {
it("should return an empty array if no dest", async () => {
const options = {} as LoaderOptions;
const transaction = { to: undefined, data: "0x01" } as Transaction;

const result = await loader.load(transaction, options);

expect(result).toEqual([]);
});

it("should return an empty array if undefined data", async () => {
const options = {} as LoaderOptions;
const transaction = {
to: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
data: undefined,
} as unknown as Transaction;

const result = await loader.load(transaction, options);

expect(result).toEqual([]);
});

it("should return an empty array if empty data", async () => {
const options = {} as LoaderOptions;
const transaction = {
to: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
data: "0x",
} as unknown as Transaction;

const result = await loader.load(transaction, options);

expect(result).toEqual([]);
});

it("should return an empty array if selector not supported", async () => {
const options = {} as LoaderOptions;
const transaction = {
to: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
data: "0x095ea7b20000000000000",
} as unknown as Transaction;

const result = await loader.load(transaction, options);

expect(result).toEqual([]);
});

it("should return an error when no plugin response", async () => {
const options = {} as LoaderOptions;
const transaction = {
to: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
data: "0x095ea7b30000000000000",
} as unknown as Transaction;
spyGetPluginPayload.mockResolvedValueOnce(undefined);

const result = await loader.load(transaction, options);

expect(result).toEqual([
expect.objectContaining({
type: "error" as const,
error: new Error("[ContextModule] NftLoader: unexpected empty response"),
}),
]);
});

it("should return an error when no nft data response", async () => {
const options = {} as LoaderOptions;
const transaction = {
to: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
data: "0x095ea7b30000000000000",
} as unknown as Transaction;
spyGetPluginPayload.mockResolvedValueOnce("payload1");
spyGetNftInfosPayload.mockResolvedValueOnce(undefined);

const result = await loader.load(transaction, options);

expect(result).toEqual([
expect.objectContaining({
type: "error" as const,
error: new Error("[ContextModule] NftLoader: no nft metadata"),
}),
]);
});

it("should return a response", async () => {
const options = {} as LoaderOptions;
const transaction = {
to: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
data: "0x095ea7b30000000000000",
} as unknown as Transaction;
spyGetPluginPayload.mockResolvedValueOnce("payload1");
spyGetNftInfosPayload.mockResolvedValueOnce("payload2");

const result = await loader.load(transaction, options);

expect(result).toEqual([
{
type: "setPlugin" as const,
payload: "payload1",
},
{
type: "provideNFTInformation" as const,
payload: "payload2",
},
]);
});
});
});

0 comments on commit e541abf

Please sign in to comment.