diff --git a/.vscode/launch.json b/.vscode/launch.json index 13aae036c..c8c7a9ba6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -99,7 +99,7 @@ "--forceExit", "--runInBand", "--testTimeout=120000", - "packages/payloadset/packages/evm/src/spec/NftIdToNftMetadataUri/NftIdToNftMetadataUri.spec.ts" + "packages/payloadset/packages/evm/src/spec/NftMetadataUriToNftMetadata/NftMetadataUriToNftMetadata.spec.ts" ], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", diff --git a/packages/payloadset/packages/api/src/Payload.ts b/packages/payloadset/packages/api/src/Payload.ts index bef97ae37..2e7d545ab 100644 --- a/packages/payloadset/packages/api/src/Payload.ts +++ b/packages/payloadset/packages/api/src/Payload.ts @@ -19,6 +19,8 @@ export type ApiUriCall = Payload< }, ApiCallSchema > +export const isApiUriCall = (value?: unknown): value is ApiUriCall => isApiCall(value) && !!(value as ApiUriCall).uri +export const asApiUriCall = AsObjectFactory.create(isApiUriCall) export type ApiUriTemplateCall = Payload< ApiCallFields & { @@ -27,12 +29,18 @@ export type ApiUriTemplateCall = Payload< }, ApiCallSchema > +export const isApiUriTemplateCall = (value?: unknown): value is ApiUriTemplateCall => + isApiCall(value) && !!((value as ApiUriTemplateCall).uriTemplate || (value as ApiUriTemplateCall).params) +export const asApiUriTemplateCall = AsObjectFactory.create(isApiUriTemplateCall) export type ApiCall = ApiUriCall | ApiUriTemplateCall export const ApiCallResultSchema = 'network.xyo.api.call.result' export type ApiCallResultSchema = typeof ApiCallResultSchema +export const isApiCall = isPayloadOfSchemaType(ApiCallSchema) +export const asApiCall = AsObjectFactory.create(isApiCall) + export interface HttpMeta { code?: string status?: number @@ -49,6 +57,11 @@ export type ApiCallJsonResult +export const isApiCallJsonResult = (x?: unknown | null): x is ApiCallJsonResult => { + return isPayloadOfSchemaType(ApiCallResultSchema)(x) && (x as ApiCallJsonResult)?.contentType === 'application/json' +} +export const asApiCallJsonResult = AsObjectFactory.create(isApiCallJsonResult) + export type ApiCallBase64Result = Payload< { call: Hash @@ -71,12 +84,5 @@ export type ApiCallResult | ApiCallErrorResult -export const isApiCall = isPayloadOfSchemaType(ApiCallSchema) -export const asApiCall = AsObjectFactory.create(isApiCall) - -export const isApiUriCall = (value?: unknown): value is ApiUriCall => isApiCall(value) && !!(value as ApiUriCall).uri -export const asApiUriCall = AsObjectFactory.create(isApiUriCall) - -export const isApiUriTemplateCall = (value?: unknown): value is ApiUriTemplateCall => - isApiCall(value) && !!((value as ApiUriTemplateCall).uriTemplate || (value as ApiUriTemplateCall).params) -export const asApiUriTemplateCall = AsObjectFactory.create(isApiUriTemplateCall) +export const isApiCallResult = isPayloadOfSchemaType(ApiCallResultSchema) +export const asApiCallResult = AsObjectFactory.create(isApiCallResult) diff --git a/packages/payloadset/packages/evm/package.json b/packages/payloadset/packages/evm/package.json index ed2bffb1d..33fd4cae9 100644 --- a/packages/payloadset/packages/evm/package.json +++ b/packages/payloadset/packages/evm/package.json @@ -24,6 +24,7 @@ "@xylabs/ts-scripts-yarn3": "^3.2.28", "@xylabs/tsconfig": "^3.2.28", "@xyo-network/account": "^2.86.1", + "@xyo-network/api-call-witness": "workspace:~", "@xyo-network/diviner-boundwitness-memory": "^2.86.1", "@xyo-network/diviner-evm-call-result-to-token-uri": "workspace:~", "@xyo-network/diviner-model": "^2.86.1", diff --git a/packages/payloadset/packages/evm/src/spec/NftIdToNftMetadataUri/NftIdToNftMetadataUri.spec.ts b/packages/payloadset/packages/evm/src/spec/NftIdToNftMetadataUri/NftIdToNftMetadataUri.spec.ts index 835da4c05..49bee60d5 100644 --- a/packages/payloadset/packages/evm/src/spec/NftIdToNftMetadataUri/NftIdToNftMetadataUri.spec.ts +++ b/packages/payloadset/packages/evm/src/spec/NftIdToNftMetadataUri/NftIdToNftMetadataUri.spec.ts @@ -85,8 +85,10 @@ describeIf(providers.length)('NftIdToNftMetadataUri', () => { }) }) describe('Index', () => { - it.each(cases)('returns indexed NftIndex results', async (address, tokenId) => { + beforeAll(async () => { await delay(100) + }) + it.each(cases)('returns indexed NftIndex results', async (address, tokenId) => { const diviner = asDivinerInstance(await node.resolve('IndexDiviner')) expect(diviner).toBeDefined() const query = { address, chainId, length: 1, schema: PayloadDivinerQuerySchema, tokenId } diff --git a/packages/payloadset/packages/evm/src/spec/NftMetadataUriToNftMetadata/NftMetadataUriToNftMetadata.json b/packages/payloadset/packages/evm/src/spec/NftMetadataUriToNftMetadata/NftMetadataUriToNftMetadata.json new file mode 100644 index 000000000..c4ffca818 --- /dev/null +++ b/packages/payloadset/packages/evm/src/spec/NftMetadataUriToNftMetadata/NftMetadataUriToNftMetadata.json @@ -0,0 +1,219 @@ +{ + "$schema": "https://raw.githubusercontent.com/XYOracleNetwork/sdk-xyo-client-js/main/packages/manifest/src/schema.json", + "nodes": [ + { + "config": { + "name": "NftTokenUriNode", + "schema": "network.xyo.node.config" + }, + "modules": { + "private": [ + { + "config": { + "name": "AddressStateArchivist", + "schema": "network.xyo.archivist.config", + "storeQueries": false + } + }, + { + "config": { + "archivist": "AddressStateArchivist", + "name": "AddressStateBoundWitnessDiviner", + "schema": "network.xyo.diviner.boundwitness.config" + } + }, + { + "config": { + "archivist": "AddressStateArchivist", + "name": "AddressStatePayloadDiviner", + "schema": "network.xyo.diviner.payload.config" + } + }, + { + "config": { + "name": "IndexArchivist", + "schema": "network.xyo.archivist.config" + } + }, + { + "config": { + "archivist": "IndexArchivist", + "name": "IndexBoundWitnessDiviner", + "schema": "network.xyo.diviner.boundwitness.config" + } + }, + { + "config": { + "archivist": "IndexArchivist", + "name": "IndexPayloadDiviner", + "schema": "network.xyo.diviner.payload.config" + } + }, + { + "config": { + "filter": { + "payload_schemas": ["network.xyo.api.call.result"] + }, + "labels": { + "network.xyo.diviner.stage": "stateToIndexCandidateDiviner" + }, + "name": "StateToIndexCandidateDiviner", + "payloadStore": { + "archivist": "Archivist", + "boundWitnessDiviner": "BoundWitnessDiviner", + "payloadDiviner": "PayloadDiviner" + }, + "schema": "network.xyo.diviner.indexing.temporal.stage.stateToIndexCandidateDiviner.config" + } + }, + { + "config": { + "labels": { + "network.xyo.diviner.stage": "indexCandidateToIndexDiviner" + }, + "name": "IndexCandidateToIndexDiviner", + "schema": "network.xyo.diviner.indexing.temporal.stage.indexCandidateToIndexDiviner.config", + "schemaTransforms": { + "network.xyo.api.call.result": [ + { + "destinationField": "uri", + "sourcePathExpression": "$.call" + } + ], + "network.xyo.timestamp": [ + { + "destinationField": "timestamp", + "sourcePathExpression": "$.timestamp" + } + ] + } + } + }, + { + "config": { + "divinerQuerySchema": "network.xyo.diviner.payload.query", + "indexQuerySchema": "network.xyo.diviner.payload.query", + "indexSchema": "network.xyo.diviner.indexing.temporal.result.index", + "labels": { + "network.xyo.diviner.stage": "divinerQueryToIndexQueryDiviner" + }, + "name": "QueryToIndexQueryDiviner", + "schema": "network.xyo.diviner.indexing.temporal.stage.divinerQueryToIndexQueryDiviner.config", + "schemaTransforms": { + "network.xyo.diviner.payload.query": [ + { + "destinationField": "uri", + "sourcePathExpression": "$.uri" + }, + { + "defaultValue": 1, + "destinationField": "limit", + "sourcePathExpression": "$.limit" + }, + { + "defaultValue": 0, + "destinationField": "offset", + "sourcePathExpression": "$.offset" + }, + { + "defaultValue": "desc", + "destinationField": "order", + "sourcePathExpression": "$.order" + } + ] + } + } + }, + { + "config": { + "labels": { + "network.xyo.diviner.stage": "indexQueryResponseToDivinerQueryResponseDiviner" + }, + "name": "IndexQueryResponseToQueryResponseDiviner", + "schema": "network.xyo.diviner.indexing.temporal.stage.indexQueryResponseToDivinerQueryResponseDiviner.config" + } + }, + { + "config": { + "name": "TimestampWitness", + "schema": "network.xyo.witness.timestamp.config" + } + }, + { + "config": { + "name": "ApiCallWitness", + "schema": "network.xyo.api.call.witness.config" + } + } + ], + "public": [ + { + "config": { + "name": "Archivist", + "schema": "network.xyo.archivist.config", + "storeQueries": false + } + }, + { + "config": { + "archivist": "Archivist", + "name": "BoundWitnessDiviner", + "schema": "network.xyo.diviner.boundwitness.config" + } + }, + { + "config": { + "archivist": "Archivist", + "name": "PayloadDiviner", + "schema": "network.xyo.diviner.payload.config" + } + }, + { + "config": { + "indexStore": { + "archivist": "IndexArchivist", + "boundWitnessDiviner": "IndexBoundWitnessDiviner", + "payloadDiviner": "IndexPayloadDiviner" + }, + "indexingDivinerStages": { + "divinerQueryToIndexQueryDiviner": "QueryToIndexQueryDiviner", + "indexCandidateToIndexDiviner": "IndexCandidateToIndexDiviner", + "indexQueryResponseToDivinerQueryResponseDiviner": "IndexQueryResponseToQueryResponseDiviner", + "stateToIndexCandidateDiviner": "StateToIndexCandidateDiviner" + }, + "name": "IndexDiviner", + "pollFrequency": 1, + "schema": "network.xyo.diviner.indexing.temporal.config", + "stateStore": { + "archivist": "AddressStateArchivist", + "boundWitnessDiviner": "AddressStateBoundWitnessDiviner", + "payloadDiviner": "AddressStatePayloadDiviner" + } + } + }, + { + "config": { + "archiving": { + "archivists": ["Archivist"] + }, + "name": "NftMetadataSentinel", + "schema": "network.xyo.sentinel.config", + "synchronous": "true", + "tasks": [ + { + "input": true, + "module": "ApiCallWitness" + }, + { + "input": true, + "module": "TimestampWitness" + } + ] + } + } + ] + } + } + ], + "schema": "network.xyo.manifest" +} diff --git a/packages/payloadset/packages/evm/src/spec/NftMetadataUriToNftMetadata/NftMetadataUriToNftMetadata.spec.ts b/packages/payloadset/packages/evm/src/spec/NftMetadataUriToNftMetadata/NftMetadataUriToNftMetadata.spec.ts new file mode 100644 index 000000000..a99b23a67 --- /dev/null +++ b/packages/payloadset/packages/evm/src/spec/NftMetadataUriToNftMetadata/NftMetadataUriToNftMetadata.spec.ts @@ -0,0 +1,97 @@ +import { delay } from '@xylabs/delay' +import { describeIf } from '@xylabs/jest-helpers' +import { HDWallet } from '@xyo-network/account' +import { ApiCall, ApiCallSchema, ApiCallWitness, ApiCallWitnessConfigSchema, ApiUriCall, isApiCallJsonResult } from '@xyo-network/api-call-witness' +import { OpenSeaNftMetadata } from '@xyo-network/crypto-nft-payload-plugin' +import { MemoryBoundWitnessDiviner } from '@xyo-network/diviner-boundwitness-memory' +import { asDivinerInstance } from '@xyo-network/diviner-model' +import { MemoryPayloadDiviner } from '@xyo-network/diviner-payload-memory' +import { PayloadDivinerQuerySchema } from '@xyo-network/diviner-payload-model' +import { + TemporalIndexingDiviner, + TemporalIndexingDivinerDivinerQueryToIndexQueryDiviner, + TemporalIndexingDivinerIndexCandidateToIndexDiviner, + TemporalIndexingDivinerIndexQueryResponseToDivinerQueryResponseDiviner, + TemporalIndexingDivinerStateToIndexCandidateDiviner, +} from '@xyo-network/diviner-temporal-indexing' +import { ManifestWrapper, PackageManifestPayload } from '@xyo-network/manifest' +import { ModuleFactory, ModuleFactoryLocator } from '@xyo-network/module-model' +import { MemoryNode } from '@xyo-network/node-memory' +import { Payload } from '@xyo-network/payload-model' +import { asSentinelInstance } from '@xyo-network/sentinel-model' +import { getProvidersFromEnv } from '@xyo-network/witness-evm-abstract' +import { TimestampWitness } from '@xyo-network/witness-timestamp' + +import nftIdToNftMetadataUri from './NftMetadataUriToNftMetadata.json' + +const maxProviders = 2 +const providers = getProvidersFromEnv(maxProviders) + +describeIf(providers.length)('NftMetadataUriToNftMetadata', () => { + const chainId = 1 + let node: MemoryNode + const cases: ApiUriCall[] = [ + { + schema: ApiCallSchema, + // BAYC + uri: 'ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/15', + }, + { + schema: ApiCallSchema, + // Gutter Cats + uri: 'https://gutter-cats-metadata.s3.us-east-2.amazonaws.com/metadata/1347', + }, + ] + beforeAll(async () => { + const wallet = await HDWallet.random() + const locator = new ModuleFactoryLocator() + locator.register(MemoryBoundWitnessDiviner) + locator.register(MemoryPayloadDiviner) + locator.register(TemporalIndexingDivinerDivinerQueryToIndexQueryDiviner) + locator.register(TemporalIndexingDivinerIndexCandidateToIndexDiviner) + locator.register(TemporalIndexingDivinerIndexQueryResponseToDivinerQueryResponseDiviner) + locator.register(TemporalIndexingDivinerStateToIndexCandidateDiviner) + locator.register(TemporalIndexingDiviner) + locator.register(TimestampWitness) + locator.register( + new ModuleFactory(ApiCallWitness, { + config: { schema: ApiCallWitnessConfigSchema }, + ipfsGateway: '5d7b6582.beta.decentralnetworkservices.com', + }), + ) + const manifest = nftIdToNftMetadataUri as PackageManifestPayload + const manifestWrapper = new ManifestWrapper(manifest, wallet, locator) + node = await manifestWrapper.loadNodeFromIndex(0) + const mods = await node.resolve() + const publicModules = manifest.nodes[0].modules?.public ?? [] + expect(mods.length).toBe(publicModules.length + 1) + }) + describe('Sentinel', () => { + it.each(cases)('returns metadata URI for token ID', async (apiCall) => { + const sentinel = asSentinelInstance(await node.resolve('NftMetadataSentinel')) + expect(sentinel).toBeDefined() + const report = await sentinel?.report([apiCall]) + const results = report?.filter(isApiCallJsonResult) ?? [] + expect(results.length).toBe(1) + expect(results[0].data).toBeObject() + const metadata = results[0].data as OpenSeaNftMetadata + expect(metadata.image).toBeString() + expect(metadata.attributes).toBeArray() + }) + }) + describe('Index', () => { + beforeAll(async () => { + await delay(100) + }) + it.each(cases)('returns indexed NftIndex results', async (apiCall) => { + const { uri } = apiCall + const diviner = asDivinerInstance(await node.resolve('IndexDiviner')) + expect(diviner).toBeDefined() + const query = { limit: 1, schema: PayloadDivinerQuerySchema, uri } + const result = (await diviner?.divine([query])) as unknown as Payload<{ uri: string }>[] + expect(result).toBeDefined() + expect(result).toBeArrayOfSize(1) + expect(result?.[0]?.uri).toBe(uri) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 9d1a869cc..30415b9aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5418,6 +5418,7 @@ __metadata: "@xylabs/ts-scripts-yarn3": "npm:^3.2.28" "@xylabs/tsconfig": "npm:^3.2.28" "@xyo-network/account": "npm:^2.86.1" + "@xyo-network/api-call-witness": "workspace:~" "@xyo-network/diviner-boundwitness-memory": "npm:^2.86.1" "@xyo-network/diviner-evm-call-result-to-token-uri": "workspace:~" "@xyo-network/diviner-model": "npm:^2.86.1"