Repository: alchemyplatform/alchemy-sdk-js
Pull request #27 (2022-06-08 21:30:04)

Add getOwnersForCollection endpoint + cleanup README
Adding `getOwnersForCollection()` endpoint along with some README cleanup.
- Note that developer return type is `owners` whereas the backend returns it under `ownerAddresses`. This is done to standardize with `getOwnersForNft()`, which also returns as `owners`.


Patch:
diff --git a/README.md b/README.md
index 337372a9..924baf42 100644
--- a/README.md
+++ b/README.md
@@ -80,15 +80,16 @@ Consult the [Ethers.js documentation](https://docs.ethers.io/v5/) for how to use
 
 The SDK currently supports the following [NFT API](https://docs.alchemy.com/alchemy/enhanced-apis/nft-api) endpoints:
 
-- `getNftMetadata()`: Gets the NFT metadata for a contract address and tokenId.
+- `getNftMetadata()`: Get the NFT metadata for a contract address and tokenId.
 - `getNftsForOwner()`: Get NFTs for an owner address.
 - `getNftsForOwnerIterator()`: Get NFTs for an owner address as an async iterator (handles paging automatically).
-- `getNftsForCollection()`: Gets all NFTs for a contract address.
-- `getNftForCollectionIterator()`: Gets all NFTs for a contract address as an async iterator (handles paging automatically).
-- `getOwnersForNft()`: Get all the owners for a given NFT contract address and token ID.
-- `checkNftOwnership()`: Checks that the provided owner address owns one or more of the provided NFT contract addresses.
-- `findContractDeployer()`: Finds the contract deployer and block number for a given NFT contract address.
-- `refreshNftMetadata()`: Refreshes the cached NFT metadata for a contract address and tokenId.
+- `getNftsForCollection()`: Get all NFTs for a contract address.
+- `getNftForCollectionIterator()`: Get all NFTs for a contract address as an async iterator (handles paging automatically).
+- `getOwnersForNft()`: Get all the owners for a given NFT contract address and a particular token ID.
+- `getOwnersForCollection()`: Get all the owners for a given NFT contract address.
+- `checkNftOwnership()`: Check that the provided owner address owns one or more of the provided NFT contract addresses.
+- `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.
 
 ### Comparing `BaseNft` and `Nft`
 
@@ -148,10 +149,8 @@ or as a webpage by opening `docs/index.html` in your browser.
 There's a long list, but here are the main ones:
 
 - Websocket support
-- Retry count support for json-rpc calls (currently only HTTP calls are retried)
+- Retry count support for json-rpc calls (currently only for NFT API calls).
 - More config options for the base ethers.js AlchemyProvider.
-- Most of these changes require extending the base ethers.js SDK with custom implementations.
-- Separating SDK into separate packages.
 
 ## Examples
 
diff --git a/src/api/nft-api.ts b/src/api/nft-api.ts
index c2a1470e..57df33e3 100644
--- a/src/api/nft-api.ts
+++ b/src/api/nft-api.ts
@@ -6,6 +6,7 @@ import {
   GetBaseNftsForOwnerOptions,
   GetNftsForCollectionOptions,
   GetNftsForOwnerOptions,
+  GetOwnersForCollectionResponse,
   GetOwnersForNftResponse,
   NftTokenType,
   OwnedBaseNft,
@@ -24,6 +25,7 @@ import {
   RawGetBaseNftsResponse,
   RawGetNftsForCollectionResponse,
   RawGetNftsResponse,
+  RawGetOwnersForCollectionResponse,
   RawNft,
   RawOwnedBaseNft,
   RawOwnedNft
@@ -323,6 +325,55 @@ export function getOwnersForNft(
   }
 }
 
+/**
+ * Gets all the owners for a given NFT collection.
+ *
+ * @param alchemy - The Alchemy SDK instance.
+ * @param contractAddress - The NFT collection to get the owners for.
+ * @beta
+ */
+export function getOwnersForCollection(
+  alchemy: Alchemy,
+  contractAddress: string
+): Promise<GetOwnersForCollectionResponse>;
+
+/**
+ * Gets all the owners for a given NFT collection.
+ *
+ * @param alchemy - The Alchemy SDK instance.
+ * @param nft - The NFT to get the owners of the collection for.
+ * @beta
+ */
+export function getOwnersForCollection(
+  alchemy: Alchemy,
+  nft: BaseNft
+): Promise<GetOwnersForCollectionResponse>;
+export async function getOwnersForCollection(
+  alchemy: Alchemy,
+  contractAddressOrNft: string | BaseNft
+): Promise<GetOwnersForCollectionResponse> {
+  let response;
+  if (typeof contractAddressOrNft === 'string') {
+    response = await requestHttpWithBackoff<
+      GetOwnersForCollectionAlchemyParams,
+      RawGetOwnersForCollectionResponse
+    >(alchemy, 'getOwnersForCollection', {
+      contractAddress: contractAddressOrNft
+    });
+  } else {
+    response = await requestHttpWithBackoff<
+      GetOwnersForCollectionAlchemyParams,
+      RawGetOwnersForCollectionResponse
+    >(alchemy, 'getOwnersForCollection', {
+      contractAddress: contractAddressOrNft.contract.address
+    });
+  }
+
+  return {
+    owners: response.ownerAddresses
+  };
+}
+
 /**
  * Fetches all NFTs for a given contract address and yields them in an async iterable.
  *
@@ -642,3 +693,12 @@ interface GetNftMetadataParams {
   tokenType?: NftTokenType;
   refreshCache?: boolean;
 }
+
+/**
+ * Interface for the `getOwnersForCollection` endpoint.
+ *
+ * @internal
+ */
+interface GetOwnersForCollectionAlchemyParams {
+  contractAddress: string;
+}
diff --git a/src/index.ts b/src/index.ts
index 2e119c53..24457bb0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -11,6 +11,7 @@ export {
   getNftsForCollection,
   getNftsForCollectionIterator,
   getOwnersForNft,
+  getOwnersForCollection,
   checkNftOwnership,
   findContractDeployer,
   refreshNftMetadata
diff --git a/src/internal/raw-interfaces.ts b/src/internal/raw-interfaces.ts
index bc61b930..d04c8934 100644
--- a/src/internal/raw-interfaces.ts
+++ b/src/internal/raw-interfaces.ts
@@ -137,3 +137,12 @@ export interface RawCollectionBaseNft {
  * @internal
  */
 export interface RawCollectionNft extends RawNft, RawCollectionBaseNft {}
+
+/**
+ * Represents Alchemy's HTTP response for `getOwnersForCollection`.
+ *
+ * @internal
+ */
+export interface RawGetOwnersForCollectionResponse {
+  ownerAddresses: string[];
+}
diff --git a/src/types/types.ts b/src/types/types.ts
index 63aa3d3b..77c73e99 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -95,6 +95,12 @@ export enum AssetTransfersCategory {
   ERC20 = 'erc20',
   ERC721 = 'erc721',
   ERC1155 = 'erc1155',
+
+  /**
+   * Special contracts that don't follow ERC 721/1155, (ex: CryptoKitties).
+   *
+   * @beta
+   */
   SPECIALNFT = 'specialnft'
 }
 
@@ -312,6 +318,16 @@ export interface GetOwnersForNftResponse {
   readonly owners: string[];
 }
 
+/**
+ * The response object for the {@link getOwnersForCollection}.
+ *
+ * @public
+ */
+export interface GetOwnersForCollectionResponse {
+  /** An array of owner addresses for the provided contract address */
+  readonly owners: string[];
+}
+
 /** @public */
 export interface TransactionReceiptsBlockNumber {
   blockNumber: string;


Test patch:
diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts
index c8847647..391e9a49 100644
--- a/test/integration/integration.test.ts
+++ b/test/integration/integration.test.ts
@@ -6,6 +6,7 @@ import {
   getNftsForCollectionIterator,
   getNftsForOwner,
   getNftsForOwnerIterator,
+  getOwnersForCollection,
   getOwnersForNft,
   getTokenBalances,
   initializeAlchemy,
@@ -101,6 +102,11 @@ describe('E2E integration tests', () => {
     expect(withSpam.totalCount).not.toEqual(noSpam.totalCount);
   });
 
+  it('getOwnersForCollection', async () => {
+    const owners = await getOwnersForCollection(alchemy, contractAddress);
+    console.log('owners', owners);
+  });
+
   it('getNftsForCollection with pageKey', async () => {
     let nftsForCollection = await getNftsForCollection(
       alchemy,
diff --git a/test/unit/nft-api.test.ts b/test/unit/nft-api.test.ts
index d6abda09..f32fdf55 100644
--- a/test/unit/nft-api.test.ts
+++ b/test/unit/nft-api.test.ts
@@ -11,6 +11,7 @@ import {
   getNftsForOwner,
   getNftsForOwnerIterator,
   GetNftsForOwnerOptions,
+  getOwnersForCollection,
   getOwnersForNft,
   initializeAlchemy,
   Nft,
@@ -859,6 +860,50 @@ describe('NFT module', () => {
     });
   });
 
+  describe('getOwnersForCollection()', () => {
+    const contractAddress = '0xCA1';
+    const tokenIdHex = '0x1b7';
+    const owners = ['0x1', '0x2', '0x3'];
+
+    beforeEach(() => {
+      mock.onGet().reply(200, {
+        ownerAddresses: owners
+      });
+    });
+
+    it('calls with the correct parameters', async () => {
+      await getOwnersForCollection(alchemy, contractAddress);
+      expect(mock.history.get.length).toEqual(1);
+      expect(mock.history.get[0].params).toHaveProperty(
+        'contractAddress',
+        contractAddress
+      );
+    });
+
+    it('can be called with BaseNft', async () => {
+      const response = await getOwnersForCollection(
+        alchemy,
+        createBaseNft(contractAddress, tokenIdHex)
+      );
+      expect(mock.history.get.length).toEqual(1);
+      expect(mock.history.get[0].params).toHaveProperty(
+        'contractAddress',
+        contractAddress
+      );
+
+      expect(response).toEqual({ owners });
+    });
+
+    it('retries with maxAttempts', async () => {
+      mock.reset();
+      mock.onGet().reply(429, 'Too many requests');
+
+      await expect(
+        getOwnersForNft(alchemy, contractAddress, tokenIdHex)
+      ).rejects.toThrow('Too many requests');
+    });
+  });
+
   describe('checkOwnership', () => {
     const owner = '0xABC';
     const addresses = ['0xCA1', '0xCA2'];