Repository: alchemyplatform/alchemy-sdk-js
Pull request #129 (2022-09-28 18:02:18)

Add verifyNftOwnership()
The `checkNftOwnership()` method is non-intuitive since it returns `true` if the provided owner owns any of the provided NFT addresses. This PR adds support for `verifyNftOwnership()`, which returns a single boolean value if only a single contract address is provided, or a mapping of contractAddress to the ownership result boolean if multiple contract addresses are provided. 

`checkNftOwnership()` is now marked as deprecated.

deric-alchemy: Did we ever support checkNftOwnership in SDK? Are we backwards compatible here? 

thebrianchen: > Did we ever support checkNftOwnership in SDK? Are we backwards compatible here?

Yeah, `checkNftOwnership()` was just a wrapped call around `getNFTs`. The old method still works, and will just be removed with the next breaking change window.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9fe904bb..3ca3dde2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,11 +7,13 @@
 - POTENTIALLY BREAKING: Fixed a typing bug where the `totalSupply` field in an `NftContract` should have type `string` instead of `number`.
 - Updated the `Nft` class to include the contract metadata in the `Nft.contract` field.
 - Added commonly used utility methods from ethers.js into a top-level `Utils` export.
+- Added the `NftNamespace.verifyNftOwnership()` method to replace the deprecated `checkNftOwnership()` method. 
 - Added `Contract` and `ContractFactory` exports from ethers into the top-level exports.  
 
 ### Minor Changes
 
 - Added support for the `erc20` token type and pagination for `CoreNamespace.getTokenBalances()`.
+- Added `bytes` field to the `Media` object in the NFT metadata responses to indicate the size of the media in bytes. Note that the `size` field is not supported by the backend and will be removed in the next version.  
 
 ## 2.0.4
 
diff --git a/README.md b/README.md
index 2674ed18..903ee9e9 100644
--- a/README.md
+++ b/README.md
@@ -170,7 +170,7 @@ under the `alchemy.nft` namespace:
   automatically).
 - `getOwnersForNft()`: Get all the owners for a given NFT contract address and a particular token ID.
 - `getOwnersForContract()`: 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.
+- `verifyNftOwnership()`: Check whether the provided owner address owns the provided NFT contract addresses.
 - `isSpamContract()`: 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))
 - `getSpamContracts()`: Returns a list of all spam contracts marked by Alchemy.
 - `findContractDeployer()`: Find the contract deployer and block number for a given NFT contract address.
