opensea-js
Version:
TypeScript SDK for the OpenSea marketplace helps developers build new experiences using NFTs and our marketplace data
1,073 lines (1,026 loc) • 39.8 kB
text/typescript
import EventEmitter = require("events");
import { Seaport } from "@opensea/seaport-js";
import { OrderComponents } from "@opensea/seaport-js/lib/types";
import {
BigNumberish,
Overrides,
Signer,
ethers,
JsonRpcProvider,
} from "ethers";
import { OpenSeaAPI } from "./api/api";
import { CollectionOffer, Listing, Offer, Order } from "./api/types";
import { OrderV2 } from "./orders/types";
import { AssetsManager } from "./sdk/assets";
import { CancellationManager } from "./sdk/cancellation";
import { FulfillmentManager } from "./sdk/fulfillment";
import { BulkOrderResult, OrdersManager } from "./sdk/orders";
import { TokensManager } from "./sdk/tokens";
import {
EventData,
EventType,
Chain,
OpenSeaAPIConfig,
OrderSide,
AssetWithTokenStandard,
AssetWithTokenId,
} from "./types";
import {
getDefaultConduit,
getOfferPaymentToken,
getListingPaymentToken,
getSeaportAddress,
} from "./utils/utils";
/**
* The OpenSea SDK main class.
* @category Main Classes
*/
export class OpenSeaSDK {
/** Provider to use for transactions. */
public provider: JsonRpcProvider;
/** Seaport client @see {@link https://github.com/ProjectOpenSea/seaport-js} */
public seaport: Seaport;
/** Logger function to use when debugging */
public logger: (arg: string) => void;
/** API instance */
public readonly api: OpenSeaAPI;
/** The configured chain */
public readonly chain: Chain;
/** Internal cache of decimals for payment tokens to save network requests */
private _cachedPaymentTokenDecimals: { [address: string]: number } = {};
private _emitter: EventEmitter;
private _signerOrProvider: Signer | JsonRpcProvider;
// Manager instances
private _tokensManager: TokensManager;
private _assetsManager: AssetsManager;
private _cancellationManager: CancellationManager;
private _ordersManager: OrdersManager;
private _fulfillmentManager: FulfillmentManager;
/**
* Create a new instance of OpenSeaSDK.
* @param signerOrProvider Signer or provider to use for transactions. For example:
* `new ethers.providers.JsonRpcProvider('https://mainnet.infura.io')` or
* `new ethers.Wallet(privKey, provider)`
* @param apiConfig configuration options, including `chain`
* @param logger optional function for logging debug strings. defaults to no logging
*/
constructor(
signerOrProvider: Signer | JsonRpcProvider,
apiConfig: OpenSeaAPIConfig = {},
logger?: (arg: string) => void,
) {
// API config
apiConfig.chain ??= Chain.Mainnet;
this.chain = apiConfig.chain;
this.api = new OpenSeaAPI(apiConfig);
this.provider = ((signerOrProvider as Signer).provider ??
signerOrProvider) as JsonRpcProvider;
this._signerOrProvider = signerOrProvider ?? this.provider;
const defaultConduit = getDefaultConduit(this.chain);
const seaportAddress = getSeaportAddress(this.chain);
this.seaport = new Seaport(this._signerOrProvider, {
conduitKeyToConduit: {
[defaultConduit.key]: defaultConduit.address,
},
overrides: {
defaultConduitKey: defaultConduit.key,
contractAddress: seaportAddress,
},
});
// Emit events
this._emitter = new EventEmitter();
// Logger: default to no logging if fn not provided
this.logger = logger ?? ((arg: string) => arg);
// Cache decimals for offer and listing payment tokens to skip network request
const offerPaymentToken = getOfferPaymentToken(this.chain).toLowerCase();
const listingPaymentToken = getListingPaymentToken(
this.chain,
).toLowerCase();
this._cachedPaymentTokenDecimals[offerPaymentToken] = 18;
this._cachedPaymentTokenDecimals[listingPaymentToken] = 18;
// Create shared context for all managers
const context = {
chain: this.chain,
signerOrProvider: this._signerOrProvider,
provider: this.provider,
api: this.api,
seaport: this.seaport,
logger: this.logger,
dispatch: this._dispatch.bind(this),
confirmTransaction: this._confirmTransaction.bind(this),
requireAccountIsAvailable: this._requireAccountIsAvailable.bind(this),
};
// Initialize manager instances
this._tokensManager = new TokensManager(context);
this._assetsManager = new AssetsManager(context);
this._cancellationManager = new CancellationManager(context);
this._ordersManager = new OrdersManager(
context,
this._getPriceParameters.bind(this),
);
this._fulfillmentManager = new FulfillmentManager(
context,
this._ordersManager,
);
}
/**
* Add a listener for events emitted by the SDK.
* @param event The {@link EventType} to listen to.
* @param listener A callback that will accept an object with {@link EventData}\
* @param once Whether the listener should only be called once, or continue listening until removed.
*/
public addListener(
event: EventType,
listener: (data: EventData) => void,
once = false,
) {
if (once) {
this._emitter.once(event, listener);
} else {
this._emitter.addListener(event, listener);
}
}
/**
* Remove an event listener by calling `.removeListener()` on an event and listener.
* @param event The {@link EventType} to remove a listener for\
* @param listener The listener to remove
*/
public removeListener(event: EventType, listener: (data: EventData) => void) {
this._emitter.removeListener(event, listener);
}
/**
* Remove all event listeners. This should be called when you're unmounting
* a component that listens to events to make UI updates.
* @param event Optional EventType to remove listeners for
*/
public removeAllListeners(event?: EventType) {
this._emitter.removeAllListeners(event);
}
/**
* Wrap native asset into wrapped native asset (e.g. ETH into WETH, POL into WPOL).
* Wrapped native assets are needed for making offers.
* @param options
* @param options.amountInEth Amount of native asset to wrap
* @param options.accountAddress Address of the user's wallet containing the native asset
*/
public async wrapEth({
amountInEth,
accountAddress,
}: {
amountInEth: BigNumberish;
accountAddress: string;
}) {
return this._tokensManager.wrapEth({ amountInEth, accountAddress });
}
/**
* Unwrap wrapped native asset into native asset (e.g. WETH into ETH, WPOL into POL).
* Emits the `UnwrapWeth` event when the transaction is prompted.
* @param options
* @param options.amountInEth How much wrapped native asset to unwrap
* @param options.accountAddress Address of the user's wallet containing the wrapped native asset
*/
public async unwrapWeth({
amountInEth,
accountAddress,
}: {
amountInEth: BigNumberish;
accountAddress: string;
}) {
return this._tokensManager.unwrapWeth({ amountInEth, accountAddress });
}
/**
* Create and submit an offer on an asset.
* @param options
* @param options.asset The asset to trade. tokenAddress and tokenId must be defined.
* @param options.accountAddress Address of the wallet making the offer.
* @param options.amount Amount in decimal format (e.g., "1.5" for 1.5 ETH, not wei). Automatically converted to base units.
* @param options.quantity Number of assets to bid for. Defaults to 1.
* @param options.domain Optional domain for on-chain attribution. Hashed and included in salt.
* @param options.salt Arbitrary salt. Auto-generated if not provided.
* @param options.expirationTime Expiration time for the order, in UTC seconds
* @param options.paymentTokenAddress ERC20 address for the payment token in the order. If unspecified, defaults to WETH
* @param options.zone Zone for order protection. Defaults to chain's signed zone.
*
* @returns The {@link OrderV2} that was created.
*
* @throws Error if the asset does not contain a token id.
* @throws Error if the accountAddress is not available through wallet or provider.
* @throws Error if the amount is not greater than 0.
* @throws Error if paymentTokenAddress is not WETH on anything other than Ethereum mainnet.
*/
public async createOffer({
asset,
accountAddress,
amount,
quantity = 1,
domain,
salt,
expirationTime,
paymentTokenAddress,
zone,
}: {
asset: AssetWithTokenId;
accountAddress: string;
amount: BigNumberish;
quantity?: BigNumberish;
domain?: string;
salt?: BigNumberish;
expirationTime?: BigNumberish;
paymentTokenAddress?: string;
zone?: string;
}): Promise<OrderV2> {
return this._ordersManager.createOffer({
asset,
accountAddress,
amount,
quantity,
domain,
salt,
expirationTime,
paymentTokenAddress,
zone,
});
}
/**
* Create and submit a listing for an asset.
* @param options
* @param options.asset The asset to trade. tokenAddress and tokenId must be defined.
* @param options.accountAddress Address of the wallet making the listing
* @param options.amount Amount in decimal format (e.g., "1.5" for 1.5 ETH, not wei). Automatically converted to base units.
* @param options.quantity Number of assets to list. Defaults to 1.
* @param options.domain Optional domain for on-chain attribution. Hashed and included in salt.
* @param options.salt Arbitrary salt. Auto-generated if not provided.
* @param options.listingTime Optional time when the order will become fulfillable, in UTC seconds. Undefined means it will start now.
* @param options.expirationTime Expiration time for the order, in UTC seconds.
* @param options.paymentTokenAddress ERC20 address for the payment token in the order. If unspecified, defaults to ETH
* @param options.buyerAddress Optional address that's allowed to purchase this item. If specified, no other address will be able to take the order, unless its value is the null address.
* @param options.includeOptionalCreatorFees If true, optional creator fees will be included in the listing. Default: false.
* @param options.zone Zone for order protection. Defaults to no zone.
* @returns The {@link OrderV2} that was created.
*
* @throws Error if the asset does not contain a token id.
* @throws Error if the accountAddress is not available through wallet or provider.
* @throws Error if the amount is not greater than 0.
* @throws Error if paymentTokenAddress is not WETH on anything other than Ethereum mainnet.
*/
public async createListing({
asset,
accountAddress,
amount,
quantity = 1,
domain,
salt,
listingTime,
expirationTime,
paymentTokenAddress,
buyerAddress,
includeOptionalCreatorFees = false,
zone,
}: {
asset: AssetWithTokenId;
accountAddress: string;
amount: BigNumberish;
quantity?: BigNumberish;
domain?: string;
salt?: BigNumberish;
listingTime?: number;
expirationTime?: number;
paymentTokenAddress?: string;
buyerAddress?: string;
includeOptionalCreatorFees?: boolean;
zone?: string;
}): Promise<OrderV2> {
return this._ordersManager.createListing({
asset,
accountAddress,
amount,
quantity,
domain,
salt,
listingTime,
expirationTime,
paymentTokenAddress,
buyerAddress,
includeOptionalCreatorFees,
zone,
});
}
/**
* Create and submit multiple listings using Seaport's bulk order creation.
* This method uses a single signature for all listings and submits them individually to the OpenSea API with rate limit handling.
* All listings must be from the same account address.
*
* Note: If only one listing is provided, this method will use a normal order signature instead of a bulk signature,
* as bulk signatures are more expensive to decode on-chain due to the merkle proof verification.
*
* @param options
* @param options.listings Array of listing parameters. Each listing requires asset, amount, and optionally other listing parameters.
* @param options.accountAddress Address of the wallet making the listings
* @param options.continueOnError If true, continue submitting remaining listings even if some fail. Default: false (throw on first error).
* @param options.onProgress Optional callback for progress updates. Called after each listing is submitted (successfully or not).
* @returns {@link BulkOrderResult} containing successful orders and any failures.
*
* @throws Error if listings array is empty
* @throws Error if the accountAddress is not available through wallet or provider.
* @throws Error if any asset does not contain a token id.
* @throws Error if continueOnError is false and any submission fails.
*/
public async createBulkListings({
listings,
accountAddress,
continueOnError,
onProgress,
}: {
listings: Array<{
asset: AssetWithTokenId;
amount: BigNumberish;
quantity?: BigNumberish;
domain?: string;
salt?: BigNumberish;
listingTime?: number;
expirationTime?: number;
paymentTokenAddress?: string;
buyerAddress?: string;
includeOptionalCreatorFees?: boolean;
zone?: string;
}>;
accountAddress: string;
continueOnError?: boolean;
onProgress?: (completed: number, total: number) => void;
}): Promise<BulkOrderResult> {
return this._ordersManager.createBulkListings({
listings,
accountAddress,
continueOnError,
onProgress,
});
}
/**
* Create and submit multiple offers using Seaport's bulk order creation.
* This method uses a single signature for all offers and submits them individually to the OpenSea API with rate limit handling.
* All offers must be from the same account address.
*
* Note: If only one offer is provided, this method will use a normal order signature instead of a bulk signature,
* as bulk signatures are more expensive to decode on-chain due to the merkle proof verification.
*
* @param options
* @param options.offers Array of offer parameters. Each offer requires asset, amount, and optionally other offer parameters.
* @param options.accountAddress Address of the wallet making the offers
* @param options.continueOnError If true, continue submitting remaining offers even if some fail. Default: false (throw on first error).
* @param options.onProgress Optional callback for progress updates. Called after each offer is submitted (successfully or not).
* @returns {@link BulkOrderResult} containing successful orders and any failures.
*
* @throws Error if offers array is empty
* @throws Error if the accountAddress is not available through wallet or provider.
* @throws Error if any asset does not contain a token id.
* @throws Error if continueOnError is false and any submission fails.
*/
public async createBulkOffers({
offers,
accountAddress,
continueOnError,
onProgress,
}: {
offers: Array<{
asset: AssetWithTokenId;
amount: BigNumberish;
quantity?: BigNumberish;
domain?: string;
salt?: BigNumberish;
expirationTime?: BigNumberish;
paymentTokenAddress?: string;
zone?: string;
}>;
accountAddress: string;
continueOnError?: boolean;
onProgress?: (completed: number, total: number) => void;
}): Promise<BulkOrderResult> {
return this._ordersManager.createBulkOffers({
offers,
accountAddress,
continueOnError,
onProgress,
});
}
/**
* Create and submit a collection offer.
* @param options
* @param options.collectionSlug Identifier for the collection.
* @param options.accountAddress Address of the wallet making the offer.
* @param options.amount Amount in decimal format (e.g., "1.5" for 1.5 ETH, not wei). Automatically converted to base units.
* @param options.quantity Number of assets to bid for.
* @param options.domain Optional domain for on-chain attribution. Hashed and included in salt.
* @param options.salt Arbitrary salt. Auto-generated if not provided.
* @param options.expirationTime Expiration time (UTC seconds).
* @param options.paymentTokenAddress Payment token address. Defaults to WETH.
* @param options.offerProtectionEnabled Use signed zone for protection against disabled items. Default: true.
* @param options.traitType If defined, the trait name to create the collection offer for.
* @param options.traitValue If defined, the trait value to create the collection offer for.
* @param options.traits If defined, an array of traits to create the multi-trait collection offer for.
* @returns The {@link CollectionOffer} that was created.
*/
public async createCollectionOffer({
collectionSlug,
accountAddress,
amount,
quantity,
domain,
salt,
expirationTime,
paymentTokenAddress,
offerProtectionEnabled = true,
traitType,
traitValue,
traits,
}: {
collectionSlug: string;
accountAddress: string;
amount: BigNumberish;
quantity: number;
domain?: string;
salt?: BigNumberish;
expirationTime?: number | string;
paymentTokenAddress: string;
offerProtectionEnabled?: boolean;
traitType?: string;
traitValue?: string;
traits?: Array<{ type: string; value: string }>;
}): Promise<CollectionOffer | null> {
return this._ordersManager.createCollectionOffer({
collectionSlug,
accountAddress,
amount,
quantity,
domain,
salt,
expirationTime,
paymentTokenAddress,
offerProtectionEnabled,
traitType,
traitValue,
traits,
});
}
/**
* Fulfill an order for an asset. The order can be either a listing or an offer.
* Uses the OpenSea API to generate fulfillment transaction data and executes it directly.
* @param options
* @param options.order The order to fulfill, a.k.a. "take"
* @param options.accountAddress Address of the wallet taking the offer.
* @param options.assetContractAddress Optional address of the NFT contract for criteria offers (e.g., collection offers). Required when fulfilling collection offers.
* @param options.tokenId Optional token ID for criteria offers (e.g., collection offers). Required when fulfilling collection offers.
* @param options.unitsToFill Optional number of units to fill. Defaults to 1 for both listings and offers.
* @param options.recipientAddress Optional recipient address for the NFT when fulfilling a listing. Not applicable for offers.
* @param options.includeOptionalCreatorFees Whether to include optional creator fees in the fulfillment. If creator fees are already required, this is a no-op. Defaults to false.
* @param options.overrides Transaction overrides, ignored if not set.
* @returns Transaction hash of the order.
*
* @throws Error if the accountAddress is not available through wallet or provider.
* @throws Error if the order's protocol address is not supported by OpenSea. See {@link isValidProtocol}.
* @throws Error if a signer is not provided (read-only providers cannot fulfill orders).
* @throws Error if the order hash is not available.
*/
public async fulfillOrder({
order,
accountAddress,
assetContractAddress,
tokenId,
unitsToFill,
recipientAddress,
includeOptionalCreatorFees = false,
overrides,
}: {
order: OrderV2 | Order | Listing | Offer;
accountAddress: string;
assetContractAddress?: string;
tokenId?: string;
unitsToFill?: BigNumberish;
recipientAddress?: string;
includeOptionalCreatorFees?: boolean;
overrides?: Overrides;
}): Promise<string> {
return this._fulfillmentManager.fulfillOrder({
order,
accountAddress,
assetContractAddress,
tokenId,
unitsToFill,
recipientAddress,
includeOptionalCreatorFees,
overrides,
});
}
/**
* Cancel multiple orders onchain, preventing them from being fulfilled.
* This method accepts either full OrderV2 objects, OrderComponents, or order hashes with protocol address.
*
* **Event Behavior**: For backwards compatibility with the singular `cancelOrder` method,
* this method dispatches a `CancelOrder` event for the first order only, and only when
* an OrderV2 object is available (either provided directly or fetched via orderHashes).
* No event is dispatched when using OrderComponents directly, as they lack the full order data.
*
* @param options
* @param options.orders Array of orders to cancel. Can be OrderV2 objects or OrderComponents.
* @param options.orderHashes Optional array of order hashes to cancel. Must provide protocolAddress if using this.
* @param options.accountAddress The account address cancelling the orders.
* @param options.protocolAddress Required when using orderHashes. The Seaport protocol address for the orders.
* @param options.domain Optional domain for on-chain attribution. Hashed and included in calldata.
* @param options.overrides Transaction overrides, ignored if not set.
* @returns Transaction hash of the cancellation.
*
* @throws Error if orderHashes is provided without protocolAddress.
* @throws Error if neither orders nor orderHashes is provided.
* @throws Error if the accountAddress is not available through wallet or provider.
* @throws Error if the order's protocol address is not supported by OpenSea. See {@link isValidProtocol}.
*/
public async cancelOrders({
orders,
orderHashes,
accountAddress,
protocolAddress,
domain,
overrides,
}: {
orders?: Array<OrderV2 | OrderComponents>;
orderHashes?: string[];
accountAddress: string;
protocolAddress?: string;
domain?: string;
overrides?: Overrides;
}): Promise<string> {
return this._cancellationManager.cancelOrders({
orders,
orderHashes,
accountAddress,
protocolAddress,
domain,
overrides,
});
}
/**
* Cancel an order onchain, preventing it from ever being fulfilled.
* This method accepts either a full OrderV2 object or an order hash with protocol address.
*
* @param options
* @param options.order The order to cancel (OrderV2 object)
* @param options.orderHash Optional order hash to cancel. Must provide protocolAddress if using this.
* @param options.accountAddress The account address that will be cancelling the order.
* @param options.protocolAddress Required when using orderHash. The Seaport protocol address for the order.
* @param options.domain Optional domain for on-chain attribution. Hashed and included in calldata.
*
* @throws Error if neither order nor orderHash is provided.
* @throws Error if the accountAddress is not available through wallet or provider.
* @throws Error if the order's protocol address is not supported by OpenSea. See {@link isValidProtocol}.
*
* @example
* // Cancel using OrderV2 object
* await sdk.cancelOrder({
* order: orderV2Object,
* accountAddress: "0x..."
* });
*
* @example
* // Cancel using order hash
* await sdk.cancelOrder({
* orderHash: "0x123...",
* protocolAddress: "0xabc...",
* accountAddress: "0x..."
* });
*/
public async cancelOrder({
order,
orderHash,
accountAddress,
protocolAddress,
domain,
}: {
order?: OrderV2;
orderHash?: string;
accountAddress: string;
protocolAddress?: string;
domain?: string;
}) {
return this._cancellationManager.cancelOrder({
order,
orderHash,
accountAddress,
protocolAddress,
domain,
});
}
/**
* Offchain cancel an order, offer or listing, by its order hash when protected by the SignedZone.
* Protocol and Chain are required to prevent hash collisions.
* Please note cancellation is only assured if a fulfillment signature was not vended prior to cancellation.
* @param protocolAddress The Seaport address for the order.
* @param orderHash The order hash, or external identifier, of the order.
* @param chain The chain where the order is located.
* @param offererSignature An EIP-712 signature from the offerer of the order.
* If this is not provided, the API key used to initialize the SDK must belong to the order's offerer.
* The signature must be a EIP-712 signature consisting of the order's Seaport contract's
* name, version, address, and chain. The struct to sign is `OrderHash` containing a
* single bytes32 field.
* @param useSignerToDeriveOffererSignature Derive the offererSignature from the Ethers signer passed into this sdk.
* @returns The response from the API.
*/
public async offchainCancelOrder(
protocolAddress: string,
orderHash: string,
chain: Chain = this.chain,
offererSignature?: string,
useSignerToDeriveOffererSignature?: boolean,
) {
return this._cancellationManager.offchainCancelOrder(
protocolAddress,
orderHash,
chain,
offererSignature,
useSignerToDeriveOffererSignature,
);
}
/**
* Returns whether an order is fulfillable.
* An order may not be fulfillable if a target item's transfer function
* is locked for some reason, e.g. an item is being rented within a game
* or trading has been locked for an item type.
* @param options
* @param options.order Order to check
* @param options.accountAddress The account address that will be fulfilling the order
* @returns True if the order is fulfillable, else False.
*
* @throws Error if the order's protocol address is not supported by OpenSea. See {@link isValidProtocol}.
*/
public async isOrderFulfillable({
order,
accountAddress,
}: {
order: OrderV2;
accountAddress: string;
}): Promise<boolean> {
return this._fulfillmentManager.isOrderFulfillable({
order,
accountAddress,
});
}
/**
* Get an account's balance of any Asset. This asset can be an ERC20, ERC1155, or ERC721.
* @param options
* @param options.accountAddress Account address to check
* @param options.asset The Asset to check balance for. tokenStandard must be set.
* @returns The balance of the asset for the account.
*
* @throws Error if the token standard does not support balanceOf.
*/
public async getBalance({
accountAddress,
asset,
}: {
accountAddress: string;
asset: AssetWithTokenStandard;
}): Promise<bigint> {
return this._assetsManager.getBalance({ accountAddress, asset });
}
/**
* Transfer an asset. This asset can be an ERC20, ERC1155, or ERC721.
* @param options
* @param options.asset The Asset to transfer. tokenStandard must be set.
* @param options.amount Amount of asset to transfer. Not used for ERC721.
* @param options.fromAddress The address to transfer from
* @param options.toAddress The address to transfer to
* @param options.overrides Transaction overrides, ignored if not set.
*/
public async transfer({
asset,
amount,
fromAddress,
toAddress,
overrides,
}: {
asset: AssetWithTokenStandard;
amount?: BigNumberish;
fromAddress: string;
toAddress: string;
overrides?: Overrides;
}): Promise<void> {
return this._assetsManager.transfer({
asset,
amount,
fromAddress,
toAddress,
overrides,
});
}
/**
* Bulk transfer multiple assets using OpenSea's TransferHelper contract.
* This method is more gas-efficient than calling transfer() multiple times.
* Note: All assets must be approved for transfer to the OpenSea conduit before calling this method.
* @param options
* @param options.assets Array of assets to transfer. Each asset must have tokenStandard set.
* @param options.fromAddress The address to transfer from
* @param options.overrides Transaction overrides, ignored if not set.
* @returns Transaction hash of the bulk transfer
*
* @throws Error if any asset is missing required fields (tokenId for NFTs, amount for ERC20/ERC1155).
* @throws Error if any asset is not approved for transfer to the OpenSea conduit.
* @throws Error if the fromAddress is not available through wallet or provider.
*/
public async bulkTransfer({
assets,
fromAddress,
overrides,
}: {
assets: Array<{
asset: AssetWithTokenStandard;
toAddress: string;
amount?: BigNumberish;
}>;
fromAddress: string;
overrides?: Overrides;
}): Promise<string> {
return this._assetsManager.bulkTransfer({
assets,
fromAddress,
overrides,
});
}
/**
* Batch approve multiple assets for transfer to the OpenSea conduit.
* This method checks which assets need approval and batches them efficiently:
* - 0 approvals needed: Returns early
* - 1 approval needed: Sends single transaction
* - 2+ approvals needed: Uses Multicall3 to batch all approvals in one transaction
*
* @param options
* @param options.assets Array of assets to approve for transfer
* @param options.fromAddress The address that owns the assets
* @param options.overrides Transaction overrides, ignored if not set.
* @returns Transaction hash of the approval transaction, or undefined if no approvals needed
*
* @throws Error if the fromAddress is not available through wallet or provider.
*/
public async batchApproveAssets({
assets,
fromAddress,
overrides,
}: {
assets: Array<{
asset: AssetWithTokenStandard;
amount?: BigNumberish;
}>;
fromAddress: string;
overrides?: Overrides;
}): Promise<string | undefined> {
return this._assetsManager.batchApproveAssets({
assets,
fromAddress,
overrides,
});
}
/**
* Instead of signing an off-chain order, this methods allows you to approve an order
* with an on-chain transaction.
* @param order Order to approve
* @param domain An optional domain to be hashed and included at the end of fulfillment calldata. This can be used for on-chain order attribution to assist with analytics.
* @returns Transaction hash of the approval transaction
*
* @throws Error if the accountAddress is not available through wallet or provider.
* @throws Error if the order's protocol address is not supported by OpenSea. See {@link isValidProtocol}.
*/
public async approveOrder(order: OrderV2, domain?: string) {
return this._fulfillmentManager.approveOrder(order, domain);
}
/**
* Validates an order onchain using Seaport's validate() method. This submits the order onchain
* and pre-validates the order using Seaport, which makes it cheaper to fulfill since a signature
* is not needed to be verified during fulfillment for the order, but is not strictly required
* and the alternative is orders can be submitted to the API for free instead of sent onchain.
* @param orderComponents Order components to validate onchain
* @param accountAddress Address of the wallet that will pay the gas to validate the order
* @returns Transaction hash of the validation transaction
*
* @throws Error if the accountAddress is not available through wallet or provider.
*/
public async validateOrderOnchain(
orderComponents: OrderComponents,
accountAddress: string,
) {
return this._fulfillmentManager.validateOrderOnchain(
orderComponents,
accountAddress,
);
}
/**
* Create and validate a listing onchain. Combines order building with onchain validation.
* Validation costs gas upfront but makes fulfillment cheaper (no signature verification needed).
* @param options
* @param options.asset The asset to trade. tokenAddress and tokenId must be defined.
* @param options.accountAddress Address of the wallet making the listing
* @param options.amount Amount in decimal format (e.g., "1.5" for 1.5 ETH, not wei). Automatically converted to base units.
* @param options.quantity Number of assets to list. Defaults to 1.
* @param options.domain Optional domain for on-chain attribution. Hashed and included in salt.
* @param options.salt Arbitrary salt. Auto-generated if not provided.
* @param options.listingTime When order becomes fulfillable (UTC seconds). Defaults to now.
* @param options.expirationTime Expiration time (UTC seconds).
* @param options.paymentTokenAddress Payment token address. Defaults to ETH.
* @param options.buyerAddress Optional buyer restriction. Only this address can purchase.
* @param options.includeOptionalCreatorFees Include optional creator fees. Default: false.
* @param options.zone Zone for order protection. Defaults to no zone.
* @returns Transaction hash
*
* @throws Error if asset missing token id or accountAddress unavailable.
*/
public async createListingAndValidateOnchain({
asset,
accountAddress,
amount,
quantity = 1,
domain,
salt,
listingTime,
expirationTime,
paymentTokenAddress,
buyerAddress,
includeOptionalCreatorFees = false,
zone,
}: {
asset: AssetWithTokenId;
accountAddress: string;
amount: BigNumberish;
quantity?: BigNumberish;
domain?: string;
salt?: BigNumberish;
listingTime?: number;
expirationTime?: number;
paymentTokenAddress?: string;
buyerAddress?: string;
includeOptionalCreatorFees?: boolean;
zone?: string;
}): Promise<string> {
return this._fulfillmentManager.createListingAndValidateOnchain({
asset,
accountAddress,
amount,
quantity,
domain,
salt,
listingTime,
expirationTime,
paymentTokenAddress,
buyerAddress,
includeOptionalCreatorFees,
zone,
});
}
/**
* Create and validate an offer onchain. Combines order building with onchain validation.
* Validation costs gas upfront but makes fulfillment cheaper (no signature verification needed).
* @param options
* @param options.asset The asset to trade. tokenAddress and tokenId must be defined.
* @param options.accountAddress Address of the wallet making the offer.
* @param options.amount Amount in decimal format (e.g., "1.5" for 1.5 ETH, not wei). Automatically converted to base units.
* @param options.quantity Number of assets to bid for. Defaults to 1.
* @param options.domain Optional domain for on-chain attribution. Hashed and included in salt.
* @param options.salt Arbitrary salt. Auto-generated if not provided.
* @param options.expirationTime Expiration time (UTC seconds).
* @param options.paymentTokenAddress Payment token address. Defaults to WETH.
* @param options.zone Zone for order protection. Defaults to chain's signed zone.
* @returns Transaction hash
*
* @throws Error if asset missing token id or accountAddress unavailable.
*/
public async createOfferAndValidateOnchain({
asset,
accountAddress,
amount,
quantity = 1,
domain,
salt,
expirationTime,
paymentTokenAddress,
zone,
}: {
asset: AssetWithTokenId;
accountAddress: string;
amount: BigNumberish;
quantity?: BigNumberish;
domain?: string;
salt?: BigNumberish;
expirationTime?: BigNumberish;
paymentTokenAddress?: string;
zone?: string;
}): Promise<string> {
return this._fulfillmentManager.createOfferAndValidateOnchain({
asset,
accountAddress,
amount,
quantity,
domain,
salt,
expirationTime,
paymentTokenAddress,
zone,
});
}
/**
* Compute the `basePrice` parameter to be used to price an order.
* Also validates the price and token address.
* @param tokenAddress Address of the ERC-20 token to use for trading. Use the null address for ETH.
* @param amount The value for the order, in the token's main units (e.g. ETH instead of wei)
*/
private async _getPriceParameters(
orderSide: OrderSide,
tokenAddress: string,
amount: BigNumberish,
) {
tokenAddress = tokenAddress.toLowerCase();
const isEther = tokenAddress === ethers.ZeroAddress;
let decimals = 18;
if (!isEther) {
if (tokenAddress in this._cachedPaymentTokenDecimals) {
decimals = this._cachedPaymentTokenDecimals[tokenAddress];
} else {
const paymentToken = await this.api.getPaymentToken(tokenAddress);
this._cachedPaymentTokenDecimals[tokenAddress] = paymentToken.decimals;
decimals = paymentToken.decimals;
}
}
const amountWei = ethers.parseUnits(amount.toString(), decimals);
const basePrice = amountWei;
// Validation
if (amount == null || amountWei < 0) {
throw new Error("Starting price must be a number >= 0");
}
if (isEther && orderSide === OrderSide.OFFER) {
throw new Error("Offers must use wrapped ETH or an ERC-20 token.");
}
return { basePrice };
}
private _dispatch(event: EventType, data: EventData) {
this._emitter.emit(event, data);
}
/** Get the accounts available from the signer or provider. */
private async _getAvailableAccounts() {
const availableAccounts: string[] = [];
try {
if ("address" in this._signerOrProvider) {
availableAccounts.push(this._signerOrProvider.address as string);
} else if ("listAccounts" in this._signerOrProvider) {
const addresses = (await this._signerOrProvider.listAccounts()).map(
(acct) => acct.address,
);
availableAccounts.push(...addresses);
} else if ("getAddress" in this._signerOrProvider) {
availableAccounts.push(await this._signerOrProvider.getAddress());
}
} catch (error) {
// If we can't get accounts (e.g., RPC error), treat as no accounts available
this.logger(
`Failed to get available accounts: ${error instanceof Error ? error.message : error}`,
);
}
return availableAccounts;
}
/**
* Throws an error if an account is not available through the provider.
* @param accountAddress The account address to check is available.
*/
private async _requireAccountIsAvailable(accountAddress: string) {
const accountAddressChecksummed = ethers.getAddress(accountAddress);
const availableAccounts = await this._getAvailableAccounts();
if (availableAccounts.includes(accountAddressChecksummed)) {
return;
}
throw new Error(
`Specified accountAddress is not available through wallet or provider: ${accountAddressChecksummed}. Accounts available: ${
availableAccounts.length > 0 ? availableAccounts.join(", ") : "none"
}`,
);
}
/**
* Wait for a transaction to confirm and log the success or failure.
* @param transactionHash The transaction hash to wait for.
* @param event The event type to log.
* @param description The description of the transaction.
*/
private async _confirmTransaction(
transactionHash: string,
event: EventType,
description: string,
): Promise<void> {
const transactionEventData = { transactionHash, event };
this.logger(`Transaction started: ${description}`);
try {
this._dispatch(EventType.TransactionCreated, transactionEventData);
await this.provider.waitForTransaction(transactionHash);
this.logger(`Transaction succeeded: ${description}`);
this._dispatch(EventType.TransactionConfirmed, transactionEventData);
} catch (error) {
this.logger(`Transaction failed: ${description}`);
this._dispatch(EventType.TransactionFailed, {
...transactionEventData,
error,
});
throw error;
}
}
}