// local imports for the ReadApi types import type { GetAssetProofRpcInput, GetAssetProofRpcResponse, GetAssetRpcInput, GetAssetsByGroupRpcInput, GetAssetsByOwnerRpcInput, ReadApiAsset, ReadApiAssetList, } from '@/ReadApi/types'; import type { Metadata, Mint, NftOriginalEdition, SplTokenCurrency } from '@metaplex-foundation/js'; // import from the `@metaplex-foundation/js` import { MetaplexError, Pda, amount, toBigNumber } from '@metaplex-foundation/js'; import { type Commitment, Connection, type ConnectionConfig, PublicKey } from '@solana/web3.js'; import { PROGRAM_ID as BUBBLEGUM_PROGRAM_ID } from '@metaplex-foundation/mpl-bubblegum'; import { TokenStandard } from '@metaplex-foundation/mpl-token-metadata'; import BN from 'bn.js'; type JsonRpcParams = { method: string; id?: string; params: ReadApiMethodParams; }; type JsonRpcOutput = { result: ReadApiJsonOutput; }; /** @group Errors */ export class ReadApiError extends MetaplexError { readonly name: string = 'ReadApiError'; constructor(message: string, cause?: Error) { super(message, 'rpc', undefined, cause); } } /** * Convert a ReadApi asset (e.g. compressed NFT) into an NftEdition */ export const toNftEditionFromReadApiAsset = (input: ReadApiAsset): NftOriginalEdition => { return { model: 'nftEdition', isOriginal: true, address: new PublicKey(input.id), supply: toBigNumber(input.supply.print_current_supply), maxSupply: toBigNumber(input.supply.print_max_supply), }; }; /** * Convert a ReadApi asset (e.g. compressed NFT) into an NFT mint */ export const toMintFromReadApiAsset = (input: ReadApiAsset): Mint => { const currency: SplTokenCurrency = { symbol: 'Token', decimals: 0, namespace: 'spl-token', }; return { model: 'mint', address: new PublicKey(input.id), mintAuthorityAddress: new PublicKey(input.id), freezeAuthorityAddress: new PublicKey(input.id), decimals: 0, supply: amount(1, currency), isWrappedSol: false, currency, }; }; /** * Convert a ReadApi asset's data into standard Metaplex `Metadata` */ export const toMetadataFromReadApiAsset = (input: ReadApiAsset): Metadata => { const updateAuthority = input.authorities?.find((authority) => authority.scopes.includes('full')); const collection = input.grouping.find(({ group_key }) => group_key === 'collection'); return { model: 'metadata', /** * We technically don't have a metadata address anymore. * So we are using the asset's id as the address */ address: Pda.find(BUBBLEGUM_PROGRAM_ID, [ Buffer.from('asset', 'utf-8'), new PublicKey(input.compression.tree).toBuffer(), Uint8Array.from(new BN(input.compression.leaf_id).toArray('le', 8)), ]), mintAddress: new PublicKey(input.id), updateAuthorityAddress: new PublicKey(updateAuthority?.address), name: input.content.metadata?.name ?? '', symbol: input.content.metadata?.symbol ?? '', json: input.content.metadata, jsonLoaded: true, uri: input.content.json_uri, isMutable: input.mutable, primarySaleHappened: input.royalty.primary_sale_happened, sellerFeeBasisPoints: input.royalty.basis_points, creators: input.creators, editionNonce: input.supply.edition_nonce, tokenStandard: TokenStandard.NonFungible, collection: collection ? { address: new PublicKey(collection.group_value), verified: false } : null, // Current regular `Metadata` does not currently have a `compression` value // @ts-ignore compression: input.compression, // Read API doesn't return this info, yet collectionDetails: null, // Read API doesn't return this info, yet uses: null, // Read API doesn't return this info, yet programmableConfig: null, }; }; /** * Wrapper class to add additional methods on top the standard Connection from `@solana/web3.js` * Specifically, adding the RPC methods used by the Digital Asset Standards (DAS) ReadApi * for state compression and compressed NFTs */ export class WrapperConnection extends Connection { private callReadApi = async ( jsonRpcParams: JsonRpcParams, ): Promise> => { const response = await fetch(this.rpcEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ jsonrpc: '2.0', method: jsonRpcParams.method, id: jsonRpcParams.id ?? 'rpd-op-123', params: jsonRpcParams.params, }), }); return (await response.json()) as JsonRpcOutput; }; // Asset id can be calculated via Bubblegum#getLeafAssetId // It is a PDA with the following seeds: ["asset", tree, leafIndex] async getAsset(assetId: PublicKey): Promise { const { result: asset } = await this.callReadApi({ method: 'getAsset', params: { id: assetId.toBase58(), }, }); if (!asset) throw new ReadApiError('No asset returned'); return asset; } // Asset id can be calculated via Bubblegum#getLeafAssetId // It is a PDA with the following seeds: ["asset", tree, leafIndex] async getAssetProof(assetId: PublicKey): Promise { const { result: proof } = await this.callReadApi({ method: 'getAssetProof', params: { id: assetId.toBase58(), }, }); if (!proof) throw new ReadApiError('No asset proof returned'); return proof; } // async getAssetsByGroup({ groupKey, groupValue, page, limit, sortBy, before, after }: GetAssetsByGroupRpcInput): Promise { // `page` cannot be supplied with `before` or `after` if (typeof page === 'number' && (before || after)) throw new ReadApiError('Pagination Error. Only one pagination parameter supported per query.'); // a pagination method MUST be selected, but we are defaulting to using `page=0` const { result } = await this.callReadApi({ method: 'getAssetsByGroup', params: { groupKey, groupValue, after: after ?? null, before: before ?? null, limit: limit ?? null, page: page ?? 1, sortBy: sortBy ?? null, }, }); if (!result) throw new ReadApiError('No results returned'); return result; } // async getAssetsByOwner({ ownerAddress, page, limit, sortBy, before, after }: GetAssetsByOwnerRpcInput): Promise { // `page` cannot be supplied with `before` or `after` if (typeof page === 'number' && (before || after)) throw new ReadApiError('Pagination Error. Only one pagination parameter supported per query.'); // a pagination method MUST be selected, but we are defaulting to using `page=0` const { result } = await this.callReadApi({ method: 'getAssetsByOwner', params: { ownerAddress, after: after ?? null, before: before ?? null, limit: limit ?? null, page: page ?? 1, sortBy: sortBy ?? null, }, }); if (!result) throw new ReadApiError('No results returned'); return result; } }