Repository: alchemyplatform/alchemy-sdk-js
Pull request #39 (2022-06-27 22:02:33)

Refactor NFT classes to be interfaces, create util functions


Patch:
diff --git a/src/api/nft-api.ts b/src/api/nft-api.ts
index 6008b429..5dbb1a4e 100644
--- a/src/api/nft-api.ts
+++ b/src/api/nft-api.ts
@@ -31,9 +31,10 @@ import {
   RawOwnedBaseNft,
   RawOwnedNft
 } from '../internal/raw-interfaces';
-import { getNftContractFromRaw, toHex } from './util';
+import { toHex } from './util';
 import { getTransactionReceipts } from './enhanced';
 import { BigNumber, BigNumberish } from 'ethers';
+import { getBaseNftFromRaw, getNftContractFromRaw, getNftFromRaw } from '../util/util';
 
 const ETH_NULL_VALUE = '0x';
 
@@ -98,7 +99,7 @@ export async function getNftMetadata(
       }
     );
   }
-  return Nft.fromResponse(response, contractAddress);
+  return getNftFromRaw(response, contractAddress);
 }
 
 /**
@@ -623,7 +624,7 @@ async function refresh(
       refreshCache: true
     }
   );
-  return Nft.fromResponse(response, contractAddress);
+  return getNftFromRaw(response, contractAddress);
 }
 
 /**
@@ -660,9 +661,9 @@ function nftFromGetNftResponse(
   ownedNft: RawOwnedBaseNft | RawOwnedNft
 ): Nft | BaseNft {
   if (isNftWithMetadata(ownedNft)) {
-    return Nft.fromResponse(ownedNft, ownedNft.contract.address);
+    return getNftFromRaw(ownedNft, ownedNft.contract.address);
   } else {
-    return BaseNft.fromResponse(ownedNft, ownedNft.contract.address);
+    return getBaseNftFromRaw(ownedNft, ownedNft.contract.address);
   }
 }
 
@@ -677,9 +678,9 @@ function nftFromGetNftCollectionResponse(
   contractAddress: string
 ): Nft | BaseNft {
   if (isNftWithMetadata(ownedNft)) {
-    return Nft.fromResponse(ownedNft, contractAddress);
+    return getNftFromRaw(ownedNft, contractAddress);
   } else {
-    return BaseNft.fromResponse(ownedNft, contractAddress);
+    return getBaseNftFromRaw(ownedNft, contractAddress);
   }
 }
 
diff --git a/src/api/nft.ts b/src/api/nft.ts
index 8807e23c..398501a0 100644
--- a/src/api/nft.ts
+++ b/src/api/nft.ts
@@ -1,6 +1,4 @@
 import { NftMetadata, NftTokenType, TokenUri } from '../types/types';
-import { RawBaseNft, RawNft } from '../internal/raw-interfaces';
-import { BigNumber } from 'ethers';
 /**
  * Alchemy representation of a base NFT contract that doesn't contain metadata.
  *
@@ -31,33 +29,12 @@ export interface NftContract extends BaseNftContract {
  *
  * @public
  */
