Repository: alchemyplatform/alchemy-sdk-js
Pull request #54 (2022-07-11 18:33:12)

Move AlchemyProvider and AlchemyWebSocketProvider methods to top level
Moving methods up from `AlchemyProvider` and `AlchemyWebSocketProvider` to `Alchemy` instance. Some methods for `AlchemyWebSocketProvider` had to be moved up as well.

## List of moved methods
### AlchemyProvider
- `getBalance()`
- `getCode()`
- `getStorageAt()`
- `getTransactionCount()`
- `getBlock()`
- `getBlockWithTransactions()`
- `getNetwork()`
- `getGasPrice()`
- `getFeeData()`
- `ready`
- `call()`
- `estimateGas()`
- `getTransaction()`
- `getTransactionReceipt()`
- `sendTransaction()`
- `waitForTransaction()`
- `getLogs()`
- `send()`
### AlchemyWebSocketProvider
- `on()`
- `once()`
- `off()`
- `removeAllListeners()`
- `listeners()`

## List of methods that were not moved
- ENS methods: `getAvatar()`, `getResolver()`, `lookupAddress()`, `resolveName()` -- We should delay moving these until we decide if we want to do a custom ENS resolution endpoint
- WebSocket methods: `emit()` -- Internal methods
- Inspection methods: `isProvider()` -- There's no confusion here since we only use one provider
- base provider: `polling()`, `pollingInterval()` -- We don't expose a polling subscription option, which makes these redundant. We can explore adding this w/ enough developer feedback
- JsonRpcProvider: `getSigner()`, `getUncheckedSigner()`, `listAccounts()` -- Haven't looked deeply into what this entails
- all other internal properties/methods that are accessible on the object, but not documented on ethers are omitted


Patch:
diff --git a/README.md b/README.md
index c80b94d3..80de8412 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,12 @@
 # Alchemy SDK for Javascript
 
-Alchemy SDK helps developers use Alchemy's APIs and endpoints more efficiently. This is a lightweight, modular SDK built
-on top of Ethers.js that encapsulates common usage patterns and abstracts away the complexities of both our HTTP and
-JSON-RPC endpoints.
+Alchemy SDK helps developers use Alchemy's APIs and endpoints more efficiently. This is a lightweight, modular SDK built as a drop-in replacement of Ethers.js that provides a superset of functionality - enabling access to the Alchemy NFT API, Websockets, and Enhanced API methods. 
 
