WrapperConnection.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. // local imports for the ReadApi types
  2. import type {
  3. GetAssetProofRpcInput,
  4. GetAssetProofRpcResponse,
  5. GetAssetRpcInput,
  6. GetAssetsByGroupRpcInput,
  7. GetAssetsByOwnerRpcInput,
  8. ReadApiAsset,
  9. ReadApiAssetList,
  10. } from '@/ReadApi/types';
  11. import type { Metadata, Mint, NftOriginalEdition, SplTokenCurrency } from '@metaplex-foundation/js';
  12. // import from the `@metaplex-foundation/js`
  13. import { MetaplexError, Pda, amount, toBigNumber } from '@metaplex-foundation/js';
  14. import { type Commitment, Connection, type ConnectionConfig, PublicKey } from '@solana/web3.js';
  15. import { PROGRAM_ID as BUBBLEGUM_PROGRAM_ID } from '@metaplex-foundation/mpl-bubblegum';
  16. import { TokenStandard } from '@metaplex-foundation/mpl-token-metadata';
  17. import BN from 'bn.js';
  18. type JsonRpcParams<ReadApiMethodParams> = {
  19. method: string;
  20. id?: string;
  21. params: ReadApiMethodParams;
  22. };
  23. type JsonRpcOutput<ReadApiJsonOutput> = {
  24. result: ReadApiJsonOutput;
  25. };
  26. /** @group Errors */
  27. export class ReadApiError extends MetaplexError {
  28. readonly name: string = 'ReadApiError';
  29. constructor(message: string, cause?: Error) {
  30. super(message, 'rpc', undefined, cause);
  31. }
  32. }
  33. /**
  34. * Convert a ReadApi asset (e.g. compressed NFT) into an NftEdition
  35. */
  36. export const toNftEditionFromReadApiAsset = (input: ReadApiAsset): NftOriginalEdition => {
  37. return {
  38. model: 'nftEdition',
  39. isOriginal: true,
  40. address: new PublicKey(input.id),
  41. supply: toBigNumber(input.supply.print_current_supply),
  42. maxSupply: toBigNumber(input.supply.print_max_supply),
  43. };
  44. };
  45. /**
  46. * Convert a ReadApi asset (e.g. compressed NFT) into an NFT mint
  47. */
  48. export const toMintFromReadApiAsset = (input: ReadApiAsset): Mint => {
  49. const currency: SplTokenCurrency = {
  50. symbol: 'Token',
  51. decimals: 0,
  52. namespace: 'spl-token',
  53. };
  54. return {
  55. model: 'mint',
  56. address: new PublicKey(input.id),
  57. mintAuthorityAddress: new PublicKey(input.id),
  58. freezeAuthorityAddress: new PublicKey(input.id),
  59. decimals: 0,
  60. supply: amount(1, currency),
  61. isWrappedSol: false,
  62. currency,
  63. };
  64. };
  65. /**
  66. * Convert a ReadApi asset's data into standard Metaplex `Metadata`
  67. */
  68. export const toMetadataFromReadApiAsset = (input: ReadApiAsset): Metadata => {
  69. const updateAuthority = input.authorities?.find((authority) => authority.scopes.includes('full'));
  70. const collection = input.grouping.find(({ group_key }) => group_key === 'collection');
  71. return {
  72. model: 'metadata',
  73. /**
  74. * We technically don't have a metadata address anymore.
  75. * So we are using the asset's id as the address
  76. */
  77. address: Pda.find(BUBBLEGUM_PROGRAM_ID, [
  78. Buffer.from('asset', 'utf-8'),
  79. new PublicKey(input.compression.tree).toBuffer(),
  80. Uint8Array.from(new BN(input.compression.leaf_id).toArray('le', 8)),
  81. ]),
  82. mintAddress: new PublicKey(input.id),
  83. updateAuthorityAddress: new PublicKey(updateAuthority?.address),
  84. name: input.content.metadata?.name ?? '',
  85. symbol: input.content.metadata?.symbol ?? '',
  86. json: input.content.metadata,
  87. jsonLoaded: true,
  88. uri: input.content.json_uri,
  89. isMutable: input.mutable,
  90. primarySaleHappened: input.royalty.primary_sale_happened,
  91. sellerFeeBasisPoints: input.royalty.basis_points,
  92. creators: input.creators,
  93. editionNonce: input.supply.edition_nonce,
  94. tokenStandard: TokenStandard.NonFungible,
  95. collection: collection ? { address: new PublicKey(collection.group_value), verified: false } : null,
  96. // Current regular `Metadata` does not currently have a `compression` value
  97. // @ts-ignore
  98. compression: input.compression,
  99. // Read API doesn't return this info, yet
  100. collectionDetails: null,
  101. // Read API doesn't return this info, yet
  102. uses: null,
  103. // Read API doesn't return this info, yet
  104. programmableConfig: null,
  105. };
  106. };
  107. /**
  108. * Wrapper class to add additional methods on top the standard Connection from `@solana/web3.js`
  109. * Specifically, adding the RPC methods used by the Digital Asset Standards (DAS) ReadApi
  110. * for state compression and compressed NFTs
  111. */
  112. export class WrapperConnection extends Connection {
  113. private callReadApi = async <ReadApiMethodParams, ReadApiJsonOutput>(
  114. jsonRpcParams: JsonRpcParams<ReadApiMethodParams>,
  115. ): Promise<JsonRpcOutput<ReadApiJsonOutput>> => {
  116. const response = await fetch(this.rpcEndpoint, {
  117. method: 'POST',
  118. headers: {
  119. 'Content-Type': 'application/json',
  120. },
  121. body: JSON.stringify({
  122. jsonrpc: '2.0',
  123. method: jsonRpcParams.method,
  124. id: jsonRpcParams.id ?? 'rpd-op-123',
  125. params: jsonRpcParams.params,
  126. }),
  127. });
  128. return (await response.json()) as JsonRpcOutput<ReadApiJsonOutput>;
  129. };
  130. // Asset id can be calculated via Bubblegum#getLeafAssetId
  131. // It is a PDA with the following seeds: ["asset", tree, leafIndex]
  132. async getAsset(assetId: PublicKey): Promise<ReadApiAsset> {
  133. const { result: asset } = await this.callReadApi<GetAssetRpcInput, ReadApiAsset>({
  134. method: 'getAsset',
  135. params: {
  136. id: assetId.toBase58(),
  137. },
  138. });
  139. if (!asset) throw new ReadApiError('No asset returned');
  140. return asset;
  141. }
  142. // Asset id can be calculated via Bubblegum#getLeafAssetId
  143. // It is a PDA with the following seeds: ["asset", tree, leafIndex]
  144. async getAssetProof(assetId: PublicKey): Promise<GetAssetProofRpcResponse> {
  145. const { result: proof } = await this.callReadApi<GetAssetProofRpcInput, GetAssetProofRpcResponse>({
  146. method: 'getAssetProof',
  147. params: {
  148. id: assetId.toBase58(),
  149. },
  150. });
  151. if (!proof) throw new ReadApiError('No asset proof returned');
  152. return proof;
  153. }
  154. //
  155. async getAssetsByGroup({ groupKey, groupValue, page, limit, sortBy, before, after }: GetAssetsByGroupRpcInput): Promise<ReadApiAssetList> {
  156. // `page` cannot be supplied with `before` or `after`
  157. if (typeof page === 'number' && (before || after)) throw new ReadApiError('Pagination Error. Only one pagination parameter supported per query.');
  158. // a pagination method MUST be selected, but we are defaulting to using `page=0`
  159. const { result } = await this.callReadApi<GetAssetsByGroupRpcInput, ReadApiAssetList>({
  160. method: 'getAssetsByGroup',
  161. params: {
  162. groupKey,
  163. groupValue,
  164. after: after ?? null,
  165. before: before ?? null,
  166. limit: limit ?? null,
  167. page: page ?? 1,
  168. sortBy: sortBy ?? null,
  169. },
  170. });
  171. if (!result) throw new ReadApiError('No results returned');
  172. return result;
  173. }
  174. //
  175. async getAssetsByOwner({ ownerAddress, page, limit, sortBy, before, after }: GetAssetsByOwnerRpcInput): Promise<ReadApiAssetList> {
  176. // `page` cannot be supplied with `before` or `after`
  177. if (typeof page === 'number' && (before || after)) throw new ReadApiError('Pagination Error. Only one pagination parameter supported per query.');
  178. // a pagination method MUST be selected, but we are defaulting to using `page=0`
  179. const { result } = await this.callReadApi<GetAssetsByOwnerRpcInput, ReadApiAssetList>({
  180. method: 'getAssetsByOwner',
  181. params: {
  182. ownerAddress,
  183. after: after ?? null,
  184. before: before ?? null,
  185. limit: limit ?? null,
  186. page: page ?? 1,
  187. sortBy: sortBy ?? null,
  188. },
  189. });
  190. if (!result) throw new ReadApiError('No results returned');
  191. return result;
  192. }
  193. }