Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: nft service #250

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/sdk-ts/src/client/wasm/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './swap'
export * from './types'
export * from './nameservice'
export * from './nft'
3 changes: 3 additions & 0 deletions packages/sdk-ts/src/client/wasm/nft/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './queries'
export * from './transformer'
export * from './types'
36 changes: 36 additions & 0 deletions packages/sdk-ts/src/client/wasm/nft/queries/QueryIpfs.ts
Original file line number Diff line number Diff line change
@@ -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<IpfsTokenResponse> {
try {
const response = await this.retry<RestApiResponse<IpfsTokenResponse>>(
() => {
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}`,
})
}
}
}
14 changes: 14 additions & 0 deletions packages/sdk-ts/src/client/wasm/nft/queries/QueryNftCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BaseWasmQuery } from '../../BaseWasmQuery'
import { toBase64 } from '../../../../utils'

export declare namespace CollectionQueryArg {
export interface Params {}
}

export class QueryNftCollection extends BaseWasmQuery<CollectionQueryArg.Params> {
toPayload() {
return toBase64({
contract_info: {},
})
}
}
24 changes: 24 additions & 0 deletions packages/sdk-ts/src/client/wasm/nft/queries/QueryNftToken.ts
Original file line number Diff line number Diff line change
@@ -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<TokenQueryArg.Params> {
toPayload() {
return toBase64({
tokens: {
owner: this.params.owner,
limit: this.params.limit,
verbose: this.params.verbose,
start_after: this.params.start_after,
},
})
}
}
3 changes: 3 additions & 0 deletions packages/sdk-ts/src/client/wasm/nft/queries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { QueryNftToken } from './QueryNftToken'
export { QueryNftCollection } from './QueryNftCollection'
export { QueryIpfs } from './QueryIpfs'
56 changes: 56 additions & 0 deletions packages/sdk-ts/src/client/wasm/nft/transformer.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
48 changes: 48 additions & 0 deletions packages/sdk-ts/src/client/wasm/nft/types/index.ts
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions packages/sdk-ui-ts/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './web3'
export * from './gas'
export * from './nameservice'
export * from './nft'
166 changes: 166 additions & 0 deletions packages/sdk-ui-ts/src/services/nft/NftService.ts
Original file line number Diff line number Diff line change
@@ -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<NftTokenMetadata[]> {
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<NftTokenWithAddress[]> {
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<NftTokenWithAddress | undefined>)
.value,
)
.filter((metadata) => metadata) as NftTokenWithAddress[]
}

private async fetchIpfsTokenMetadata(
allTokensList: Array<NftTokenMetadata & { contract: string }>,
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<NftCollectionWithAddress> {
const queryNftCollectionPayload = new QueryNftCollection({}).toPayload()

const response = await this.chainGrpcWasmApiClient.fetchSmartContractState(
contractAddress,
queryNftCollectionPayload,
)

const collection =
NftQueryTransformer.collectionResponseToCollection(response)

return { ...collection, collectionAddress: contractAddress }
}
}
1 change: 1 addition & 0 deletions packages/sdk-ui-ts/src/services/nft/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './NftService'