-Note that the SDK is still in public beta. Alchemy reserves the right to (and almost certainly will) make breaking
-API changes in subsequent releases (don't write production code around it just yet).
+It also provides access to Alchemy's hardened node infrastructure, guaranteeing reliability, scalability, and quality-of-life improvements such as automatic exponential backoff retries.
+
+Note that the SDK is still in public beta. Alchemy reserves the right to (and almost certainly will) make breaking API changes in subsequent releases (don't write production code around it just yet).
 
 ## Getting started
-### TODO: Remove usage of `initializeAlchemy()`
 
 ```
 npm install @alch/alchemy-sdk
@@ -17,7 +15,7 @@ npm install @alch/alchemy-sdk
 After installing the app, you can then import and use the SDK:
 
 ```ts
-import { Network, initializeAlchemy } from '@alch/alchemy-sdk';
+import { Network, Alchemy } from '@alch/alchemy-sdk';
 
 // Optional Config object, but defaults to demo api-key and eth-mainnet.
 const settings = {
@@ -26,17 +24,27 @@ const settings = {
   maxRetries: 10
 };
 
-const alchemy = initializeAlchemy(settings);
+const alchemy = new Alchemy(settings);
 ```
 
-You can access each method using the following pattern:
+You can access each method using the following pattern. Methods supported include: 
+- All commonly-used Ethers.js methods
+- Alchemy NFT API methods
+- Alchemy Enhanced API methods
+
+If you are already using Ethers.js, you should be simply able to replace the Ethers.js object with the Alchemy object and it should just work.
 
 ```ts
 // Initializing the alchemy config object
-import { initializeAlchemy } from '@alch/alchemy-sdk';
+import { Alchemy } from '@alch/alchemy-sdk';
+
+// Using default settings - pass in a settings object to specify your API key and network
+const alchemy = new Alchemy(); 
 
-const alchemy = initializeAlchemy(); // using default settings - pass in a settings object to specify your API key and network
+// Access standard Ethers.js JSON-RPC node requests
+alchemy.getBlockNumber().then(console.log);
 
+// Access Alchemy-specific NFT API or Enhanced API requests
 alchemy.getNftsForOwner('0xshah.eth').then(console.log);
 ```
 
@@ -59,41 +67,32 @@ For example, to pin to a specific version of the SDK in your `package.json` file
 
 ## SDK Structure
 
-#### TODO: UPDATE THIS SECTION AFTER FULL MIGRATION
-The `Alchemy` object returned by `initializeAlchemy()` provides access to the Alchemy API. An optional config
+The `Alchemy` object returned by `new Alchemy()` provides access to the Alchemy API. An optional config
 object can be passed in when initializing to set your API key, change the network, or specify the max number of retries.
 
 There are two different patterns for the Alchemy object to be used:
 
-1. It can be passed into top-level functions like `getNftsForOwner()` or `getAssetTransfers()`. The current supported
-   functions using this pattern are the Alchemy NFT API endpoints and Alchemy Enhanced APIs.
+1. The Alchemy object enables access to methods like `alchemy.getNftsForOwner()` or `alchemy.getBlockNumber()`. This pattern current supports nearly all Ethers.js methods with a 1:1 mapping, the Alchemy NFT API endpoints, and the Alchemy Enhanced APIs.
 
-2. It can be used to generate an Ethers.js provider that allows access to Alchemy Provider-specific
-   [Ethers.js methods](https://docs.ethers.io/v5/single-page/). These encompass most standard JSON-RPC requests to
-   the blockchain.
+2. In the case that an Ethers.js method is not supported on the top-level, the Alchemy object can be used to generate an Ethers.js provider that allows access to less-common [Ethers.js methods](https://docs.ethers.io/v5/single-page/).
 
-## Ethers.js for standard JSON-RPC Calls
+## Accessing the full Ethers.js provider
 
-To access standard JSON-RPC calls not in the NFT API or Alchemy Enhanced APIs, the SDK includes Ethers.js.
-The `Alchemy.getProvider()` function configures the
-Ethers.js [AlchemyProvider](https://docs.ethers.io/v5/api/providers/api-providers/#AlchemyProvider) and returns it. This
-allows you to perform core JSON-RPC calls with an Alchemy provider, just as you normally would with Ethers. If you are
-already using Ethers, you can simply use the provider from `alchemy-sdk` and the rest of your code should just work:
+To keep the package clean, we don't support certain uncommonly-used Ethers.js methods as top-level methods the Alchemy object - for example, `provider.formatter`. If you'd like to access these methods, simply use the `alchemy.getProvider()` function to configure the
+Ethers.js [AlchemyProvider](https://docs.ethers.io/v5/api/providers/api-providers/#AlchemyProvider) and return it.
 
 ```ts
-import { initializeAlchemy } from '@alch/alchemy-sdk';
+import { Alchemy } from '@alch/alchemy-sdk';
 
-const alchemy = initializeAlchemy();
+const alchemy = new Alchemy();
 
-// ETH JSON-RPC calls through ethers.js Provider
-const ethersAlchemyProvider = alchemy.getProvider();
-ethersAlchemyProvider
-  .getBalance('0x994b342dd87fc825f66e51ffa3ef71ad818b6893', 'latest')
-  .then(console.log);
+async function runAlchemy() {
+  const ethersProvider = await alchemy.getProvider();
+  console.log(ethersProvider.formatter);
+}
+runAlchemy();
 ```
 
-Consult the [Ethers.js documentation](https://docs.ethers.io/v5/) for how to use it to call standard JSON-RPC methods.
-
 ## Websockets and Subscription Listeners
 
 In addition to the built-in Ethers.js listeners, the Alchemy SDK includes support for Alchemy's Subscription API. This
@@ -109,9 +108,9 @@ Alchemy [AlchemyWebSocketProvider](https://docs.ethers.io/v5/api/providers/api-p
 returns it. This can be used like the standard Ethers.js Websocket provider to add listeners for Alchemy events:
 
 ```ts
-import { initializeAlchemy } from '@alch/alchemy-sdk';
+import { Alchemy } from '@alch/alchemy-sdk';
 
-const alchemy = initializeAlchemy();
+const alchemy = new Alchemy();
 
 const websocketProvider = alchemy.getWebsocketProvider();
 
@@ -156,11 +155,11 @@ under the `Alchemy` class:
 - `getNftContractMetadata()`: Get the metadata associated with an NFT contract
 - `getNftsForOwner()`: Get NFTs for an owner address.
 - `getNftsForOwnerIterator()`: Get NFTs for an owner address as an async iterator (handles paging automatically).
-- `getNftsForCollection()`: Get all NFTs for a contract address.
-- `getNftForCollectionIterator()`: Get all NFTs for a contract address as an async iterator (handles paging
+- `getNftsForNftContract()`: Get all NFTs for a contract address.
+- `getNftForNftContractIterator()`: 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.
+- `getOwnersForNftContract()`: 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.
 - `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.
@@ -184,15 +183,15 @@ interfaces in more detail.
 The Alchemy NFT endpoints return 100 results per page. To get the next page, you can pass in the `pageKey` returned by
 the
 previous call. To simplify paginating through all results, the SDK provides the `getNftsIterator()`
-and `getNftsForCollectionIterator()` functions that automatically paginate through all NFTs and yields them via
+and `getNftsForNftContractIterator()` functions that automatically paginate through all NFTs and yields them via
 an `AsyncIterable`.
 
 Here's an example of how to paginate through all the NFTs in Vitalik's ENS address:
 
 ```ts
-import { initializeAlchemy } from '@alch/alchemy-sdk';
+import { Alchemy } from '@alch/alchemy-sdk';
 
-const alchemy = initializeAlchemy();
+const alchemy = new Alchemy();
 
 async function main() {
   const ownerAddress = 'vitalik.eth';
@@ -209,6 +208,7 @@ main();
 The NFT API in the SDK standardizes response types to reduce developer friction, but note this results in some
 differences compared to the Alchemy REST endpoints:
 
+- Methods referencing `Collection` have been renamed to use the name `NftContract` for greater accuracy: e.g. `getNftsForNftContract`.
 - Some methods have different naming that the REST API counterparts in order to provide a consistent API interface (
   e.g. `getNftsForOwner()` is `alchemy_getNfts`, `getOwnersForNft()` is `alchemy_getOwnersForToken`).
 - SDK standardizes to `omitMetadata` parameter (vs. `withMetadata`).
@@ -241,10 +241,10 @@ Getting the NFTs owned by an address.
 ```ts
 import {
   NftExcludeFilters,
-  initializeAlchemy
+  Alchemy
 } from '@alch/alchemy-sdk';
 
-const alchemy = initializeAlchemy();
+const alchemy = new Alchemy();
 
 // Get how many NFTs an address owns.
 alchemy.getNftsForOwner('0xshah.eth').then(nfts => {
@@ -270,16 +270,16 @@ Getting all the owners of the BAYC NFT.
 
 ```ts
 import {
-  initializeAlchemy
+  Alchemy
 } from '@alch/alchemy-sdk';
 
-const alchemy = initializeAlchemy();
+const alchemy = new Alchemy();
 
 // Bored Ape Yacht Club contract address.
 const baycAddress = '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D';
 
 async function main() {
-  for await (const nft of alchemy.getNftsForCollectionIterator(baycAddress, {
+  for await (const nft of alchemy.getNftsForNftContractIterator(baycAddress, {
     // Omit the NFT metadata for smaller payloads.
     omitMetadata: true
   })) {
@@ -295,9 +295,9 @@ main();
 Get all outbound transfers for a provided address.
 
 ```ts
-import { initializeAlchemy } from '@alch/alchemy-sdk';
+import { Alchemy } from '@alch/alchemy-sdk';
 
-const alchemy = initializeAlchemy();
+const alchemy = new Alchemy();
 
 alchemy.getTokenBalances('0x994b342dd87fc825f66e51ffa3ef71ad818b6893').then(
   console.log
diff --git a/src/api/alchemy-websocket-provider.ts b/src/api/alchemy-websocket-provider.ts
index a1f93044..1cb85698 100644
--- a/src/api/alchemy-websocket-provider.ts
+++ b/src/api/alchemy-websocket-provider.ts
@@ -115,7 +115,7 @@ export class AlchemyWebSocketProvider
   }
 
   /**
-   * Overridden implementation of ethers' that includes Alchemy based subscriptions.
+   * Overridden implementation of ethers that includes Alchemy based subscriptions.
    *
    * @param eventName Event to subscribe to
    * @param listener The listener function to call when the event is triggered.
@@ -127,6 +127,86 @@ export class AlchemyWebSocketProvider
     return this._addEventListener(eventName, listener, false);
   }
 
+  /**
+   * Overridden implementation of ethers that includes Alchemy based
+   * subscriptions. Adds a listener to the triggered for only the next
+   * {@link eventName} event, after which it will be removed.
+   *
+   * @param eventName Event to subscribe to
+   * @param listener The listener function to call when the event is triggered.
+   * @override
+   * @public
+   */
+  // TODO: Override `Listener` type to get type autocompletions.
+  once(eventName: AlchemyEventType, listener: Listener): this {
+    return this._addEventListener(eventName, listener, true);
+  }
+
+  /**
+   * Removes the provided {@link listener} for the {@link eventName} event. If no
+   * listener is provided, all listeners for the event will be removed.
+   *
+   * @param eventName Event to unlisten to.
+   * @param listener The listener function to remove.
+   * @override
+   * @public
+   */
+  off(eventName: AlchemyEventType, listener?: Listener): this {
+    if (isAlchemyEvent(eventName)) {
+      return this._off(eventName, listener);
+    } else {
+      return super.off(eventName, listener);
+    }
+  }
+
+  /**
+   * Remove all listeners for the provided {@link eventName} event. If no event
+   * is provided, all events and their listeners are removed.
+   *
+   * @param eventName The event to remove all listeners for.
+   * @override
+   * @public
+   */
+  removeAllListeners(eventName?: AlchemyEventType): this {
+    if (eventName !== undefined && isAlchemyEvent(eventName)) {
+      return this._removeAllListeners(eventName);
+    } else {
+      return super.removeAllListeners(eventName);
+    }
+  }
+
+  /**
+   * Returns the number of listeners for the provided {@link eventName} event. If
+   * no event is provided, the total number of listeners for all events is returned.
+   *
+   * @param eventName The event to get the number of listeners for.
+   * @public
+   * @override
+   */
+  listenerCount(eventName?: AlchemyEventType): number {
+    if (eventName !== undefined && isAlchemyEvent(eventName)) {
+      return this._listenerCount(eventName);
+    } else {
+      return super.listenerCount(eventName);
+    }
+  }
+
+  /**
+   * Returns an array of listeners for the provided {@link eventName} event. If
+   * no event is provided, all listeners will be included.
+   *
+   * @param eventName The event to get the listeners for.
+   * @public
+   * @override
+   */
+  listeners(eventName?: AlchemyEventType): Array<Listener> {
+    if (eventName !== undefined && isAlchemyEvent(eventName)) {
+      return this._listeners(eventName);
+    } else {
+      return super.listeners(eventName);
+    }
+  }
+
   /**
    * Overrides the method in `BaseProvider` in order to properly format the
    * Alchemy subscription events.
@@ -675,6 +755,128 @@ export class AlchemyWebSocketProvider
         throw new Error('Invalid event type to `emitProcessFn()`');
     }
   }
+
+  /**
+   * DO NOT MODIFY.
+   *
+   * Original code copied over from ether.js's `BaseProvider.off()`.
+   *
+   * This method is copied over directly in order to implement Alchemy's unique
+   * subscription types. The only difference is that this method calls
+   * {@link getAlchemyEventTag} instead of the original `getEventTag()` method in
+   * order to parse the Alchemy subscription event.
+   *
+   * @private
+   */
+  private _off(eventName: AlchemyEventType, listener?: Listener): this {
+    if (listener == null) {
+      return this.removeAllListeners(eventName);
+    }
+
+    const stopped: Array<EthersEvent> = [];
+
+    let found = false;
+    const eventTag = getAlchemyEventTag(eventName);
+    this._events = this._events.filter(event => {
+      if (event.tag !== eventTag || event.listener != listener) {
+        return true;
+      }
+      if (found) {
+        return true;
+      }
+      found = true;
+      stopped.push(event);
+      return false;
+    });
+
+    stopped.forEach(event => {
+      this._stopEvent(event);
+    });
+
+    return this;
+  }
+
+  /**
+   * DO NOT MODIFY.
+   *
+   * Original code copied over from ether.js's `BaseProvider.removeAllListeners()`.
+   *
+   * This method is copied over directly in order to implement Alchemy's unique
+   * subscription types. The only difference is that this method calls
+   * {@link getAlchemyEventTag} instead of the original `getEventTag()` method in
+   * order to parse the Alchemy subscription event.
+   *
+   * @private
+   */
+  private _removeAllListeners(eventName: AlchemyEventType): this {
+    let stopped: Array<EthersEvent> = [];
+    if (eventName == null) {
+      stopped = this._events;
+
+      this._events = [];
+    } else {
+      const eventTag = getAlchemyEventTag(eventName);
+      this._events = this._events.filter(event => {
+        if (event.tag !== eventTag) {
+          return true;
+        }
+        stopped.push(event);
+        return false;
+      });
+    }
+
+    stopped.forEach(event => {
+      this._stopEvent(event);
+    });
+
+    return this;
+  }
+
+  /**
+   * DO NOT MODIFY.
+   *
+   * Original code copied over from ether.js's `BaseProvider.listenerCount()`.
+   *
+   * This method is copied over directly in order to implement Alchemy's unique
+   * subscription types. The only difference is that this method calls
+   * {@link getAlchemyEventTag} instead of the original `getEventTag()` method in
+   * order to parse the Alchemy subscription event.
+   *
+   * @private
+   */
+  private _listenerCount(eventName?: AlchemyEventType): number {
+    if (!eventName) {
+      return this._events.length;
+    }
+
+    const eventTag = getAlchemyEventTag(eventName);
+    return this._events.filter(event => {
+      return event.tag === eventTag;
+    }).length;
+  }
+
+  /**
+   * DO NOT MODIFY.
+   *
+   * Original code copied over from ether.js's `BaseProvider.listeners()`.
+   *
+   * This method is copied over directly in order to implement Alchemy's unique
+   * subscription types. The only difference is that this method calls
+   * {@link getAlchemyEventTag} instead of the original `getEventTag()` method in
+   * order to parse the Alchemy subscription event.
+   *
+   * @private
+   */
+  private _listeners(eventName?: AlchemyEventType): Array<Listener> {
+    if (eventName == null) {
+      return this._events.map(event => event.listener);
+    }
+
+    const eventTag = getAlchemyEventTag(eventName);
+    return this._events
+      .filter(event => event.tag === eventTag)
+      .map(event => event.listener);
+  }
 }
 
 function getWebsocketConstructor(): any {
diff --git a/src/api/alchemy.ts b/src/api/alchemy.ts
index 8a9fc28c..9dcbb4e9 100644
--- a/src/api/alchemy.ts
+++ b/src/api/alchemy.ts
@@ -1,5 +1,6 @@
 import {
   AlchemyConfig,
+  AlchemyEventType,
   AssetTransfersParams,
   AssetTransfersResponse,
   DeployResult,
@@ -33,7 +34,6 @@ import {
 } from '../util/const';
 import type { AlchemyWebSocketProvider } from './alchemy-websocket-provider';
 import type { AlchemyProvider } from './alchemy-provider';
-import { BigNumberish } from '@ethersproject/bignumber';
 import { BaseNft, BaseNftContract, Nft, NftContract } from './nft';
 import {
   getNftContractMetadata,
@@ -53,6 +53,24 @@ import {
 } from '../internal/nft-api';
 import { formatBlock } from '../util/util';
 import { toHex } from './util';
+import type { BigNumber, BigNumberish } from '@ethersproject/bignumber';
+import type {
+  Block,
+  BlockTag,
+  BlockWithTransactions,
+  FeeData,
+  TransactionRequest,
+  Listener,
+  TransactionReceipt,
+  TransactionResponse
+} from '@ethersproject/abstract-provider';
+import type { Network as EthersNetworkAlias } from '@ethersproject/networks';
+import type { Deferrable } from '@ethersproject/properties';
+import {
+  Filter,
+  FilterByBlockHash,
+  Log
+} from '@ethersproject/abstract-provider';
 
 /**
  * The Alchemy SDK client. This class holds config information and provides
@@ -66,10 +84,12 @@ export class Alchemy {
   readonly maxRetries: number;
 
   /** @internal */
-  private _baseAlchemyProvider: AlchemyProvider | undefined;
+  private _baseAlchemyProvider: Promise<AlchemyProvider> | undefined;
 
   /** @internal */
-  private _baseAlchemyWssProvider: AlchemyWebSocketProvider | undefined;
+  private _baseAlchemyWssProvider:
+    | Promise<AlchemyWebSocketProvider>
+    | undefined;
 
   /**
    * @param {string} [config.apiKey] - The API key to use for Alchemy
@@ -507,38 +527,430 @@ export class Alchemy {
   }
 
   /**
-   * Creates an AlchemyProvider instance. Only one provider is created per
+   * Returns the balance of a given address as of the provided block.
+   *
+   * @param addressOrName The address or name of the account to get the balance for.
+   * @param blockTag The optional block number or hash to get the balance for.
+   *   Defaults to 'latest' if unspecified.
+   * @public
+   */
+  async getBalance(
+    addressOrName: string | Promise<string>,
+    blockTag?: BlockTag | Promise<BlockTag>
+  ): Promise<BigNumber> {
+    const provider = await this.getProvider();
+    return provider.getBalance(addressOrName, blockTag);
+  }
+
+  /**
+   * Returns the contract code of the provided address at the block. If there is
+   * no contract deployed, the result is `0x`.
+   *
+   * @param addressOrName The address or name of the account to get the code for.
+   * @param blockTag The optional block number or hash to get the code for.
+   *   Defaults to 'latest' if unspecified.
+   * @public
+   */
+  async getCode(
+    addressOrName: string | Promise<string>,
+    blockTag?: BlockTag | Promise<BlockTag>
+  ): Promise<string> {
+    const provider = await this.getProvider();
+    return provider.getCode(addressOrName, blockTag);
+  }
+
+  /**
+   * Return the value of the provided position at the provided address, at the
+   * provided block in `Bytes32` format.
+   *
+   * @param addressOrName The address or name of the account to get the code for.
+   * @param position The position of the storage slot to get.
+   * @param blockTag The optional block number or hash to get the code for.
+   *   Defaults to 'latest' if unspecified.
+   * @public
+   */
+  async getStorageAt(
+    addressOrName: string | Promise<string>,
+    position: BigNumberish | Promise<BigNumberish>,
+    blockTag?: BlockTag | Promise<BlockTag>
+  ): Promise<string> {
+    const provider = await this.getProvider();
+    return provider.getStorageAt(addressOrName, position, blockTag);
+  }
+
+  /**
+   * Returns the number of transactions ever sent from the provided address, as
+   * of the provided block tag. This value is used as the nonce for the next
+   * transaction from the address sent to the network.
+   *
+   * @param addressOrName The address or name of the account to get the nonce for.
+   * @param blockTag The optional block number or hash to get the nonce for.
+   * @public
+   */
+  async getTransactionCount(
+    addressOrName: string | Promise<string>,
+    blockTag?: BlockTag | Promise<BlockTag>
+  ): Promise<number> {
+    const provider = await this.getProvider();
+    return provider.getTransactionCount(addressOrName, blockTag);
+  }
+
+  /**
+   * Returns the block from the network based on the provided block number or
+   * hash. Transactions on the block are represented as an array of transaction
+   * hashes. To get the full transaction details on the block, use
+   * {@link getBlockWithTransactions} instead.
+   *
+   * @param blockHashOrBlockTag The block number or hash to get the block for.
+   * @public
+   */
+  async getBlock(
+    blockHashOrBlockTag: BlockTag | string | Promise<BlockTag | string>
+  ): Promise<Block> {
+    const provider = await this.getProvider();
+    return provider.getBlock(blockHashOrBlockTag);
+  }
+
+  /**
+   * Returns the block from the network based on the provided block number or
+   * hash. Transactions on the block are represented as an array of
+   * {@link TransactionResponse} objects.
+   *
+   * @param blockHashOrBlockTag The block number or hash to get the block for.
+   * @public
+   */
+  async getBlockWithTransactions(
+    blockHashOrBlockTag: BlockTag | string | Promise<BlockTag | string>
+  ): Promise<BlockWithTransactions> {
+    const provider = await this.getProvider();
+    return provider.getBlockWithTransactions(blockHashOrBlockTag);
+  }
+
+  /**
+   * Returns the {@link EthersNetworkAlias} Alchemy is connected to.
+   *
+   * @public
+   */
+  async getNetwork(): Promise<EthersNetworkAlias> {
+    const provider = await this.getProvider();
+    return provider.getNetwork();
+  }
+
+  /**
+   * Returns the block number of the most recently mined block.
+   *
+   * @public
+   */
+  async getBlockNumber(): Promise<number> {
+    const provider = await this.getProvider();
+    return provider.getBlockNumber();
+  }
+
+  /**
+   * Returns the best guess of the current gas price to use in a transaction.
+   *
+   * @public
+   */
+  async getGasPrice(): Promise<BigNumber> {
+    const provider = await this.getProvider();
+    return provider.getGasPrice();
+  }
+
+  /**
+   * Returns the recommended fee data to use in a transaction.
+   *
+   * For an EIP-1559 transaction, the maxFeePerGas and maxPriorityFeePerGas
+   * should be used.
+   *
+   * For legacy transactions and networks which do not support EIP-1559, the
+   * gasPrice should be used.
+   *
+   * @public
+   */
+  async getFeeData(): Promise<FeeData> {
+    const provider = await this.getProvider();
+    return provider.getFeeData();
+  }
+
+  /**
+   * Returns a Promise which will stall until the network has heen established,
+   * ignoring errors due to the target node not being active yet.
+   *
+   * This can be used for testing or attaching scripts to wait until the node is
+   * up and running smoothly.
+   *
+   * @public
+   */
+  async ready(): Promise<EthersNetworkAlias> {
+    const provider = await this.getProvider();
+    return provider.ready;
+  }
+
+  /**
+   * Returns the result of executing the transaction, using call. A call does
+   * not require any ether, but cannot change any state. This is useful for
+   * calling getters on Contracts.
+   *
+   * @param transaction The transaction to execute.
+   * @param blockTag The optional block number or hash to get the call for.
+   * @public
+   */
+  async call(
+    transaction: Deferrable<TransactionRequest>,
+    blockTag?: BlockTag | Promise<BlockTag>
+  ): Promise<string> {
+    const provider = await this.getProvider();
+    return provider.call(transaction, blockTag);
+  }
+
+  /**
+   * Returns an estimate of the amount of gas that would be required to submit
+   * transaction to the network.
+   *
+   * An estimate may not be accurate since there could be another transaction on
+   * the network that was not accounted for, but after being mined affects the
+   * relevant state.
+   *
+   * @param transaction The transaction to estimate gas for.
+   * @public
+   */
+  async estimateGas(
+    transaction: Deferrable<TransactionRequest>
+  ): Promise<BigNumber> {
+    const provider = await this.getProvider();
+    return provider.estimateGas(transaction);
+  }
+
+  /**
+   * Returns the transaction with hash or null if the transaction is unknown.
+   *
+   * If a transaction has not been mined, this method will search the
+   * transaction pool. Various backends may have more restrictive transaction
+   * pool access (e.g. if the gas price is too low or the transaction was only
+   * recently sent and not yet indexed) in which case this method may also return null.
+   *
+   * @param transactionHash The hash of the transaction to get.
+   * @public
+   */
+  async getTransaction(
+    transactionHash: string | Promise<string>
+  ): Promise<TransactionResponse | null> {
+    const provider = await this.getProvider();
+    return provider.getTransaction(transactionHash);
+  }
+
+  /**
+   * Returns the transaction receipt for hash or null if the transaction has not
+   * been mined.
+   *
+   * To stall until the transaction has been mined, consider the
+   * waitForTransaction method below.
+   *
+   * @param transactionHash The hash of the transaction to get.
+   * @public
+   */
+  async getTransactionReceipt(
+    transactionHash: string | Promise<string>
+  ): Promise<TransactionReceipt | null> {
+    const provider = await this.getProvider();
+    return provider.getTransactionReceipt(transactionHash);
+  }
+
+  /**
+   * Submits transaction to the network to be mined. The transaction must be
+   * signed, and be valid (i.e. the nonce is correct and the account has
+   * sufficient balance to pay for the transaction).
+   *
+   * @param signedTransaction The signed transaction to send.
+   * @public
+   */
+  async sendTransaction(
+    signedTransaction: string | Promise<string>
+  ): Promise<TransactionResponse> {
+    const provider = await this.getProvider();
+    return provider.sendTransaction(signedTransaction);
+  }
+
+  /**
+   * Returns a promise which will not resolve until specified transaction hash is mined.
+   *
+   * If {@link confirmations} is 0, this method is non-blocking and if the
+   * transaction has not been mined returns null. Otherwise, this method will
+   * block until the transaction has confirmed blocks mined on top of the block
+   * in which it was mined.
+   *
+   * @param transactionHash The hash of the transaction to wait for.
+   * @param confirmations The number of blocks to wait for.
+   * @param timeout The maximum time to wait for the transaction to confirm.
+   * @public
+   */
+  async waitForTransaction(
+    transactionHash: string,
+    confirmations?: number,
+    timeout?: number
+  ): Promise<TransactionReceipt | null> {
+    const provider = await this.getProvider();
+    return provider.waitForTransaction(transactionHash, confirmations, timeout);
+  }
+
+  /**
+   * Returns an array of logs that match the provided filter.
+   *
+   * @param filter The filter object to use.
+   * @public
+   */
+  async getLogs(
+    filter: Filter | FilterByBlockHash | Promise<Filter | FilterByBlockHash>
+  ): Promise<Array<Log>> {
+    const provider = await this.getProvider();
+    return provider.getLogs(filter);
+  }
+
+  /**
+   * Allows sending a raw message to the Alchemy backend.
+   *
+   * @param method The method to call.
+   * @param params The parameters to pass to the method.
+   * @public
+   */
+  async send(method: string, params: Array<any>): Promise<any> {
+    const provider = await this.getProvider();
+    return provider.send(method, params);
+  }
+
+  /**
+   * Adds a listener to be triggered for each {@link eventName} event. Also
+   * includes Alchemy's Subscription API events. See {@link AlchemyEventType} for
+   * how to use them.
+   *
+   * @param eventName The event to listen for.
+   * @param listener The listener to call when the event is triggered.
+   * @public
+   */
+  on(eventName: AlchemyEventType, listener: Listener): this {
+    void (async () => {
+      const provider = await this.getWebsocketProvider();
+      provider.on(eventName, listener);
+    })();
+    return this;
+  }
+
+  /**
+   * Adds a listener to be triggered for only the next {@link eventName} event,
+   * after which it will be removed. Also includes Alchemy's Subscription API
+   * events. See {@link AlchemyEventType} for how to use them.
+   *
+   * @param eventName The event to listen for.
+   * @param listener The listener to call when the event is triggered.
+   * @public
+   */
+  once(eventName: AlchemyEventType, listener: Listener): this {
+    void (async () => {
+      const provider = await this.getWebsocketProvider();
+      provider.once(eventName, listener);
+    })();
+    return this;
+  }
+
+  /**
+   * Removes the provided {@link listener} for the {@link eventName} event. If no
+   * listener is provided, all listeners for the event will be removed.
+   *
+   * @param eventName The event to unlisten to.
+   * @param listener The listener to remove.
+   * @public
+   */
+  off(eventName: AlchemyEventType, listener?: Listener): this {
+    void (async () => {
+      const provider = await this.getWebsocketProvider();
+      return provider.off(eventName, listener);
+    })();
+    return this;
+  }
+
+  /**
+   * Remove all listeners for the provided {@link eventName} event. If no event
+   * is provided, all events and their listeners are removed.
+   *
+   * @param eventName The event to remove all listeners for.
+   * @public
+   */
+  removeAllListeners(eventName?: AlchemyEventType): this {
+    void (async () => {
+      const provider = await this.getWebsocketProvider();
+      provider.removeAllListeners(eventName);
+    })();
+    return this;
+  }
+
+  /**
+   * Returns the number of listeners for the provided {@link eventName} event. If
+   * no event is provided, the total number of listeners for all events is returned.
+   *
+   * @param eventName The event to get the number of listeners for.
+   * @public
+   */
+  async listenerCount(eventName?: AlchemyEventType): Promise<number> {
+    const provider = await this.getWebsocketProvider();
+    return provider.listenerCount(eventName);
+  }
+
+  /**
+   * Returns an array of listeners for the provided {@link eventName} event. If
+   * no event is provided, all listeners will be included.
+   *
+   * @param eventName The event to get the listeners for.
+   */
+  async listeners(eventName?: AlchemyEventType): Promise<Listener[]> {
+    const provider = await this.getWebsocketProvider();
+    return provider.listeners(eventName);
+  }
+
+  /**
+   * Returns an AlchemyProvider instance. Only one provider is created per
    * Alchemy instance.
    *
+   * The AlchemyProvider is a wrapper around ether's `AlchemyProvider` class and
+   * has been expanded to support Alchemy's Enhanced APIs.
+   *
+   * Most common methods on the provider are available as top-level methods on
+   * the {@link Alchemy} instance, but the provider is exposed here to access
+   * other less-common methods.
+   *
    * @public
    */
-  async getProvider(): Promise<AlchemyProvider> {
+  getProvider(): Promise<AlchemyProvider> {
     if (!this._baseAlchemyProvider) {
-      const { AlchemyProvider } = await import('./alchemy-provider');
-      this._baseAlchemyProvider = new AlchemyProvider(
-        this.network,
-        this.apiKey,
-        this.maxRetries
-      );
+      this._baseAlchemyProvider = (async () => {
+        const { AlchemyProvider } = await import('./alchemy-provider');
+        return new AlchemyProvider(this.network, this.apiKey, this.maxRetries);
+      })();
     }
     return this._baseAlchemyProvider;
   }
 
   /**
-   * Creates an AlchemyWebsocketProvider instance. Only one provider is created
+   * Returns an AlchemyWebsocketProvider instance. Only one provider is created
    * per Alchemy instance.
    *
+   * The AlchemyWebSocketProvider is a wrapper around ether's
+   * `AlchemyWebSocketProvider` class and has been expanded to support Alchemy's
+   * Subscription APIs, automatic backfilling, and other performance improvements.
+   *
+   * Most common methods on the provider are available as top-level methods on
+   * the {@link Alchemy} instance, but the provider is exposed here to access
+   * other less-common methods.
+   *
    * @public
    */
-  async getWebsocketProvider(): Promise<AlchemyWebSocketProvider> {
+  getWebsocketProvider(): Promise<AlchemyWebSocketProvider> {
     if (!this._baseAlchemyWssProvider) {
-      const { AlchemyWebSocketProvider } = await import(
-        './alchemy-websocket-provider'
-      );
-      this._baseAlchemyWssProvider = new AlchemyWebSocketProvider(
-        this.network,
-        this.apiKey
-      );
+      this._baseAlchemyWssProvider = (async () => {
+        const { AlchemyWebSocketProvider } = await import(
+          './alchemy-websocket-provider'
+        );
+        return new AlchemyWebSocketProvider(this.network, this.apiKey);
+      })();
     }
     return this._baseAlchemyWssProvider;
   }
diff --git a/src/internal/nft-api.ts b/src/internal/nft-api.ts
index e658ec6f..ae5801ae 100644
--- a/src/internal/nft-api.ts
+++ b/src/internal/nft-api.ts
@@ -248,7 +248,7 @@ export async function* getNftsForNftContractIterator(
   for await (const response of paginateEndpoint(
     alchemy,
     AlchemyApiType.NFT,
-    'getNFTsForNftContract',
+    'getNFTsForCollection',
     'startToken',
     'nextToken',
     {
diff --git a/src/types/types.ts b/src/types/types.ts
index 1ddfea08..ae34cbef 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -15,7 +15,10 @@ export interface AlchemyConfig {
   /** The Alchemy API key that can be found in the Alchemy dashboard. */
   apiKey?: string;
 
-  /** The name of the network. */
+  /**
+   * The name of the network. Once configured, the network cannot be changed. To
+   * use a different network, instantiate a new `Alchemy` instance
+   */
   network?: Network;
 
   /** The maximum number of retries to attempt if a request fails. Defaults to 5. */
diff --git a/tsconfig.json b/tsconfig.json
index b6217e2d..c7a26bd0 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -20,5 +20,5 @@
     "typeRoots": ["node_modules/@types"]
   },
   "include": ["src/**/*.ts", "test/**/*.ts"],
-  "exclude": ["dist/**/*"]
+  "exclude": ["dist/**/*", "node_modules/**/*"],
 }


Test patch:
diff --git a/test/unit/alchemy.test.ts b/test/unit/alchemy.test.ts
index 6ea39b8c..08c677b1 100644
--- a/test/unit/alchemy.test.ts
+++ b/test/unit/alchemy.test.ts
@@ -43,4 +43,30 @@ describe('Alchemy class', () => {
     expect(alchemy.network).toEqual(DEFAULT_NETWORK);
     expect(alchemy.maxRetries).toEqual(DEFAULT_MAX_RETRIES);
   });
+
+  it('reuses the same provider', async () => {
+    const alchemy = new Alchemy();
+    const provider = await alchemy.getProvider();
+    const provider2 = await alchemy.getProvider();
+    expect(provider).toBe(provider2);
+
+    const wsProvider = await alchemy.getWebsocketProvider();
+    const wsProvider2 = await alchemy.getWebsocketProvider();
+    expect(wsProvider).toBe(wsProvider2);
+  });
+
+  it('providers are loaded once', async () => {
+    const alchemy = new Alchemy();
+    const providerPromise = alchemy.getProvider();
+    const provider2Promise = alchemy.getProvider();
+    const provider = await providerPromise;
+    const provider2 = await provider2Promise;
+    expect(provider).toBe(provider2);
+
+    const wsProviderPromise = alchemy.getWebsocketProvider();
+    const wsProvider2Promise = alchemy.getWebsocketProvider();
+    const wsProvider = await wsProviderPromise;
+    const wsProvider2 = await wsProvider2Promise;
+    expect(wsProvider).toBe(wsProvider2);
+  });
 });