-export class BaseNft {
-  readonly contract: BaseNftContract;
-
-  /**
-   * This constructor should never be called directly. All Nft instances should
-   * be created from a backend response via the `fromResponse` method.
-   */
-  protected constructor(
-    address: string,
-    /** The NFT token ID as an integer string. */
-    readonly tokenId: string,
-    /** The type of ERC token, if known. */
-    readonly tokenType: NftTokenType
-  ) {
-    this.contract = { address };
-  }
-
-  /** @internal */
-  static fromResponse(baseNft: RawBaseNft, contractAddress: string): BaseNft {
-    return new BaseNft(
-      contractAddress,
-      // We have to normalize the token id here since the backend sometimes
-      // returns the token ID as a hex string and sometimes as an integer.
-      BigNumber.from(baseNft.id.tokenId).toString(),
-      baseNft.id.tokenMetadata?.tokenType ?? NftTokenType.UNKNOWN
-    );
-  }
+export interface BaseNft {
+  contract: BaseNftContract;
+  /** The NFT token ID as an integer string. */
+  tokenId: string;
+  /** The type of ERC token, if known. */
+  tokenType: NftTokenType;
 }
 
 /**
@@ -65,107 +42,28 @@ export class BaseNft {
  *
  * @public
  */
-export class Nft extends BaseNft {
+export interface Nft extends BaseNft {
   /** The NFT title. */
-  readonly title: string;
+  title: string;
 
   /** The NFT description. */
-  readonly description: string;
+  description: string;
 
   /** When the NFT was last updated in the blockchain. Represented in ISO-8601 format. */
-  readonly timeLastUpdated: string;
+  timeLastUpdated: string;
 
   /** Holds an error message if there was an issue fetching metadata. */
-  readonly metadataError: string | undefined;
+  metadataError: string | undefined;
 
   /**
    * The raw metadata fetched from the metadata URL specified by the NFT. The
    * field is undefined if Alchemy was unable to fetch metadata.
    */
-  readonly rawMetadata: NftMetadata | undefined;
+  rawMetadata: NftMetadata | undefined;
 
   /** URIs for accessing the NFT's metadata blob. */
-  readonly tokenUri: TokenUri | undefined;
+  tokenUri: TokenUri | undefined;
 
   /** URIs for accessing the NFT's media assets. */
-  readonly media: TokenUri[] = [];
-
-  /**
-   * This constructor should never be called directly. All Nft instances should
-   * be created from a backend response via the `fromResponse` method.
-   *
-   * @internal
-   */
-  private constructor(
-    address: string,
-    tokenId: string,
-    tokenType: NftTokenType,
-    title: string,
-    description: string | Array<string>,
-    timeLastUpdated: string,
-    tokenUri?: TokenUri,
-    media?: TokenUri[],
-    metadata?: NftMetadata,
-    error?: string
-  ) {
-    super(address, tokenId, tokenType);
-    this.title = title;
-    this.description = Nft.parseDescription(description);
-    this.timeLastUpdated = timeLastUpdated;
-    this.metadataError = error;
-    this.rawMetadata = metadata;
-    this.tokenUri = Nft.parseTokenUri(tokenUri);
-    this.media = Nft.parseTokenUriArray(media);
-  }
-
-  /** @internal */
-  static fromResponse(nft: RawNft, contractAddress: string): Nft {
-    return new Nft(
-      contractAddress,
-      // We have to normalize the token id here since the backend sometimes
-      // returns the token ID as a hex string and sometimes as an integer string.
-      BigNumber.from(nft.id.tokenId).toString(),
-      nft.id.tokenMetadata?.tokenType ?? NftTokenType.UNKNOWN,
-      nft.title,
-      nft.description,
-      nft.timeLastUpdated,
-      nft.tokenUri,
-      nft.media,
-      nft.metadata,
-      nft.error
-    );
-  }
-
-  /** @internal */
-  private static parseDescription(description: string | Array<string>): string {
-    return typeof description === 'string'
-      ? description
-      : description.join(' ');
-  }
-
-  /**
-   * Returns undefined if the uri has empty string fields.
-   *
-   * @internal
-   */
-  private static parseTokenUri(
-    uri: TokenUri | undefined
-  ): TokenUri | undefined {
-    if (uri && uri.raw.length === 0 && uri.gateway.length == 0) {
-      return undefined;
-    }
-    return uri;
-  }
-
-  /**
-   * Removes empty URIs from the array.
-   *
-   * @internal
-   */
-  private static parseTokenUriArray(arr: TokenUri[] | undefined): TokenUri[] {
-    if (arr === undefined) {
-      return [];
-    }
-    return arr.filter(uri => this.parseTokenUri(uri) !== undefined);
-  }
+  media: TokenUri[];
 }
diff --git a/src/api/util.ts b/src/api/util.ts
index b6f8fc32..ac9fed65 100644
--- a/src/api/util.ts
+++ b/src/api/util.ts
@@ -1,6 +1,4 @@
 import { BigNumber } from 'ethers';
-import { RawBaseNftContract, RawNftContract } from '../internal/raw-interfaces';
-import { BaseNftContract, NftContract } from './nft';
 
 /**
  * Converts a hex string to a decimal number.
@@ -31,21 +29,3 @@ export function toHex(num: number): string {
 export function isHex(possibleHexString: string): boolean {
   return /^0x[0-9a-fA-F]+$/.test(possibleHexString);
 }
-
-export function getBaseNftContractFromRaw(
-  rawBaseNftContract: RawBaseNftContract
-): BaseNftContract {
-  return { address: rawBaseNftContract.address };
-}
-
-export function getNftContractFromRaw(
-  rawNftContract: RawNftContract
-): NftContract {
-  return {
-    address: rawNftContract.address,
-    name: rawNftContract.contractMetadata.name,
-    symbol: rawNftContract.contractMetadata.symbol,
-    totalSupply: rawNftContract.contractMetadata.totalSupply,
-    tokenType: rawNftContract.contractMetadata.tokenType
-  };
-}
diff --git a/src/util/util.ts b/src/util/util.ts
index 2a973c5a..0ef096ec 100644
--- a/src/util/util.ts
+++ b/src/util/util.ts
@@ -1,4 +1,11 @@
+import { BigNumber } from 'ethers';
+import { BaseNft, BaseNftContract, Nft, NftContract } from '../api/nft';
 import { toHex } from '../api/util';
+import {
+  RawBaseNftContract,
+  RawNftContract
+, RawBaseNft, RawNft } from '../internal/raw-interfaces';
+import { NftTokenType, TokenUri } from '../types/types';
 
 export function formatBlock(block: string | number): string {
   if (typeof block === 'string') {
@@ -8,3 +15,75 @@ export function formatBlock(block: string | number): string {
   }
   return block.toString();
 }
+
+export function getBaseNftContractFromRaw(
+  rawBaseNftContract: RawBaseNftContract
+): BaseNftContract {
+  return { address: rawBaseNftContract.address };
+}
+
+export function getNftContractFromRaw(
+  rawNftContract: RawNftContract
+): NftContract {
+  return {
+    address: rawNftContract.address,
+    name: rawNftContract.contractMetadata.name,
+    symbol: rawNftContract.contractMetadata.symbol,
+    totalSupply: rawNftContract.contractMetadata.totalSupply,
+    tokenType: rawNftContract.contractMetadata.tokenType
+  };
+}
+
+export function getBaseNftFromRaw(
+  rawBaseNft: RawBaseNft,
+  contractAddress: string
+): BaseNft {
+  return {
+    contract: { address: contractAddress },
+    tokenId: BigNumber.from(rawBaseNft.id.tokenId).toString(),
+    tokenType: rawBaseNft.id.tokenMetadata?.tokenType ?? NftTokenType.UNKNOWN
+  };
+}
+
+export function getNftFromRaw(rawNft: RawNft, contractAddress: string): Nft {
+  return {
+    contract: { address: contractAddress },
+    tokenId: parseNftTokenId(rawNft.id.tokenId),
+    tokenType: parseNftTokenType(rawNft.id.tokenMetadata?.tokenType),
+    title: rawNft.title,
+    description: parseNftDescription(rawNft.description),
+    timeLastUpdated: rawNft.timeLastUpdated,
+    metadataError: rawNft.error,
+    rawMetadata: rawNft.metadata,
+    tokenUri: parseNftTokenUri(rawNft.tokenUri),
+    media: parseNftTokenUriArray(rawNft.media)
+  };
+}
+
+function parseNftTokenId(tokenId: string): string {
+  // We have to normalize the token id here since the backend sometimes
+  // returns the token ID as a hex string and sometimes as an integer.
+  return BigNumber.from(tokenId).toString();
+}
+
+function parseNftTokenType(tokenType: NftTokenType | undefined): NftTokenType {
+  return tokenType ?? NftTokenType.UNKNOWN;
+}
+
+function parseNftDescription(description: string | string[]): string {
+  return typeof description === 'string' ? description : description.join(' ');
+}
+
+function parseNftTokenUri(uri: TokenUri | undefined): TokenUri | undefined {
+  if (uri && uri.raw.length === 0 && uri.gateway.length == 0) {
+    return undefined;
+  }
+  return uri;
+}
+
+function parseNftTokenUriArray(arr: TokenUri[] | undefined): TokenUri[] {
+  if (arr === undefined) {
+    return [];
+  }
+  return arr.filter(uri => parseNftTokenUri(uri) !== undefined);
+}


Test patch:
diff --git a/test/test-util.ts b/test/test-util.ts
index fb1ecd2c..1a0e52fa 100644
--- a/test/test-util.ts
+++ b/test/test-util.ts
@@ -9,7 +9,6 @@ import {
 } from '../src/internal/raw-interfaces';
 import {
   BaseNft,
-  BaseNftContract,
   Nft,
   NftTokenType,
   OwnedBaseNft,
@@ -19,7 +18,12 @@ import {
 } from '../src';
 import { BigNumber } from 'ethers';
 import { BlockHead, LogsEvent } from '../src/internal/websocket-backfiller';
-import { getBaseNftContractFromRaw } from '../src/api/util';
+import {
+  getBaseNftContractFromRaw,
+  getBaseNftFromRaw,
+  getNftFromRaw
+} from '../src/util/util';
+import { BaseNftContract } from '../src/api/nft';
 
 export function createRawNftContract(
   address: string,
@@ -97,7 +101,7 @@ export function createBaseNft(
   tokenId: string | number,
   tokenType = NftTokenType.UNKNOWN
 ): BaseNft {
-  return BaseNft.fromResponse(createRawBaseNft(tokenId, tokenType), address);
+  return getBaseNftFromRaw(createRawBaseNft(tokenId, tokenType), address);
 }
 
 export function createNft(
@@ -108,7 +112,7 @@ export function createNft(
   tokenUri?: TokenUri,
   media?: TokenUri[] | undefined
 ): Nft {
-  return Nft.fromResponse(
+  return getNftFromRaw(
     createRawNft(title, tokenId, tokenType, tokenUri, media),
     address
   );
diff --git a/test/unit/nft-api.test.ts b/test/unit/nft-api.test.ts
index 434bb45e..6b8206c7 100644
--- a/test/unit/nft-api.test.ts
+++ b/test/unit/nft-api.test.ts
@@ -5,7 +5,6 @@ import {
   CollectionBaseNftsResponse,
   CollectionNftsResponse,
   fromHex,
-  getNftContractMetadata,
   getNftMetadata,
   getNftsForCollection,
   getNftsForCollectionIterator,
@@ -45,7 +44,8 @@ import {
   RawGetNftsForCollectionResponse,
   RawGetNftsResponse
 } from '../../src/internal/raw-interfaces';
-import { getNftContractFromRaw } from '../../src/api/util';
+import { getNftContractFromRaw, getNftFromRaw } from '../../src/util/util';
+import { getNftContractMetadata } from '../../src/api/nft-api';
 
 describe('NFT module', () => {
   let alchemy: Alchemy;
@@ -149,7 +149,7 @@ describe('NFT module', () => {
     // Special case token ID as an integer string, since that's what the NFT
     // API endpoint returns.
     const rawNftResponse = createRawNft(title, tokenId.toString());
-    const expectedNft = Nft.fromResponse(rawNftResponse, contractAddress);
+    const expectedNft = getNftFromRaw(rawNftResponse, contractAddress);
 
     beforeEach(() => {
       mock.onGet().reply(200, rawNftResponse);
diff --git a/test/unit/nft.test.ts b/test/unit/nft.test.ts
index fc94b805..8de62dec 100644
--- a/test/unit/nft.test.ts
+++ b/test/unit/nft.test.ts
@@ -1,4 +1,5 @@
-import { BaseNft, Nft, NftTokenType, toHex } from '../../src';
+import { NftTokenType, toHex } from '../../src';
+import { getBaseNftFromRaw, getNftFromRaw } from '../../src/util/util';
 import { createNft, createRawBaseNft, createRawNft } from '../test-util';
 
 describe('BaseNft class', () => {
@@ -6,10 +7,7 @@ describe('BaseNft class', () => {
   const tokenId = '0x01';
   const tokenIdString = '1';
   it('fromResponse() defaults to UNKNOWN token type', () => {
-    const nft = BaseNft.fromResponse(
-      createRawBaseNft(tokenId),
-      contractAddress
-    );
+    const nft = getBaseNftFromRaw(createRawBaseNft(tokenId), contractAddress);
     expect(nft.tokenType).toEqual(NftTokenType.UNKNOWN);
     expect(nft.tokenId).toEqual(tokenIdString);
     expect(nft.contract.address).toEqual(contractAddress);
@@ -18,13 +16,10 @@ describe('BaseNft class', () => {
   it('fromResponse() normalizes tokenId fields', () => {
     const tokenIdIntegerAsString = '42';
     const tokenIdHex = toHex(42);
-    let nft = BaseNft.fromResponse(
-      createRawBaseNft(tokenIdHex),
-      contractAddress
-    );
+    let nft = getBaseNftFromRaw(createRawBaseNft(tokenIdHex), contractAddress);
     expect(nft.tokenId).toEqual(tokenIdIntegerAsString);
 
-    nft = BaseNft.fromResponse(
+    nft = getBaseNftFromRaw(
       createRawBaseNft(tokenIdIntegerAsString),
       contractAddress
     );
@@ -37,7 +32,7 @@ describe('Nft class', () => {
   const tokenId = 1;
   const tokenIdString = '1';
   it('fromResponse() defaults to UNKNOWN token type', () => {
-    const nft = Nft.fromResponse(
+    const nft = getNftFromRaw(
       createRawNft('title', toHex(tokenId)),
       contractAddress
     );
@@ -49,13 +44,10 @@ describe('Nft class', () => {
   it('fromResponse() normalizes tokenId fields', () => {
     const tokenIdIntegerAsString = '42';
     const tokenIdHex = toHex(42);
-    let nft = Nft.fromResponse(
-      createRawNft('title', tokenIdHex),
-      contractAddress
-    );
+    let nft = getNftFromRaw(createRawNft('title', tokenIdHex), contractAddress);
     expect(nft.tokenId).toEqual(tokenIdIntegerAsString);
 
-    nft = Nft.fromResponse(
+    nft = getNftFromRaw(
       createRawNft('title', tokenIdIntegerAsString),
       contractAddress
     );
@@ -100,7 +92,7 @@ describe('Nft class', () => {
       '2022-02-16T17:12:00.280Z',
       description
     );
-    const nft = Nft.fromResponse(rawNft, '0xCA1');
+    const nft = getNftFromRaw(rawNft, '0xCA1');
     expect(nft.description).toEqual('very special and unique description');
   });