diff --git a/src/api/nft-namespace.ts b/src/api/nft-namespace.ts
index 92c197cb..fa058a1b 100644
--- a/src/api/nft-namespace.ts
+++ b/src/api/nft-namespace.ts
@@ -32,7 +32,8 @@ import {
   getSpamContracts,
   isSpamContract,
   refreshContract,
-  refreshNftMetadata
+  refreshNftMetadata,
+  verifyNftOwnership
 } from '../internal/nft-api';
 
 /**
@@ -254,11 +255,13 @@ export class NftNamespace {
   }
 
   /**
-   * Checks that the provided owner address owns one of more of the provided NFTs.
+   * DEPRECATED - Checks that the provided owner address owns one of more of the
+   * provided NFTs.
    *
+   * @deprecated - Use {@link verifyNftOwnership} instead. This method will be
+   *   removed in a future release.
    * @param owner - The owner address to check.
    * @param contractAddresses - An array of NFT contract addresses to check ownership for.
-   * @beta
    */
   checkNftOwnership(
     owner: string,
@@ -267,6 +270,33 @@ export class NftNamespace {
     return checkNftOwnership(this.config, owner, contractAddresses);
   }
 
+  /**
+   * Checks that the provided owner address owns one of more of the provided
+   * NFT. Returns a boolean indicating whether the owner address owns the provided NFT.
+   *
+   * @param owner - The owner address to check.
+   * @param contractAddress - An NFT contract address to check ownership for.
+   */
+  verifyNftOwnership(owner: string, contractAddress: string): Promise<boolean>;
+
+  /**
+   * Checks which of the provided NFTs the owner address owns. Returns a map of
+   * contract address to a boolean indicating whether the owner address owns the NFT.
+   *
+   * @param owner - The owner address to check.
+   * @param contractAddresses - An array NFT contract address to check ownership for.
+   */
+  verifyNftOwnership(
+    owner: string,
+    contractAddresses: string[]
+  ): Promise<{ [contractAddress: string]: boolean }>;
+  verifyNftOwnership(
+    owner: string,
+    contractAddress: string | string[]
+  ): Promise<boolean | { [contractAddress: string]: boolean }> {
+    return verifyNftOwnership(this.config, owner, contractAddress);
+  }
+
   /**
    * Returns whether a contract is marked as spam or not by Alchemy. For more
    * information on how we classify spam, go to our NFT API FAQ at
diff --git a/src/internal/nft-api.ts b/src/internal/nft-api.ts
index d3b9f782..0abea3cb 100644
--- a/src/internal/nft-api.ts
+++ b/src/internal/nft-api.ts
@@ -252,6 +252,53 @@ export async function checkNftOwnership(
   return response.ownedNfts.length > 0;
 }
 
+export async function verifyNftOwnership(
+  config: AlchemyConfig,
+  owner: string,
+  contractAddresses: string | string[],
+  srcMethod = 'verifyNftOwnership'
+): Promise<boolean | { [contractAddress: string]: boolean }> {
+  if (typeof contractAddresses === 'string') {
+    const response = await getNftsForOwner(
+      config,
+      owner,
+      {
+        contractAddresses: [contractAddresses],
+        omitMetadata: true
+      },
+      srcMethod
+    );
+    return response.ownedNfts.length > 0;
+  } else {
+    if (contractAddresses.length === 0) {
+      throw new Error('Must provide at least one contract address');
+    }
+    const response = await getNftsForOwner(
+      config,
+      owner,
+      {
+        contractAddresses,
+        omitMetadata: true
+      },
+      srcMethod
+    );
+
+    // Create map where all input contract addresses are set to false, then flip
+    // owned nfts to true.
+    const result = contractAddresses.reduce(
+      (acc: { [contractAddress: string]: boolean }, curr) => {
+        acc[curr] = false;
+        return acc;
+      },
+      {}
+    );
+    for (const nft of response.ownedNfts) {
+      result[nft.contract.address] = true;
+    }
+    return result;
+  }
+}
+
 export async function isSpamContract(
   config: AlchemyConfig,
   contractAddress: string,
diff --git a/src/types/types.ts b/src/types/types.ts
index d10de217..f18008a4 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -457,8 +457,16 @@ export interface Media {
    */
   format?: string;
 
-  /** The size of the media asset in bytes. */
+  /**
+   * DEPRECATED - The size of the media asset in bytes
+   *
+   * @deprecated - Please use {@link bytes} instead. This field will be removed
+   *   in a subsequent release.
+   */
   size?: number;
+
+  /** The size of the media asset in bytes. */
+  bytes?: number;
 }
 
 /**
diff --git a/src/util/util.ts b/src/util/util.ts
index dcbfaaae..48a78280 100644
--- a/src/util/util.ts
+++ b/src/util/util.ts
@@ -92,6 +92,12 @@ function parseNftDescription(description?: string | string[]): string {
   if (description === undefined) {
     return '';
   }
+
+  // TODO: Remove after backend adds JSON stringification.
+  if (!Array.isArray(description) && typeof description === 'object') {
+    return JSON.stringify(description);
+  }
+
   return typeof description === 'string' ? description : description.join(' ');
 }
 


Test patch:
diff --git a/test/unit/nft-api.test.ts b/test/unit/nft-api.test.ts
index ffa4c3a5..e72904c5 100644
--- a/test/unit/nft-api.test.ts
+++ b/test/unit/nft-api.test.ts
@@ -989,13 +989,19 @@ describe('NFT module', () => {
     });
   });
 
-  describe('checkNftOwnership', () => {
+  describe('verifyNftOwnership()', () => {
     const owner = '0xABC';
     const addresses = ['0xCA1', '0xCA2'];
     const emptyResponse: RawGetNftsResponse = {
       ownedNfts: [],
       totalCount: 0
     };
+    const partialResponse: RawGetNftsResponse = {
+      ownedNfts: [
+        createRawOwnedNft('b', '0xCA2', '0x2', '2', NftTokenType.ERC1155)
+      ],
+      totalCount: 1
+    };
     const nftResponse: RawGetNftsResponse = {
       ownedNfts: [
         createRawOwnedNft('a', '0xCA1', '0x1', '1'),
@@ -1006,7 +1012,7 @@ describe('NFT module', () => {
 
     it('calls with the correct parameters', async () => {
       mock.onGet().reply(200, emptyResponse);
-      await alchemy.nft.checkNftOwnership(owner, addresses);
+      await alchemy.nft.verifyNftOwnership(owner, addresses);
       expect(mock.history.get.length).toEqual(1);
       expect(mock.history.get[0].params).toHaveProperty('owner', owner);
       expect(mock.history.get[0].params).toHaveProperty(
@@ -1017,27 +1023,43 @@ describe('NFT module', () => {
     });
 
     it('throws if no contract address is passed in', async () => {
-      await expect(alchemy.nft.checkNftOwnership(owner, [])).rejects.toThrow(
+      await expect(alchemy.nft.verifyNftOwnership(owner, [])).rejects.toThrow(
         'Must provide at least one contract address'
       );
     });
 
     const cases = [
-      [emptyResponse, false],
-      [nftResponse, true]
+      [emptyResponse, { '0xCA1': false, '0xCA2': false }],
+      [partialResponse, { '0xCA1': false, '0xCA2': true }],
+      [nftResponse, { '0xCA1': true, '0xCA2': true }]
     ];
     it.each(cases)(
-      'returns the correct response',
+      'returns the correct response for arrayinputs',
+      async (response, expected) => {
+        mock.onGet().reply(200, response);
+        const result = await alchemy.nft.verifyNftOwnership(owner, addresses);
+        expect(result).toEqual(expected);
+      }
+    );
+
+    const cases2 = [
+      [emptyResponse, false],
+      [partialResponse, true]
+    ];
+    it.each(cases2)(
+      'returns the correct response for inputs',
       async (response, expected) => {
+        const address = '0xCA2';
         mock.onGet().reply(200, response);
-        const result = await alchemy.nft.checkNftOwnership(owner, addresses);
+        const result = await alchemy.nft.verifyNftOwnership(owner, address);
         expect(result).toEqual(expected);
       }
     );
+
     it('surfaces errors', async () => {
       mock.onGet().reply(500, 'Internal Server Error');
       await expect(
-        alchemy.nft.checkNftOwnership(owner, addresses)
+        alchemy.nft.verifyNftOwnership(owner, addresses)
       ).rejects.toThrow('Internal Server Error');
     });
   });