Repository: alchemyplatform/alchemy-sdk-js
Pull request #163 (2022-10-28 02:40:25)

Nft api search contract metadata
### Changelog

- Adds the `searchContractMetadata` method in the `nft` namespace.
- Refactors unit tests for the `getNftContractMetadata` method.
- Moves the `verifyNftContractMetadata` util function to the `test/test-util.ts` file to reuse it for the `searchContractMetadata` unit tests.
- Adds unit and integration tests for `searchContractMetadata` method.


Patch:
diff --git a/src/api/nft-namespace.ts b/src/api/nft-namespace.ts
index 12be92f9..2e6aa18d 100644
--- a/src/api/nft-namespace.ts
+++ b/src/api/nft-namespace.ts
@@ -16,6 +16,7 @@ import {
   isSpamContract,
   refreshContract,
   refreshNftMetadata,
+  searchContractMetadata,
   summarizeNftAttributes,
   verifyNftOwnership
 } from '../internal/nft-api';
@@ -377,6 +378,15 @@ export class NftNamespace {
     return computeRarity(this.config, contractAddress, tokenId);
   }
 
+  /**
+   * Search for a keyword across metadata of all ERC-721 and ERC-1155 smart contracts.
+   *
+   * @param query - The search string that you want to search for in contract metadata.
+   */
+  searchContractMetadata(query: string): Promise<NftContract[]> {
+    return searchContractMetadata(this.config, query);
+  }
+
   /**
    * Get a summary of attribute prevalence for an NFT collection.
    *
diff --git a/src/internal/nft-api.ts b/src/internal/nft-api.ts
index 095d58af..6a7d2e1c 100644
--- a/src/internal/nft-api.ts
+++ b/src/internal/nft-api.ts
@@ -29,6 +29,7 @@ import { AlchemyApiType } from '../util/const';
 import {
   getBaseNftFromRaw,
   getNftContractFromRaw,
+  getNftContractsFromRaw,
   getNftFromRaw,
   getNftRarityFromRaw
 } from '../util/util';
@@ -379,6 +380,21 @@ export async function computeRarity(
   return getNftRarityFromRaw(response);
 }
 
+export async function searchContractMetadata(
+  config: AlchemyConfig,
+  query: string,
+  srcMethod = 'searchContractMetadata'
+): Promise<NftContract[]> {
+  const response = await requestHttpWithBackoff<
+    SearchContractMetadataParams,
+    RawNftContract[]
+  >(config, AlchemyApiType.NFT, 'searchContractMetadata', srcMethod, {
+    query
+  });
+
+  return getNftContractsFromRaw(response);
+}
+
 export async function summarizeNftAttributes(
   config: AlchemyConfig,
   contractAddress: string,
@@ -617,6 +633,15 @@ interface ComputeRarityParams {
   tokenId: string;
 }
 
+/**
+ * Interface for the `searchContractMetadata` endpoint.
+ *
+ * @internal
+ */
+interface SearchContractMetadataParams {
+  query: string;
+}
+
 /**
  * Interface for the `summarizeNFTAttributes` endpoint.
  *
diff --git a/src/util/util.ts b/src/util/util.ts
index 3f1b628b..0917c38a 100644
--- a/src/util/util.ts
+++ b/src/util/util.ts
@@ -44,6 +44,14 @@ export function getNftContractFromRaw(
   };
 }
 
+export function getNftContractsFromRaw(
+  rawNftContracts: RawNftContract[]
+): NftContract[] {
+  return rawNftContracts.map(rawNftContract =>
+    getNftContractFromRaw(rawNftContract)
+  );
+}
+
 export function getBaseNftFromRaw(
   rawBaseNft: RawBaseNft,
   contractAddress: string


Test patch:
diff --git a/test/integration/nft.test.ts b/test/integration/nft.test.ts
index 9cc0741e..6672706b 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('searchContractMetadata()', async () => {
+    const query = 'meta alchemy';
+
+    const response = await alchemy.nft.searchContractMetadata(query);
+
+    expect(response).toBeDefined();
+    expect(response.length).toBeGreaterThan(0);
+    expect(response[0].address).toBeDefined();
+    expect(typeof response[0].address).toEqual('string');
+    expect(response[0].tokenType).toBeDefined();
+    expect(response[0].tokenType).toEqual(NftTokenType.ERC721);
+  });
+
   it('summarizeNftAttributes()', async () => {
     const contractAddress = '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d';
 
diff --git a/test/test-util.ts b/test/test-util.ts
index be7f88b2..32a63ada 100644
--- a/test/test-util.ts
+++ b/test/test-util.ts
@@ -8,7 +8,8 @@ import {
   OwnedBaseNft,
   OwnedNft,
   TokenUri,
-  toHex
+  toHex,
+  NftContract
 } from '../src';
 import {
   RawBaseNft,
@@ -203,6 +204,23 @@ export function createRawNftContractBaseNft(
   };
 }
 
+export function verifyNftContractMetadata(
+  actualNftContract: NftContract,
+  expectedNftContract: NftContract,
+  address: string,
+  name: string,
+  symbol: string,
+  totalSupply: string,
+  tokenType?: NftTokenType
+) {
+  expect(actualNftContract).toEqual(expectedNftContract);
+  expect(actualNftContract.address).toEqual(address);
+  expect(actualNftContract.name).toEqual(name);
+  expect(actualNftContract.symbol).toEqual(symbol);
+  expect(actualNftContract.totalSupply).toEqual(totalSupply);
+  expect(actualNftContract.tokenType).toEqual(tokenType);
+}
+
 export type Mocked<T> = T & {
   [K in keyof T]: T[K] extends Function ? T[K] & jest.Mock : T[K];
 };
diff --git a/test/unit/nft-api.test.ts b/test/unit/nft-api.test.ts
index 4df1f426..f23f8306 100644
--- a/test/unit/nft-api.test.ts
+++ b/test/unit/nft-api.test.ts
@@ -4,12 +4,12 @@ import MockAdapter from 'axios-mock-adapter';
 import {
   Alchemy,
   BaseNft,
+  fromHex,
   GetFloorPriceResponse,
   GetNftsForOwnerOptions,
   GetOwnersForContractWithTokenBalancesResponse,
   Nft,
   NftAttributesResponse,
-  NftContract,
   NftContractBaseNftsResponse,
   NftContractNftsResponse,
   NftExcludeFilters,
@@ -18,8 +18,7 @@ import {
   OwnedBaseNftsResponse,
   OwnedNft,
   OwnedNftsResponse,
-  RefreshState,
-  fromHex
+  RefreshState
 } from '../../src';
 import {
   RawGetBaseNftsForContractResponse,
@@ -43,7 +42,8 @@ import {
   createRawNftContract,
   createRawNftContractBaseNft,
   createRawOwnedBaseNft,
-  createRawOwnedNft
+  createRawOwnedNft,
+  verifyNftContractMetadata
 } from '../test-util';
 
 describe('NFT module', () => {
@@ -83,33 +83,21 @@ describe('NFT module', () => {
       mock.onGet().reply(200, rawNftContractResponse);
     });
 
-    function verifyNftContractMetadata(
-      actualNftContract: NftContract,
-      expectedNftContract: NftContract,
-      address: string,
-      name: string,
-      symbol: string,
-      totalSupply: string,
-      tokenType?: NftTokenType
-    ) {
-      expect(actualNftContract).toEqual(expectedNftContract);
-
-      expect(actualNftContract.address).toEqual(address);
-      expect(actualNftContract.name).toEqual(name);
-      expect(actualNftContract.symbol).toEqual(symbol);
-      expect(actualNftContract.totalSupply).toEqual(totalSupply);
-      expect(actualNftContract.tokenType).toEqual(tokenType);
+    it('can be called with raw parameters', async () => {
+      await alchemy.nft.getContractMetadata(address);
 
       expect(mock.history.get.length).toEqual(1);
       expect(mock.history.get[0].params).toHaveProperty(
         'contractAddress',
         address
       );
-    }
+    });
+
+    it('returns the api response in the expected format', async () => {
+      const response = await alchemy.nft.getContractMetadata(address);
 
-    it('can be called with raw parameters', async () => {
       verifyNftContractMetadata(
-        await alchemy.nft.getContractMetadata(address),
+        response,
         expectedNftContract,
         address,
         name,
@@ -1261,6 +1249,62 @@ describe('NFT module', () => {
     });
   });
 
+  describe('searchContractMetadata()', () => {
+    const query = 'alchemy';
+    const address = '0xf6e12a3b482c8d51a0f66e6d80c496c310833389';
+    const name = 'NFT Contract Name';
+    const symbol = 'AXN';
+    const totalSupply = '1155';
+    const tokenType = NftTokenType.ERC721;
+
+    const rawNftContractResponse = createRawNftContract(
+      address,
+      tokenType,
+      name,
+      symbol,
+      totalSupply
+    );
+    const templateResponse = [rawNftContractResponse];
+    const expectedNftContract = getNftContractFromRaw(rawNftContractResponse);
+
+    beforeEach(() => {
+      mock.onGet().reply(200, templateResponse);
+    });
+
+    it('calls with the correct parameters', async () => {
+      await alchemy.nft.searchContractMetadata(query);
+
+      expect(mock.history.get.length).toEqual(1);
+      expect(mock.history.get[0].params).toHaveProperty('query', query);
+    });
+
+    it('returns the api response in the expected format', async () => {
+      const response = await alchemy.nft.searchContractMetadata(query);
+
+      expect(response.length).toEqual(1);
+      verifyNftContractMetadata(
+        response[0],
+        expectedNftContract,
+        address,
+        name,
+        symbol,
+        totalSupply,
+        tokenType
+      );
+    });
+
+    it('surfaces errors', async () => {
+      mock.reset();
+      mock
+        .onGet()
+        .reply(429, 'Your app has exceeded its concurrent requests capacity.');
+
+      await expect(alchemy.nft.searchContractMetadata(query)).rejects.toThrow(
+        'Your app has exceeded its concurrent requests capacity.'
+      );
+    });
+  });
+
   describe('summarizeNftAttributes()', () => {
     const contractAddress = '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d';
     const templateResponse: NftAttributesResponse = {