From a1bc48c026b6ebdc92e1893515a2e5b487f3306e Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 18 Oct 2023 15:23:21 -0700 Subject: [PATCH] chore: nft service --- packages/sdk-ts/src/client/wasm/index.ts | 1 + packages/sdk-ts/src/client/wasm/nft/index.ts | 3 + .../src/client/wasm/nft/queries/QueryIpfs.ts | 36 ++++ .../wasm/nft/queries/QueryNftCollection.ts | 14 ++ .../client/wasm/nft/queries/QueryNftToken.ts | 24 +++ .../src/client/wasm/nft/queries/index.ts | 3 + .../sdk-ts/src/client/wasm/nft/transformer.ts | 56 ++++++ .../sdk-ts/src/client/wasm/nft/types/index.ts | 48 +++++ packages/sdk-ui-ts/src/services/index.ts | 1 + .../sdk-ui-ts/src/services/nft/NftService.ts | 166 ++++++++++++++++++ packages/sdk-ui-ts/src/services/nft/index.ts | 1 + 11 files changed, 353 insertions(+) create mode 100644 packages/sdk-ts/src/client/wasm/nft/index.ts create mode 100644 packages/sdk-ts/src/client/wasm/nft/queries/QueryIpfs.ts create mode 100644 packages/sdk-ts/src/client/wasm/nft/queries/QueryNftCollection.ts create mode 100644 packages/sdk-ts/src/client/wasm/nft/queries/QueryNftToken.ts create mode 100644 packages/sdk-ts/src/client/wasm/nft/queries/index.ts create mode 100644 packages/sdk-ts/src/client/wasm/nft/transformer.ts create mode 100644 packages/sdk-ts/src/client/wasm/nft/types/index.ts create mode 100644 packages/sdk-ui-ts/src/services/nft/NftService.ts create mode 100644 packages/sdk-ui-ts/src/services/nft/index.ts diff --git a/packages/sdk-ts/src/client/wasm/index.ts b/packages/sdk-ts/src/client/wasm/index.ts index 93c4cea8e..3a4f31b06 100644 --- a/packages/sdk-ts/src/client/wasm/index.ts +++ b/packages/sdk-ts/src/client/wasm/index.ts @@ -1,3 +1,4 @@ export * from './swap' export * from './types' export * from './nameservice' +export * from './nft' diff --git a/packages/sdk-ts/src/client/wasm/nft/index.ts b/packages/sdk-ts/src/client/wasm/nft/index.ts new file mode 100644 index 000000000..2b6403383 --- /dev/null +++ b/packages/sdk-ts/src/client/wasm/nft/index.ts @@ -0,0 +1,3 @@ +export * from './queries' +export * from './transformer' +export * from './types' diff --git a/packages/sdk-ts/src/client/wasm/nft/queries/QueryIpfs.ts b/packages/sdk-ts/src/client/wasm/nft/queries/QueryIpfs.ts new file mode 100644 index 000000000..b0368b97f --- /dev/null +++ b/packages/sdk-ts/src/client/wasm/nft/queries/QueryIpfs.ts @@ -0,0 +1,36 @@ +import BaseRestConsumer from '../../../BaseRestConsumer' +import { + HttpRequestException, + UnspecifiedErrorCode, +} from '@injectivelabs/exceptions' +import { IpfsTokenResponse } from '../types' +import { RestApiResponse } from '../../../chain/types' + +export class QueryIpfs extends BaseRestConsumer { + constructor(endpoint: string) { + super(endpoint) + } + + public async fetchJson(ipfsPath: string): Promise { + try { + const response = await this.retry>( + () => { + return this.get(ipfsPath) + }, + 3, + 1000, + ) + + return response.data + } catch (e: unknown) { + if (e instanceof HttpRequestException) { + throw e + } + + throw new HttpRequestException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + context: `${this.endpoint}/${ipfsPath}`, + }) + } + } +} diff --git a/packages/sdk-ts/src/client/wasm/nft/queries/QueryNftCollection.ts b/packages/sdk-ts/src/client/wasm/nft/queries/QueryNftCollection.ts new file mode 100644 index 000000000..8f025ff64 --- /dev/null +++ b/packages/sdk-ts/src/client/wasm/nft/queries/QueryNftCollection.ts @@ -0,0 +1,14 @@ +import { BaseWasmQuery } from '../../BaseWasmQuery' +import { toBase64 } from '../../../../utils' + +export declare namespace CollectionQueryArg { + export interface Params {} +} + +export class QueryNftCollection extends BaseWasmQuery { + toPayload() { + return toBase64({ + contract_info: {}, + }) + } +} diff --git a/packages/sdk-ts/src/client/wasm/nft/queries/QueryNftToken.ts b/packages/sdk-ts/src/client/wasm/nft/queries/QueryNftToken.ts new file mode 100644 index 000000000..c8d2ac669 --- /dev/null +++ b/packages/sdk-ts/src/client/wasm/nft/queries/QueryNftToken.ts @@ -0,0 +1,24 @@ +import { BaseWasmQuery } from '../../BaseWasmQuery' +import { toBase64 } from '../../../../utils' + +export declare namespace TokenQueryArg { + export interface Params { + owner: string + limit: number + verbose: boolean + start_after?: string + } +} + +export class QueryNftToken extends BaseWasmQuery { + toPayload() { + return toBase64({ + tokens: { + owner: this.params.owner, + limit: this.params.limit, + verbose: this.params.verbose, + start_after: this.params.start_after, + }, + }) + } +} diff --git a/packages/sdk-ts/src/client/wasm/nft/queries/index.ts b/packages/sdk-ts/src/client/wasm/nft/queries/index.ts new file mode 100644 index 000000000..13a9e8986 --- /dev/null +++ b/packages/sdk-ts/src/client/wasm/nft/queries/index.ts @@ -0,0 +1,3 @@ +export { QueryNftToken } from './QueryNftToken' +export { QueryNftCollection } from './QueryNftCollection' +export { QueryIpfs } from './QueryIpfs' diff --git a/packages/sdk-ts/src/client/wasm/nft/transformer.ts b/packages/sdk-ts/src/client/wasm/nft/transformer.ts new file mode 100644 index 000000000..b2bfcdfd8 --- /dev/null +++ b/packages/sdk-ts/src/client/wasm/nft/transformer.ts @@ -0,0 +1,56 @@ +import { WasmContractQueryResponse } from '../types' +import { + NftToken, + NftTokenMetadata, + IpfsTokenResponse, + NftCollectionMetadata, + QueryNftTokenResponse, +} from './types' +import { toUtf8 } from '../../../utils' + +export class NftQueryTransformer { + static tokensResponseToTokens( + response: WasmContractQueryResponse, + ): NftTokenMetadata[] | undefined { + const { tokens } = JSON.parse(toUtf8(response.data)) + + if (tokens.length === 0) { + return + } + + return tokens.map(this.tokenResponseToToken) + } + + static tokenResponseToToken(token: QueryNftTokenResponse): NftTokenMetadata { + return { + owner: token.owner, + tokenId: token.token_id, + metadataUri: token.metadata_uri, + } + } + + static ipfsTokenResponseToToken(token: IpfsTokenResponse): NftToken { + return { + title: token.title, + description: token.description, + rarity: token.rarity, + rank: token.rank, + release: token.release, + style: token.style, + license: token.license, + media: token.media, + tags: token.tags, + } + } + + static collectionResponseToCollection( + response: WasmContractQueryResponse, + ): NftCollectionMetadata { + const collection = JSON.parse(toUtf8(response.data)) + + return { + name: collection.name, + symbol: collection.symbol, + } + } +} diff --git a/packages/sdk-ts/src/client/wasm/nft/types/index.ts b/packages/sdk-ts/src/client/wasm/nft/types/index.ts new file mode 100644 index 000000000..ddce8c15f --- /dev/null +++ b/packages/sdk-ts/src/client/wasm/nft/types/index.ts @@ -0,0 +1,48 @@ +export interface QueryNftTokenResponse { + owner: string + token_id: string + metadata_uri: string +} + +export interface NftTokenMetadata { + owner: string + tokenId: string + metadataUri: string +} + +export interface IpfsTokenResponse { + title: string + description: string + rarity: string + rank: string + release: string + style: string + license: string + media: string + tags: string +} + +export interface NftToken { + title: string + description: string + rarity: string + rank: string + release: string + style: string + license: string + media: string + tags: string +} + +export interface NftTokenWithAddress extends NftToken { + collectionAddress: string +} + +export interface NftCollectionMetadata { + name: string + symbol: string +} + +export interface NftCollectionWithAddress extends NftCollectionMetadata { + collectionAddress: string +} diff --git a/packages/sdk-ui-ts/src/services/index.ts b/packages/sdk-ui-ts/src/services/index.ts index d8a050ed5..b9a3d528e 100644 --- a/packages/sdk-ui-ts/src/services/index.ts +++ b/packages/sdk-ui-ts/src/services/index.ts @@ -1,3 +1,4 @@ export * from './web3' export * from './gas' export * from './nameservice' +export * from './nft' diff --git a/packages/sdk-ui-ts/src/services/nft/NftService.ts b/packages/sdk-ui-ts/src/services/nft/NftService.ts new file mode 100644 index 000000000..9ccd96e02 --- /dev/null +++ b/packages/sdk-ui-ts/src/services/nft/NftService.ts @@ -0,0 +1,166 @@ +import { + QueryIpfs, + QueryNftToken, + NftTokenMetadata, + ChainGrpcWasmApi, + QueryNftCollection, + NftQueryTransformer, + NftTokenWithAddress, + NftCollectionWithAddress, +} from '@injectivelabs/sdk-ts' +import { + Network, + NetworkEndpoints, + getNetworkEndpoints, +} from '@injectivelabs/networks' + +export class NftService { + protected chainGrpcWasmApiClient: ChainGrpcWasmApi + private ipfsQueryClient: QueryIpfs + + constructor( + network: Network = Network.Testnet, + ipfsEndpoint: string, + endpoints?: NetworkEndpoints, + ) { + const networkEndpoints = endpoints || getNetworkEndpoints(network) + this.chainGrpcWasmApiClient = new ChainGrpcWasmApi(networkEndpoints.grpc) + this.ipfsQueryClient = new QueryIpfs(ipfsEndpoint) + } + + private async accumulateTokens({ + contractAddress, + injectiveAddress, + lastTokenId, + allTokens = [], + }: { + contractAddress: string + injectiveAddress: string + lastTokenId?: string + allTokens?: NftTokenMetadata[] + }): Promise { + const MAX_ITEMS = 30 + + const queryNftTokenPayload = new QueryNftToken({ + owner: injectiveAddress, + limit: MAX_ITEMS, + verbose: true, + ...(lastTokenId ? { start_after: lastTokenId } : undefined), + }).toPayload() + + const response = await this.chainGrpcWasmApiClient.fetchSmartContractState( + contractAddress, + queryNftTokenPayload, + ) + + const tokens = NftQueryTransformer.tokensResponseToTokens(response) + + if (!tokens) { + return allTokens + } + + return await this.accumulateTokens({ + contractAddress, + injectiveAddress, + lastTokenId: tokens[tokens.length - 1].tokenId, + allTokens: [...allTokens, ...tokens], + }) + } + + private async fetchAllTokensForContract( + contractAddress: string, + injectiveAddress: string, + ) { + return await this.accumulateTokens({ contractAddress, injectiveAddress }) + } + + public async fetchTokens( + { + codeId, + limit = 100_000, + injectiveAddress, + }: { + codeId: number + limit?: number + injectiveAddress: string + }, + errorCallback?: (error: Error) => void, + ): Promise { + const collections = + await this.chainGrpcWasmApiClient.fetchContractCodeContracts(codeId, { + limit, + }) + + const allTokensList = await Promise.all( + collections.contractsList.map(async (contract) => ({ + contract, + tokens: await this.fetchAllTokensForContract( + contract, + injectiveAddress, + ), + })), + ) + + const tokensWithContract = allTokensList.flatMap(({ contract, tokens }) => + tokens.map((token) => ({ ...token, contract })), + ) + + const ipfsTokenMetadataPromises = await this.fetchIpfsTokenMetadata( + tokensWithContract, + errorCallback, + ) + + return (await Promise.allSettled(ipfsTokenMetadataPromises)) + .filter((result) => result.status === 'fulfilled') + .map( + (result) => + (result as PromiseFulfilledResult) + .value, + ) + .filter((metadata) => metadata) as NftTokenWithAddress[] + } + + private async fetchIpfsTokenMetadata( + allTokensList: Array, + errorCallback?: (error: Error) => void, + ) { + return allTokensList.map(async ({ metadataUri, contract }) => { + try { + const path = metadataUri.replace('ipfs://', '') + const tokenMetadata = await this.ipfsQueryClient.fetchJson(path) + const transformedTokenMetadata = + NftQueryTransformer.ipfsTokenResponseToToken(tokenMetadata) + + return { + ...transformedTokenMetadata, + collectionAddress: contract, + } + } catch (e: any) { + /** + * Since we still want to pass the successful results, we handle ipfs timeouts on the client + **/ + if (errorCallback) { + errorCallback(e) + } + + return + } + }) + } + + public async fetchCollectionInfo( + contractAddress: string, + ): Promise { + const queryNftCollectionPayload = new QueryNftCollection({}).toPayload() + + const response = await this.chainGrpcWasmApiClient.fetchSmartContractState( + contractAddress, + queryNftCollectionPayload, + ) + + const collection = + NftQueryTransformer.collectionResponseToCollection(response) + + return { ...collection, collectionAddress: contractAddress } + } +} diff --git a/packages/sdk-ui-ts/src/services/nft/index.ts b/packages/sdk-ui-ts/src/services/nft/index.ts new file mode 100644 index 000000000..3612473b2 --- /dev/null +++ b/packages/sdk-ui-ts/src/services/nft/index.ts @@ -0,0 +1 @@ +export * from './NftService'