WrapperConnection.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import { Commitment, Connection, ConnectionConfig, PublicKey } from "@solana/web3.js";
  2. // local imports for the ReadApi types
  3. import type {
  4. GetAssetProofRpcInput,
  5. GetAssetProofRpcResponse,
  6. GetAssetRpcInput,
  7. GetAssetsByGroupRpcInput,
  8. GetAssetsByOwnerRpcInput,
  9. ReadApiAsset,
  10. ReadApiAssetList,
  11. } from "@/ReadApi/types";
  12. import type { Metadata, Mint, NftOriginalEdition, SplTokenCurrency } from "@metaplex-foundation/js";
  13. // import from the `@metaplex-foundation/js`
  14. import { MetaplexError, Pda, amount, toBigNumber } from "@metaplex-foundation/js";
  15. import BN from "bn.js";
  16. import { PROGRAM_ID as BUBBLEGUM_PROGRAM_ID } from "@metaplex-foundation/mpl-bubblegum";
  17. import { TokenStandard } from "@metaplex-foundation/mpl-token-metadata";
  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
  96. ? { address: new PublicKey(collection.group_value), verified: false }
  97. : null,
  98. // Current regular `Metadata` does not currently have a `compression` value
  99. // @ts-ignore
  100. compression: input.compression,
  101. // Read API doesn't return this info, yet
  102. collectionDetails: null,
  103. // Read API doesn't return this info, yet
  104. uses: null,
  105. // Read API doesn't return this info, yet
  106. programmableConfig: null,
  107. };
  108. };
  109. /**
  110. * Wrapper class to add additional methods on top the standard Connection from `@solana/web3.js`
  111. * Specifically, adding the RPC methods used by the Digital Asset Standards (DAS) ReadApi
  112. * for state compression and compressed NFTs
  113. */
  114. export class WrapperConnection extends Connection {
  115. constructor(endpoint: string, commitmentOrConfig?: Commitment | ConnectionConfig) {
  116. super(endpoint, commitmentOrConfig);
  117. }
  118. private callReadApi = async <ReadApiMethodParams, ReadApiJsonOutput>(
  119. jsonRpcParams: JsonRpcParams<ReadApiMethodParams>,
  120. ): Promise<JsonRpcOutput<ReadApiJsonOutput>> => {
  121. const response = await fetch(this.rpcEndpoint, {
  122. method: "POST",
  123. headers: {
  124. "Content-Type": "application/json",
  125. },
  126. body: JSON.stringify({
  127. jsonrpc: "2.0",
  128. method: jsonRpcParams.method,
  129. id: jsonRpcParams.id ?? "rpd-op-123",
  130. params: jsonRpcParams.params,
  131. }),
  132. });
  133. return await response.json() as JsonRpcOutput<ReadApiJsonOutput>;
  134. };
  135. // Asset id can be calculated via Bubblegum#getLeafAssetId
  136. // It is a PDA with the following seeds: ["asset", tree, leafIndex]
  137. async getAsset(assetId: PublicKey): Promise<ReadApiAsset> {
  138. const { result: asset } = await this.callReadApi<GetAssetRpcInput, ReadApiAsset>({
  139. method: "getAsset",
  140. params: {
  141. id: assetId.toBase58(),
  142. },
  143. });
  144. if (!asset) throw new ReadApiError("No asset returned");
  145. return asset;
  146. }
  147. // Asset id can be calculated via Bubblegum#getLeafAssetId
  148. // It is a PDA with the following seeds: ["asset", tree, leafIndex]
  149. async getAssetProof(assetId: PublicKey): Promise<GetAssetProofRpcResponse> {
  150. const { result: proof } = await this.callReadApi<
  151. GetAssetProofRpcInput,
  152. GetAssetProofRpcResponse
  153. >({
  154. method: "getAssetProof",
  155. params: {
  156. id: assetId.toBase58(),
  157. },
  158. });
  159. if (!proof) throw new ReadApiError("No asset proof returned");
  160. return proof;
  161. }
  162. //
  163. async getAssetsByGroup({
  164. groupKey,
  165. groupValue,
  166. page,
  167. limit,
  168. sortBy,
  169. before,
  170. after,
  171. }: GetAssetsByGroupRpcInput): Promise<ReadApiAssetList> {
  172. // `page` cannot be supplied with `before` or `after`
  173. if (typeof page == "number" && (before || after))
  174. throw new ReadApiError(
  175. "Pagination Error. Only one pagination parameter supported per query.",
  176. );
  177. // a pagination method MUST be selected, but we are defaulting to using `page=0`
  178. const { result } = await this.callReadApi<GetAssetsByGroupRpcInput, ReadApiAssetList>({
  179. method: "getAssetsByGroup",
  180. params: {
  181. groupKey,
  182. groupValue,
  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. //
  194. async getAssetsByOwner({
  195. ownerAddress,
  196. page,
  197. limit,
  198. sortBy,
  199. before,
  200. after,
  201. }: GetAssetsByOwnerRpcInput): Promise<ReadApiAssetList> {
  202. // `page` cannot be supplied with `before` or `after`
  203. if (typeof page == "number" && (before || after))
  204. throw new ReadApiError(
  205. "Pagination Error. Only one pagination parameter supported per query.",
  206. );
  207. // a pagination method MUST be selected, but we are defaulting to using `page=0`
  208. const { result } = await this.callReadApi<GetAssetsByOwnerRpcInput, ReadApiAssetList>({
  209. method: "getAssetsByOwner",
  210. params: {
  211. ownerAddress,
  212. after: after ?? null,
  213. before: before ?? null,
  214. limit: limit ?? null,
  215. page: page ?? 1,
  216. sortBy: sortBy ?? null,
  217. },
  218. });
  219. if (!result) throw new ReadApiError("No results returned");
  220. return result;
  221. }
  222. }