Repository: alchemyplatform/alchemy-sdk-js
Pull request #155 (2022-10-20 01:54:02)

Nft api summarize nft attributes
### Changelog

- Adds  the `summarizeNft` method in the `nft` namespace.
- Adds the `NftAttributesSummary` type to describe the result for the `summarizeNft` endpoint.
- Adds unit and integration tests for the `summarizeNft` method.


Patch:
diff --git a/src/api/nft-namespace.ts b/src/api/nft-namespace.ts
index 7f918a48..12be92f9 100644
--- a/src/api/nft-namespace.ts
+++ b/src/api/nft-namespace.ts
@@ -16,6 +16,7 @@ import {
   isSpamContract,
   refreshContract,
   refreshNftMetadata,
+  summarizeNftAttributes,
   verifyNftOwnership
 } from '../internal/nft-api';
 import {
@@ -30,6 +31,7 @@ import {
   GetOwnersForContractWithTokenBalancesResponse,
   GetOwnersForNftResponse,
   NftAttributeRarity,
+  NftAttributesResponse,
   NftContractBaseNftsResponse,
   NftContractNftsResponse,
   NftTokenType,
@@ -375,6 +377,17 @@ export class NftNamespace {
     return computeRarity(this.config, contractAddress, tokenId);
   }
 
+  /**
+   * Get a summary of attribute prevalence for an NFT collection.
+   *
+   * @param contractAddress - Contract address for the NFT collection.
+   */
+  summarizeNftAttributes(
+    contractAddress: string
+  ): Promise<NftAttributesResponse> {
+    return summarizeNftAttributes(this.config, contractAddress);
+  }
+
   /**
    * Refreshes the cached metadata for a provided NFT contract address and token
    * id. Returns a boolean value indicating whether the metadata was refreshed.
diff --git a/src/internal/nft-api.ts b/src/internal/nft-api.ts
index 1d6e4532..095d58af 100644
--- a/src/internal/nft-api.ts
+++ b/src/internal/nft-api.ts
@@ -14,6 +14,7 @@ import {
   GetOwnersForContractWithTokenBalancesResponse,
   GetOwnersForNftResponse,
   NftAttributeRarity,
+  NftAttributesResponse,
   NftContractBaseNftsResponse,
   NftContractNftsResponse,
   NftTokenType,
@@ -378,6 +379,19 @@ export async function computeRarity(
   return getNftRarityFromRaw(response);
 }
 
+export async function summarizeNftAttributes(
+  config: AlchemyConfig,
+  contractAddress: string,
+  srcMethod = 'summarizeNftAttributes'
+): Promise<NftAttributesResponse> {
+  return requestHttpWithBackoff<
+    SummarizeNftAttributesParams,
+    NftAttributesResponse
+  >(config, AlchemyApiType.NFT, 'summarizeNftAttributes', srcMethod, {
+    contractAddress
+  });
+}
+
 export async function refreshNftMetadata(
   config: AlchemyConfig,
   contractAddress: string,
@@ -603,6 +617,15 @@ interface ComputeRarityParams {
   tokenId: string;
 }
 
+/**
+ * Interface for the `summarizeNFTAttributes` endpoint.
+ *
+ * @internal
+ */
+interface SummarizeNftAttributesParams {
+  contractAddress: string;
+}
+
 interface ReingestContractParams {
   contractAddress: string;
 }
diff --git a/src/types/types.ts b/src/types/types.ts
index fe2868a4..872df0b1 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -781,6 +781,25 @@ export interface NftAttributeRarity {
   prevalence: number;
 }
 
+/**
+ * Summary of the attribute prevalence for the specified NFT contract.
+ *
+ * @public
+ */
+export interface NftAttributesResponse {
+  /** The specified NFT contract's address. */
+  contractAddress: string;
+
+  /** The specified NFT contract's total supply. */
+  totalSupply: number;
+
+  /**
+   * The attribute prevalence of each trait grouped by the trait type for the
+   * provided NFT.
+   */
+  summary: Record<string, Record<string, number>>;
+}
+
 /** The refresh result response object returned by {@link refreshContract}. */
 export interface RefreshContractResult {
   /** The NFT contract address that was passed in to be refreshed. */
@@ -1124,8 +1143,8 @@ export interface AlchemyMinedTransactionsEventFilter {
 }
 
 /**
- * Address filters for {@link AlchemyMinedTransactionsEventFilter}. Requires
- * at least one of the fields to be set.
+ * Address filters for {@link AlchemyMinedTransactionsEventFilter}. Requires at
+ * least one of the fields to be set.
  */
 export type AlchemyMinedTransactionsAddress = RequireAtLeastOne<{
   to?: string;
@@ -1140,8 +1159,10 @@ export type AlchemyMinedTransactionsAddress = RequireAtLeastOne<{
  */
 export type AlchemyEventType = EventType | AlchemyEventFilter;
 
-/** This type represents the Alchemy's Subscription API endpoints as event filters
- * compatible with other ethers events. */
+/**
+ * This type represents the Alchemy's Subscription API endpoints as event
+ * filters compatible with other ethers events.
+ */
 export type AlchemyEventFilter =
   | AlchemyMinedTransactionsEventFilter
   | AlchemyPendingTransactionsEventFilter;
@@ -1161,13 +1182,14 @@ export interface SendPrivateTransactionOptions {
 
 /**
  * Response object for the {@link TransactNamespace.sendGasOptimizedTransaction} method.
+ *
  * @internal
  */
 // TODO(txjob): Remove internal tag once this feature is released.
 export interface GasOptimizedTransactionResponse {
   /**
-   * The tracking id. This can be used to check the status of the transaction via
-   * {@link TransactNamespace.getGasOptimizedTransactionStatus}.
+   * The tracking id. This can be used to check the status of the transaction
+   * via {@link TransactNamespace.getGasOptimizedTransactionStatus}.
    */
   trackingId: string;
 
@@ -1175,8 +1197,12 @@ export interface GasOptimizedTransactionResponse {
   transactionHashes: string[];
 }
 
-/** Response object for the {@link TransactNamespace.getGasOptimizedTransactionStatus} method.
- * @internal*/
+/**
+ * Response object for the
+ * {@link TransactNamespace.getGasOptimizedTransactionStatus} method.
+ *
+ * @internal
+ */
 // TODO(txjob): Remove internal tag once this feature is released.
 export interface GasOptimizedTransactionStatusResponse {
   /** The status of the submitted transaction job. */
@@ -1189,8 +1215,11 @@ export interface GasOptimizedTransactionStatusResponse {
   minedTransactionHash?: string;
 }
 
-/** Potential transaction job statuses for a {@link GasOptimizedTransactionResponse}
- * @internal */
+/**
+ * Potential transaction job statuses for a {@link GasOptimizedTransactionResponse}
+ *
+ * @internal
+ */
 // TODO(txjob): Remove internal tag once this feature is released.
 export enum GasOptimizedTransactionStatus {
   UNSPECIFIED = 'TRANSACTION_JOB_STATUS_UNSPECIFIED',
@@ -1424,15 +1453,13 @@ export type AddressWebhookUpdate =
 /**
  * Requires at least one of the properties to be set.
  *
- * Implementation copied over
- * from {@link https://learn.microsoft.com/en-us/javascript/api/@azure/keyvault-certificates/requireatleastone?view=azure-node-latest}
+ * Implementation copied over from
+ * {@link https://learn.microsoft.com/en-us/javascript/api/@azure/keyvault-certificates/requireatleastone?view=azure-node-latest}
  */
 export type RequireAtLeastOne<T> = {
   [K in keyof T]-?: Required<Pick<T, K>> &
     Partial<Pick<T, Exclude<keyof T, K>>>;
 }[keyof T];
 
-/**
- * Requires an array with at least one value.
- */
+/** Requires an array with at least one value. */
 export type NonEmptyArray<T> = [T, ...T[]];


Test patch:
diff --git a/test/integration/nft.test.ts b/test/integration/nft.test.ts
index 5e798285..9cc0741e 100644
--- a/test/integration/nft.test.ts
+++ b/test/integration/nft.test.ts
@@ -253,6 +253,19 @@ describe('E2E integration tests', () => {
     expect(response[0].value).toBeDefined();
   });
 
+  it('summarizeNftAttributes()', async () => {
+    const contractAddress = '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d';
+
+    const response = await alchemy.nft.summarizeNftAttributes(contractAddress);
+
+    expect(response).toBeDefined();
+    expect(response.contractAddress).toBeDefined();
+    expect(response.contractAddress).toEqual(contractAddress);
+    expect(response.totalSupply).toBeDefined();
+    expect(typeof response.totalSupply).toEqual('number');
+    expect(response.summary).toBeDefined();
+  });
+
   it('refreshNftContract()', async () => {
     const contractAddress = '0x0510745d2ca36729bed35c818527c4485912d99e';
     const result = await alchemy.nft.refreshContract(contractAddress);
diff --git a/test/unit/nft-api.test.ts b/test/unit/nft-api.test.ts
index d8e984cb..4df1f426 100644
--- a/test/unit/nft-api.test.ts
+++ b/test/unit/nft-api.test.ts
@@ -8,6 +8,7 @@ import {
   GetNftsForOwnerOptions,
   GetOwnersForContractWithTokenBalancesResponse,
   Nft,
+  NftAttributesResponse,
   NftContract,
   NftContractBaseNftsResponse,
   NftContractNftsResponse,
@@ -1260,6 +1261,51 @@ describe('NFT module', () => {
     });
   });
 
+  describe('summarizeNftAttributes()', () => {
+    const contractAddress = '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d';
+    const templateResponse: NftAttributesResponse = {
+      contractAddress,
+      totalSupply: 1000,
+      summary: {
+        Background: {
+          Aquamarine: 3,
+          'New Punk Blue': 5
+        },
+        Eyes: {
+          Cyborg: 1,
+          Zombie: 7
+        }
+      }
+    };
+
+    it('calls with the correct parameters', async () => {
+      mock.onGet().reply(200, templateResponse);
+
+      const response = await alchemy.nft.summarizeNftAttributes(
+        contractAddress
+      );
+
+      expect(mock.history.get.length).toEqual(1);
+      expect(mock.history.get[0].params).toHaveProperty(
+        'contractAddress',
+        contractAddress
+      );
+      expect(response.contractAddress).toBeDefined();
+      expect(response.contractAddress).toEqual(contractAddress);
+      expect(response.totalSupply).toEqual(templateResponse.totalSupply);
+      expect(response.summary).toEqual(templateResponse.summary);
+    });
+
+    it('surfaces errors', async () => {
+      mock.reset();
+      mock.onGet().reply(400, 'No NFTs found for that contract!');
+
+      await expect(
+        alchemy.nft.summarizeNftAttributes(contractAddress)
+      ).rejects.toThrow('No NFTs found for that contract!');
+    });
+  });
+
   describe('refreshNftMetadata()', () => {
     const originalTimestamp = '2022-02-16T17:12:00.280Z';
     const updatedTimestamp = '2022-02-16T17:12:10.281Z';