Repository: alchemyplatform/alchemy-sdk-js
Pull request #58 (2022-07-14 18:33:48)

Add refreshNftContract() method
Adds the endpoint for `reingestContract`. Decided to rename to `refreshNftContract()` in order to match `refreshNftMetadata()`. 


Patch:
diff --git a/README.md b/README.md
index 80de8412..20efece0 100644
--- a/README.md
+++ b/README.md
@@ -164,8 +164,9 @@ under the `Alchemy` class:
 - `isSpamNftContract()`: Check whether the given NFT contract address is a spam contract as defined by Alchemy (see the [NFT API FAQ](https://docs.alchemy.com/alchemy/enhanced-apis/nft-api/nft-api-faq#nft-spam-classification))
 - `getSpamNftContracts()`: Returns a list of all spam contracts marked by Alchemy.
 - `findContractDeployer()`: Find the contract deployer and block number for a given NFT contract address.
-- `refreshNftMetadata()`: Refresh the cached NFT metadata for a contract address and tokenId.
-- `getNftFloorPrice()`: Returns the floor prices of a NFT contract by marketplace.
+- `refreshNftMetadata()`: Refresh the cached NFT metadata for a contract address and a single tokenId.
+- `refreshNftContract()`: Enqueues the specified contract address to have all token ids' metadata refreshed. 
+- `getNftFloorPrice()`: Return the floor prices of a NFT contract by marketplace.
 
 ### Comparing `BaseNft` and `Nft`
 
diff --git a/src/api/alchemy.ts b/src/api/alchemy.ts
index 9dcbb4e9..bcab64ff 100644
--- a/src/api/alchemy.ts
+++ b/src/api/alchemy.ts
@@ -19,6 +19,7 @@ import {
   OwnedBaseNftsResponse,
   OwnedNft,
   OwnedNftsResponse,
+  RefreshNftContractResult,
   TokenBalancesResponse,
   TokenMetadataResponse,
   TransactionReceiptsParams,
@@ -49,7 +50,8 @@ import {
   getSpamNftContracts,
   getNftFloorPrice,
   findContractDeployer,
-  refreshNftMetadata
+  refreshNftMetadata,
+  refreshNftContract
 } from '../internal/nft-api';
 import { formatBlock } from '../util/util';
 import { toHex } from './util';
@@ -429,6 +431,9 @@ export class Alchemy {
    * The last refresh time for an NFT can be accessed on the
    * {@link Nft.timeLastUpdated} field.
    *
+   * To trigger a refresh for all NFTs in a contract, use
+   * {@link refreshNftContract} instead.
+   *
    * @param contractAddress - The contract address of the NFT.
    * @param tokenId - The token id of the NFT.
    */
@@ -444,6 +449,9 @@ export class Alchemy {
    * has been updated since the last time it was fetched. Note that the backend
    * only allows one refresh per token every 15 minutes, globally for all users.
    *
+   * To trigger a refresh for all NFTs in a contract, use
+   * {@link refreshNftContract} instead.
+   *
    * @param nft - The NFT to refresh the metadata for.
    */
   refreshNftMetadata(nft: BaseNft): Promise<boolean>;
@@ -454,6 +462,38 @@ export class Alchemy {
     return refreshNftMetadata(this, contractAddressOrBaseNft, tokenId);
   }
 
+  /**
+   * Triggers a metadata refresh all NFTs in the provided contract address. This
+   * method is useful after an NFT collection is revealed.
+   *
+   * Refreshes are queued on the Alchemy backend and may take time to fully
+   * process. To refresh the metadata for a specific token, use the
+   * {@link refreshNftMetadata} method instead.
+   *
+   * @param contractAddress - The contract address of the NFT collection.
+   * @beta
+   */
+  refreshNftContract(
+    contractAddress: string
+  ): Promise<RefreshNftContractResult>;
+  /**
+   * Triggers a metadata refresh all NFTs in the provided contract address. This
+   * method is useful after an NFT collection is revealed.
+   *
+   * Refreshes are queued on the Alchemy backend and may take time to fully
+   * process. To refresh the metadata for a specific token, use the
+   * {@link refreshNftMetadata} method instead.
+   *
+   * @param nft - The contract address of the NFT collection.
+   * @beta
+   */
+  refreshNftContract(nft: BaseNft): Promise<RefreshNftContractResult>;
+  refreshNftContract(
+    contractAddressOrBaseNft: string | BaseNft
+  ): Promise<RefreshNftContractResult> {
+    return refreshNftContract(this, contractAddressOrBaseNft);
+  }
+
   /**
    * Returns the token balances for a specific owner address given a list of contracts.
    *
diff --git a/src/internal/nft-api.ts b/src/internal/nft-api.ts
index ae5801ae..a72c3fd8 100644
--- a/src/internal/nft-api.ts
+++ b/src/internal/nft-api.ts
@@ -15,7 +15,9 @@ import {
   OwnedBaseNft,
   OwnedBaseNftsResponse,
   OwnedNft,
-  OwnedNftsResponse
+  OwnedNftsResponse,
+  RefreshNftContractResult,
+  RefreshState
 } from '../types/types';
 import { paginateEndpoint, requestHttpWithBackoff } from './dispatch';
 import {
@@ -30,7 +32,8 @@ import {
   RawNftContractBaseNft,
   RawNftContractNft,
   RawOwnedBaseNft,
-  RawOwnedNft
+  RawOwnedNft,
+  RawReingestContractResponse
 } from './raw-interfaces';
 import { AlchemyApiType } from '../util/const';
 import {
@@ -378,6 +381,33 @@ export async function refreshNftMetadata(
   return first.timeLastUpdated !== second.timeLastUpdated;
 }
 
+export async function refreshNftContract(
+  alchemy: Alchemy,
+  contractAddressOrBaseNft: string | BaseNft
+): Promise<RefreshNftContractResult> {
+  let response;
+  if (typeof contractAddressOrBaseNft === 'string') {
+    response = await requestHttpWithBackoff<
+      ReingestContractParams,
+      RawReingestContractResponse
+    >(alchemy, AlchemyApiType.NFT, 'reingestContract', {
+      contractAddress: contractAddressOrBaseNft
+    });
+  } else {
+    response = await requestHttpWithBackoff<
+      ReingestContractParams,
+      RawReingestContractResponse
+    >(alchemy, AlchemyApiType.NFT, 'reingestContract', {
+      contractAddress: contractAddressOrBaseNft.contract.address
+    });
+  }
+  return {
+    contractAddress: response.contractAddress,
+    refreshState: parseReingestionState(response.reingestionState),
+    progress: response.progress
+  };
+}
+
 async function refresh(
   alchemy: Alchemy,
   contractAddress: string,
@@ -473,6 +503,25 @@ function omitMetadataToWithMetadata(
   return omitMetadata === undefined ? true : !omitMetadata;
 }
 
+function parseReingestionState(reingestionState: string): RefreshState {
+  switch (reingestionState) {
+    case 'does_not_exist':
+      return RefreshState.DOES_NOT_EXIST;
+    case 'already_queued':
+      return RefreshState.ALREADY_QUEUED;
+    case 'in_progress':
+      return RefreshState.IN_PROGRESS;
+    case 'finished':
+      return RefreshState.FINISHED;
+    case 'queued':
+      return RefreshState.QUEUED;
+    case 'queue_failed':
+      return RefreshState.QUEUE_FAILED;
+    default:
+      throw new Error('Unknown reingestion state: ' + reingestionState);
+  }
+}
+
 /**
  * Interface for the `getNftsForNftContract` endpoint. The main difference is
  * that the endpoint has a `startToken` parameter, but the SDK standardizes all
@@ -548,3 +597,7 @@ interface GetOwnersForNftContractAlchemyParams {
 interface GetFloorPriceParams {
   contractAddress: string;
 }
+
+interface ReingestContractParams {
+  contractAddress: string;
+}
diff --git a/src/internal/raw-interfaces.ts b/src/internal/raw-interfaces.ts
index a5b711ba..5ece2c45 100644
--- a/src/internal/raw-interfaces.ts
+++ b/src/internal/raw-interfaces.ts
@@ -165,3 +165,9 @@ export interface RawNftContractNft extends RawNft, RawNftContractBaseNft {}
 export interface RawGetOwnersForNftContractResponse {
   ownerAddresses: string[];
 }
+
+export interface RawReingestContractResponse {
+  contractAddress: string;
+  reingestionState: string;
+  progress: string | null;
+}
diff --git a/src/types/types.ts b/src/types/types.ts
index e5cf2fd4..51a80086 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -406,6 +406,42 @@ export interface GetNftFloorPriceResponse {
   readonly looksRare: FloorPriceMarketplace | FloorPriceError;
 }
 
+/** The refresh result response object returned by {@link refreshNftContract}. */
+export interface RefreshNftContractResult {
+  /** The NFT contract address that was passed in to be refreshed. */
+  contractAddress: string;
+
+  /** The current state of the refresh request. */
+  refreshState: RefreshState;
+
+  /**
+   * Percentage of tokens currently refreshed, represented as an integer string.
+   * Field can be null if the refresh has not occurred.
+   */
+  progress: string | null;
+}
+
+/** The current state of the NFT contract refresh process. */
+export enum RefreshState {
+  /** The provided contract is not an NFT or does not contain metadata. */
+  DOES_NOT_EXIST = 'does_not_exist',
+
+  /** The contract has already been queued for refresh. */
+  ALREADY_QUEUED = 'already_queued',
+
+  /** The contract is currently being refreshed. */
+  IN_PROGRESS = 'in_progress',
+
+  /** The contract refresh is complete. */
+  FINISHED = 'finished',
+
+  /** The contract refresh has been queued and await execution. */
+  QUEUED = 'queued',
+
+  /** The contract was unable to be queued due to an internal error. */
+  QUEUE_FAILED = 'queue_failed'
+}
+
 /** @public */
 export interface TransactionReceiptsBlockNumber {
   blockNumber: string;


Test patch:
diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts
index e3932a8b..167d2152 100644
--- a/test/integration/integration.test.ts
+++ b/test/integration/integration.test.ts
@@ -167,6 +167,12 @@ describe('E2E integration tests', () => {
     await alchemy.refreshNftMetadata(nft);
   });
 
+  it('refreshNftContract()', async () => {
+    const contractAddress = '0x0510745d2ca36729bed35c818527c4485912d99e';
+    const result = await alchemy.refreshNftContract(contractAddress);
+    console.log('result', result);
+  });
+
   describe('README examples', () => {
     it('Example 1: Getting the Nfts owned by an address', async () => {
       void alchemy.getNftsForOwner('0xshah.eth').then(nfts => {
diff --git a/test/unit/nft-api.test.ts b/test/unit/nft-api.test.ts
index 7d60f32a..7c16d5d9 100644
--- a/test/unit/nft-api.test.ts
+++ b/test/unit/nft-api.test.ts
@@ -13,7 +13,8 @@ import {
   OwnedBaseNftsResponse,
   OwnedNft,
   OwnedNftsResponse,
-  Nft
+  Nft,
+  RefreshState
 } from '../../src';
 import axios from 'axios';
 import MockAdapter from 'axios-mock-adapter';
@@ -1181,6 +1182,57 @@ describe('NFT module', () => {
     });
   });
 
+  describe('refreshNftContract', () => {
+    const contractAddress = '0xCA1';
+    const refreshResponse = {
+      contractAddress,
+      reingestionState: 'queued',
+      progress: '5'
+    };
+    beforeEach(() => {
+      mock.onGet().reply(200, refreshResponse);
+    });
+    it('calls with the correct parameters', async () => {
+      const response = await alchemy.refreshNftContract(contractAddress);
+      expect(mock.history.get.length).toEqual(1);
+      expect(mock.history.get[0].params).toHaveProperty(
+        'contractAddress',
+        contractAddress
+      );
+      expect(response).toEqual({
+        contractAddress,
+        refreshState: RefreshState.QUEUED,
+        progress: '5'
+      });
+    });
+
+    it('can be called with BaseNft', async () => {
+      const response = await alchemy.refreshNftContract(
+        createBaseNft(contractAddress, '0x42')
+      );
+      expect(mock.history.get.length).toEqual(1);
+      expect(mock.history.get[0].params).toHaveProperty(
+        'contractAddress',
+        contractAddress
+      );
+
+      expect(response).toEqual({
+        contractAddress,
+        refreshState: RefreshState.QUEUED,
+        progress: '5'
+      });
+    });
+
+    it('retries with maxAttempts', async () => {
+      mock.reset();
+      mock.onGet().reply(429, 'Too many requests');
+
+      await expect(alchemy.refreshNftContract(contractAddress)).rejects.toThrow(
+        'Too many requests'
+      );
+    });
+  });
+
   // TODO: add unit test coverage for the endpoint after overriding the ethers.js
   // AlchemyProvider class, which would make mocking returns much easier.
   describe('findContractDeployer()', () => {