-
Notifications
You must be signed in to change notification settings - Fork 294
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(context-module): add Nft loader
- Loading branch information
1 parent
367649f
commit ecaf487
Showing
4 changed files
with
256 additions
and
0 deletions.
There are no files selected for viewing
30 changes: 30 additions & 0 deletions
30
libs/ledgerjs/packages/context-module/src/nft/data/HttpNftDataSource.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
libs/ledgerjs/packages/context-module/src/nft/data/NftDataSource.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
} |
127 changes: 127 additions & 0 deletions
127
libs/ledgerjs/packages/context-module/src/nft/domain/NftContextLoader.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}, | ||
]); | ||
}); | ||
}); | ||
}); |
84 changes: 84 additions & 0 deletions
84
libs/ledgerjs/packages/context-module/src/nft/domain/NftContextLoader.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
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 { NftDataSource } from "../data/NftDataSource"; | ||
|
||
enum ERC721_SUPPORTED_SELECTOR { | ||
Approve = "0x095ea7b3", | ||
SetApprovalForAll = "0xa22cb465", | ||
TransferFrom = "0x23b872dd", | ||
SafeTransferFrom = "0x42842e0e", | ||
SafeTransferFromWithData = "0xb88d4fde", | ||
} | ||
|
||
enum ERC1155_SUPPORTED_SELECTOR { | ||
SetApprovalForAll = "0xa22cb465", | ||
SafeTransferFrom = "0xf242432a", | ||
SafeBatchTransferFrom = "0x2eb2c2d6", | ||
} | ||
|
||
const SUPPORTED_SELECTORS: `0x${string}`[] = [ | ||
...Object.values(ERC721_SUPPORTED_SELECTOR), | ||
...Object.values(ERC1155_SUPPORTED_SELECTOR), | ||
]; | ||
|
||
export class NftContextLoader implements ContextLoader { | ||
private _dataSource: NftDataSource; | ||
|
||
constructor(dataSource: NftDataSource) { | ||
this._dataSource = dataSource; | ||
} | ||
|
||
async load(transaction: Transaction, _options: LoaderOptions): Promise<ContextResponse[]> { | ||
const responses: ContextResponse[] = []; | ||
|
||
if (!transaction.to || !transaction.data || transaction.data === "0x") { | ||
return []; | ||
} | ||
|
||
const selector = transaction.data.slice(0, 10) as `0x${string}`; | ||
|
||
if (!this.isSelectorSupported(selector)) { | ||
return []; | ||
} | ||
|
||
// EXAMPLE: | ||
// https://nft.api.live.ledger.com/v1/ethereum/1/contracts/0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D/plugin-selector/0x095ea7b3 | ||
const setPluginPayload = await this._dataSource.getSetPluginPayload({ | ||
chainId: transaction.chainId, | ||
address: transaction.to, | ||
selector, | ||
}); | ||
|
||
if (!setPluginPayload) { | ||
return [ | ||
{ | ||
type: "error" as const, | ||
error: new Error("[ContextModule] NftLoader: unexpected empty response"), | ||
}, | ||
]; | ||
} | ||
|
||
responses.push({ type: "setPlugin", payload: setPluginPayload }); | ||
|
||
const nftInformationsPayload = await this._dataSource.getNftInfosPayload({ | ||
chainId: transaction.chainId, | ||
address: transaction.to, | ||
}); | ||
|
||
if (!nftInformationsPayload) { | ||
return [ | ||
{ type: "error" as const, error: new Error("[ContextModule] NftLoader: no nft metadata") }, | ||
]; | ||
} | ||
|
||
responses.push({ type: "provideNFTInformation", payload: nftInformationsPayload }); | ||
|
||
return responses; | ||
} | ||
|
||
private isSelectorSupported(selector: `0x${string}`) { | ||
return Object.values(SUPPORTED_SELECTORS).includes(selector); | ||
} | ||
} |