Repository: alchemyplatform/alchemy-sdk-js
Pull request #403 (2024-01-08 01:46:28)

feat: add reportSpam and isAirdropNFT endpoints
https://alchemyinsights.slack.com/archives/C04P0BZDUQ5/p1704678527884079


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e778687e..497cdf6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
 
 ### Minor Changes
 
+- Added endpoints for `isAirdropNft()` and `reportSpam()` on the nft namespace.
 - Added a missing `marketplaceAddress` field to the `NftSale` in `NftNamespace.getNftSales()`.
 - Fixed an incorrect field name for `bannerImageUrl` in `OpenSeaCollectionMetadata`. Deprecated the old field name `imageBannerUrl`.
 
diff --git a/README.md b/README.md
index 3d46ee59..f47dd58a 100644
--- a/README.md
+++ b/README.md
@@ -225,6 +225,8 @@ under the `alchemy.nft` namespace:
 - `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.
+- `reportSpam()`: Report feedback that a given NFT contract address is a spam contract as defined by Alchemy.
+- `isAirdropNft()`: Check whether the given NFT token is marked as an airdrop or not. Airdrops are defined as NFTs that were minted to a user address in a transaction sent by a different address.
 - `refreshNftMetadata()`: Refresh the cached NFT metadata for a contract address and a single tokenId.
 - `refreshContract()`: Enqueues the specified contract address to have all token ids' metadata refreshed.
 - `getFloorPrice()`: Return the floor prices of a NFT contract by marketplace.
diff --git a/src/api/nft-namespace.ts b/src/api/nft-namespace.ts
index c518f263..8a503995 100644
--- a/src/api/nft-namespace.ts
+++ b/src/api/nft-namespace.ts
@@ -20,9 +20,11 @@ import {
   getSpamContracts,
   getTransfersForContract,
   getTransfersForOwner,
+  isAirdropNft,
   isSpamContract,
   refreshContract,
   refreshNftMetadata,
+  reportSpam,
   searchContractMetadata,
   summarizeNftAttributes,
   verifyNftOwnership
@@ -52,6 +54,7 @@ import {
   GetOwnersForNftResponse,
   GetSpamContractsResponse,
   GetTransfersForContractOptions,
+  IsAirdropNftResponse,
   IsSpamContractResponse,
   Nft,
   NftAttributesResponse,
@@ -456,7 +459,6 @@ export class NftNamespace {
    * https://docs.alchemy.com/alchemy/enhanced-apis/nft-api/nft-api-faq#nft-spam-classification.
    *
    * @param contractAddress - The contract address to check.
-   * @beta
    */
   isSpamContract(contractAddress: string): Promise<IsSpamContractResponse> {
     return isSpamContract(this.config, contractAddress);
@@ -466,13 +468,37 @@ export class NftNamespace {
    * Returns a list of all spam contracts marked by Alchemy. For details on how
    * Alchemy marks spam contracts, go to
    * https://docs.alchemy.com/alchemy/enhanced-apis/nft-api/nft-api-faq#nft-spam-classification.
-   *
-   * @beta
    */
   getSpamContracts(): Promise<GetSpamContractsResponse> {
     return getSpamContracts(this.config);
   }
 
+  /**
+   * 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
+   * https://docs.alchemy.com/alchemy/enhanced-apis/nft-api/nft-api-faq#nft-spam-classification.
+   *
+   * @param contractAddress - The contract address to check.
+   */
+  reportSpam(contractAddress: string): Promise<void> {
+    return reportSpam(this.config, contractAddress);
+  }
+
+  /**
+   * Returns whether a token is marked as an airdrop or not.
+   * Airdrops are defined as NFTs that were minted to a user address in a transaction
+   * sent by a different address.
+   *
+   * @param contractAddress - The contract address to check.
+   * @param tokenId - Token id of the NFT.
+   */
+  isAirdropNft(
+    contractAddress: string,
+    tokenId: string
+  ): Promise<IsAirdropNftResponse> {
+    return isAirdropNft(this.config, contractAddress, tokenId);
+  }
+
   /**
    * Returns the floor prices of a NFT contract by marketplace.
    *
diff --git a/src/api/notify-namespace.ts b/src/api/notify-namespace.ts
index 5eb7ef07..85ed1cdd 100644
--- a/src/api/notify-namespace.ts
+++ b/src/api/notify-namespace.ts
@@ -639,7 +639,10 @@ const WEBHOOK_NETWORK_TO_NETWORK: { [key: string]: Network } = {
   ARB_MAINNET: Network.ARB_MAINNET,
   ARB_GOERLI: Network.ARB_GOERLI,
   OPT_MAINNET: Network.OPT_MAINNET,
-  OPT_GOERLI: Network.OPT_GOERLI
+  OPT_GOERLI: Network.OPT_GOERLI,
+  BASE_MAINNET: Network.BASE_MAINNET,
+  BASE_GOERLI: Network.BASE_GOERLI,
+  BASE_SEPOLIA: Network.BASE_SEPOLIA
 };
 
 /** Mapping of the SDK's network representation the webhook API's network representation. */
diff --git a/src/internal/nft-api.ts b/src/internal/nft-api.ts
index d93fcdb5..09fe45ce 100644
--- a/src/internal/nft-api.ts
+++ b/src/internal/nft-api.ts
@@ -25,6 +25,7 @@ import {
   GetOwnersForNftResponse,
   GetSpamContractsResponse,
   GetTransfersForContractOptions,
+  IsAirdropNftResponse,
   IsSpamContractResponse,
   Nft,
   NftAttributesResponse,
@@ -85,6 +86,7 @@ import {
   RawGetOwnersForContractResponse,
   RawGetOwnersForContractWithTokenBalancesResponse,
   RawGetSpamContractsResponse,
+  RawIsAirdropNftResponse,
   RawIsSpamContractResponse,
   RawNft,
   RawNftAttributesResponse,
@@ -622,6 +624,40 @@ export async function getSpamContracts(
   );
 }
 
+export async function reportSpam(
+  config: AlchemyConfig,
+  contractAddress: string,
+  srcMethod = 'reportSpam'
+): Promise<void> {
+  void requestHttpWithBackoff<ReportSpamParams, void>(
+    config,
+    AlchemyApiType.NFT,
+    'reportSpam',
+    srcMethod,
+    {
+      contractAddress
+    }
+  );
+}
+
+export async function isAirdropNft(
+  config: AlchemyConfig,
+  contractAddress: string,
+  tokenId: string,
+  srcMethod = 'isAirdropNft'
+): Promise<IsAirdropNftResponse> {
+  return requestHttpWithBackoff<isAirdropNftParams, RawIsAirdropNftResponse>(
+    config,
+    AlchemyApiType.NFT,
+    'isAirdropNFT',
+    srcMethod,
+    {
+      contractAddress,
+      tokenId
+    }
+  );
+}
+
 export async function getFloorPrice(
   config: AlchemyConfig,
   contractAddress: string,
@@ -1013,6 +1049,25 @@ interface IsSpamContractParams {
   contractAddress: string;
 }
 
+/**
+ * Interface for the `reportSpam` endpoint.
+ *
+ * @internal
+ */
+interface ReportSpamParams {
+  contractAddress: string;
+}
+
+/**
+ * Interface for the `isAirdropNft` endpoint.
+ *
+ * @internal
+ */
+interface isAirdropNftParams {
+  contractAddress: string;
+  tokenId: string;
+}
+
 /**
  * Interface for the `getContractMetadata` endpoint.
  *
diff --git a/src/internal/raw-interfaces.ts b/src/internal/raw-interfaces.ts
index c24c5955..e5352e61 100644
--- a/src/internal/raw-interfaces.ts
+++ b/src/internal/raw-interfaces.ts
@@ -385,6 +385,10 @@ export interface RawGetSpamContractsResponse {
   contractAddresses: string[];
 }
 
+export interface RawIsAirdropNftResponse {
+  isAirdrop: boolean;
+}
+
 export interface RawDisplayNftForContract {
   tokenId: string;
   name: string | null;
diff --git a/src/types/nft-types.ts b/src/types/nft-types.ts
index 9d8bf980..1179435b 100644
--- a/src/types/nft-types.ts
+++ b/src/types/nft-types.ts
@@ -384,6 +384,10 @@ export interface IsSpamContractResponse {
   isSpamContract: boolean;
 }
 
+export interface IsAirdropNftResponse {
+  isAirdrop: boolean;
+}
+
 export interface GetSpamContractsResponse {
   contractAddresses: string[];
 }


Test patch:
diff --git a/test/integration/nft.test.ts b/test/integration/nft.test.ts
index d1d45ac3..772b7287 100644
--- a/test/integration/nft.test.ts
+++ b/test/integration/nft.test.ts
@@ -622,6 +622,16 @@ describe('E2E integration tests', () => {
     expect(typeof response.contractAddresses[0]).toEqual('string');
   });
 
+  it('reportSpam()', async () => {
+    const response = await alchemy.nft.reportSpam(contractAddress);
+    expect(typeof response).toEqual('undefined');
+  });
+
+  it('isAirdropNft()', async () => {
+    const response = await alchemy.nft.isAirdropNft(contractAddress, '0');
+    expect(typeof response.isAirdrop).toEqual('boolean');
+  });
+
   it('getFloorPrice()', async () => {
     const response = await alchemy.nft.getFloorPrice(contractAddress);
     expect(response.openSea).toBeDefined();
diff --git a/test/unit/nft-api.test.ts b/test/unit/nft-api.test.ts
index 5b1dd17a..3ebc55ed 100644
--- a/test/unit/nft-api.test.ts
+++ b/test/unit/nft-api.test.ts
@@ -1348,6 +1348,34 @@ describe('NFT module', () => {
     });
   });
 
+  describe('reportSpam', () => {
+    const spamContract = '0x000440f08436a7b866d1ae42db5e0be801da722a';
+    it('calls with the correct parameters', async () => {
+      mock.onGet().reply(200, true);
+      await alchemy.nft.reportSpam(spamContract);
+      expect(mock.history.get.length).toEqual(1);
+      expect(mock.history.get[0].params).toHaveProperty(
+        'contractAddress',
+        spamContract
+      );
+    });
+  });
+
+  describe('isAirdropNft', () => {
+    const contract = '0x000440f08436a7b866d1ae42db5e0be801da722a';
+    const tokenId = '1';
+    it('calls with the correct parameters', async () => {
+      mock.onGet().reply(200, true);
+      await alchemy.nft.isAirdropNft(contract, tokenId);
+      expect(mock.history.get.length).toEqual(1);
+      expect(mock.history.get[0].params).toHaveProperty(
+        'contractAddress',
+        contract
+      );
+      expect(mock.history.get[0].params).toHaveProperty('tokenId', tokenId);
+    });
+  });
+
   describe('getNftFloorPrice', () => {
     const contractAddress = '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d';
     const templateResponse: GetFloorPriceResponse = {