opensea-js
Version:
TypeScript SDK for the OpenSea marketplace helps developers build new experiences using NFTs and our marketplace data
954 lines • 51.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenSeaSDK = void 0;
const EventEmitter = require("events");
const seaport_js_1 = require("@opensea/seaport-js");
const constants_1 = require("@opensea/seaport-js/lib/constants");
const ethers_1 = require("ethers");
const api_1 = require("./api/api");
const constants_2 = require("./constants");
const privateListings_1 = require("./orders/privateListings");
const types_1 = require("./orders/types");
const utils_1 = require("./orders/utils");
const contracts_1 = require("./typechain/contracts");
const types_2 = require("./types");
const utils_2 = require("./utils/utils");
/**
* The OpenSea SDK main class.
* @category Main Classes
*/
class OpenSeaSDK {
/**
* 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, apiConfig = {}, logger) {
/** Internal cache of decimals for payment tokens to save network requests */
this._cachedPaymentTokenDecimals = {};
this.getAmountWithBasisPointsApplied = (amount, basisPoints) => {
return ((amount * basisPoints) / constants_2.INVERSE_BASIS_POINT).toString();
};
// API config
apiConfig.chain ?? (apiConfig.chain = types_2.Chain.Mainnet);
this.chain = apiConfig.chain;
this.api = new api_1.OpenSeaAPI(apiConfig);
this.provider = (signerOrProvider.provider ??
signerOrProvider);
this._signerOrProvider = signerOrProvider ?? this.provider;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.seaport_v1_6 = new seaport_js_1.Seaport(this._signerOrProvider, {
conduitKeyToConduit: {
[constants_2.OPENSEA_CONDUIT_KEY_2]: constants_2.OPENSEA_CONDUIT_ADDRESS_2,
},
overrides: { defaultConduitKey: (0, utils_2.getDefaultConduitKey)(this.chain) },
});
// Emit events
this._emitter = new EventEmitter();
// Logger: default to no logging if fn not provided
this.logger = logger ?? ((arg) => arg);
// Cache decimals for offer and listing payment tokens to skip network request
const offerPaymentToken = (0, utils_2.getOfferPaymentToken)(this.chain).toLowerCase();
const listingPaymentToken = (0, utils_2.getListingPaymentToken)(this.chain).toLowerCase();
this._cachedPaymentTokenDecimals[offerPaymentToken] = 18;
this._cachedPaymentTokenDecimals[listingPaymentToken] = 18;
}
/**
* 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.
*/
addListener(event, listener, 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
*/
removeListener(event, listener) {
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
*/
removeAllListeners(event) {
this._emitter.removeAllListeners(event);
}
/**
* Get the appropriate token address for wrap/unwrap operations.
* For Polygon, use WPOL. For other chains, use getOfferPaymentToken,
* which is the wrapped native asset for the chain.
* @param chain The chain to get the token address for
* @returns The token address for wrap/unwrap operations
*/
getNativeWrapTokenAddress(chain) {
switch (chain) {
case types_2.Chain.Polygon:
return constants_2.WPOL_ADDRESS;
default:
return (0, utils_2.getOfferPaymentToken)(chain);
}
}
/**
* 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
*/
async wrapEth({ amountInEth, accountAddress, }) {
await this._requireAccountIsAvailable(accountAddress);
const value = (0, ethers_1.parseEther)(amountInEth.toString());
this._dispatch(types_2.EventType.WrapEth, { accountAddress, amount: value });
const wethContract = new ethers_1.Contract(this.getNativeWrapTokenAddress(this.chain), ["function deposit() payable"], this._signerOrProvider);
try {
const transaction = await wethContract.deposit({ value });
await this._confirmTransaction(transaction.hash, types_2.EventType.WrapEth, "Wrapping native asset");
}
catch (error) {
console.error(error);
this._dispatch(types_2.EventType.TransactionDenied, { error, 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
*/
async unwrapWeth({ amountInEth, accountAddress, }) {
await this._requireAccountIsAvailable(accountAddress);
const amount = (0, ethers_1.parseEther)(amountInEth.toString());
this._dispatch(types_2.EventType.UnwrapWeth, { accountAddress, amount });
const wethContract = new ethers_1.Contract(this.getNativeWrapTokenAddress(this.chain), ["function withdraw(uint wad) public"], this._signerOrProvider);
try {
const transaction = await wethContract.withdraw(amount);
await this._confirmTransaction(transaction.hash, types_2.EventType.UnwrapWeth, "Unwrapping wrapped native asset");
}
catch (error) {
console.error(error);
this._dispatch(types_2.EventType.TransactionDenied, { error, accountAddress });
}
}
/**
* Build listing order without submitting to API
* @param options Listing parameters
* @returns OrderWithCounter ready for API submission or onchain validation
*/
async _buildListingOrder({ asset, accountAddress, startAmount, endAmount, quantity = 1, domain, salt, listingTime, expirationTime, paymentTokenAddress = (0, utils_2.getListingPaymentToken)(this.chain), buyerAddress, englishAuction, excludeOptionalCreatorFees = false, zone = ethers_1.ZeroAddress, }) {
await this._requireAccountIsAvailable(accountAddress);
const { nft } = await this.api.getNFT(asset.tokenAddress, asset.tokenId);
const offerAssetItems = this.getNFTItems([nft], [BigInt(quantity ?? 1)]);
if (englishAuction) {
throw new Error("English auctions are no longer supported on OpenSea");
}
if (englishAuction && paymentTokenAddress == ethers_1.ethers.ZeroAddress) {
throw new Error(`English auctions must use wrapped ETH or an ERC-20 token.`);
}
const { basePrice, endPrice } = await this._getPriceParameters(types_2.OrderSide.LISTING, paymentTokenAddress, expirationTime ?? (0, utils_2.getMaxOrderExpirationTimestamp)(), startAmount, endAmount ?? undefined);
const collection = await this.api.getCollection(nft.collection);
const considerationFeeItems = await this.getFees({
collection,
seller: accountAddress,
paymentTokenAddress,
startAmount: basePrice,
endAmount: endPrice,
excludeOptionalCreatorFees,
isPrivateListing: !!buyerAddress,
});
if (buyerAddress) {
considerationFeeItems.push(...(0, privateListings_1.getPrivateListingConsiderations)(offerAssetItems, buyerAddress));
}
if (englishAuction) {
zone = constants_2.ENGLISH_AUCTION_ZONE_MAINNETS;
}
else if (collection.requiredZone) {
zone = collection.requiredZone;
}
const { executeAllActions } = await this.seaport_v1_6.createOrder({
offer: offerAssetItems,
consideration: considerationFeeItems,
startTime: listingTime?.toString(),
endTime: expirationTime?.toString() ??
(0, utils_2.getMaxOrderExpirationTimestamp)().toString(),
zone,
domain,
salt: BigInt(salt ?? 0).toString(),
restrictedByZone: zone !== ethers_1.ZeroAddress,
allowPartialFills: englishAuction ? false : true,
}, accountAddress);
return executeAllActions();
}
/**
* Build listing order components without submitting to API
* @param options Listing parameters
* @returns OrderComponents ready for onchain validation
*/
async _buildListingOrderComponents({ asset, accountAddress, startAmount, endAmount, quantity = 1, domain, salt, listingTime, expirationTime, paymentTokenAddress = (0, utils_2.getListingPaymentToken)(this.chain), buyerAddress, englishAuction, excludeOptionalCreatorFees = false, zone = ethers_1.ZeroAddress, }) {
const order = await this._buildListingOrder({
asset,
accountAddress,
startAmount,
endAmount,
quantity,
domain,
salt,
listingTime,
expirationTime,
paymentTokenAddress,
buyerAddress,
englishAuction,
excludeOptionalCreatorFees,
zone,
});
return order.parameters;
}
/**
* Build offer order without submitting to API
* @param options Offer parameters
* @returns OrderWithCounter ready for API submission or onchain validation
*/
async _buildOfferOrder({ asset, accountAddress, startAmount, quantity = 1, domain, salt, expirationTime, paymentTokenAddress = (0, utils_2.getOfferPaymentToken)(this.chain), excludeOptionalCreatorFees = true, zone = (0, utils_2.getSignedZone)(this.chain), }) {
await this._requireAccountIsAvailable(accountAddress);
const { nft } = await this.api.getNFT(asset.tokenAddress, asset.tokenId);
const considerationAssetItems = this.getNFTItems([nft], [BigInt(quantity ?? 1)]);
const { basePrice } = await this._getPriceParameters(types_2.OrderSide.OFFER, paymentTokenAddress, expirationTime ?? (0, utils_2.getMaxOrderExpirationTimestamp)(), startAmount);
const collection = await this.api.getCollection(nft.collection);
const considerationFeeItems = await this.getFees({
collection,
paymentTokenAddress,
startAmount: basePrice,
excludeOptionalCreatorFees,
});
if (collection.requiredZone) {
zone = collection.requiredZone;
}
const { executeAllActions } = await this.seaport_v1_6.createOrder({
offer: [
{
token: paymentTokenAddress,
amount: basePrice.toString(),
},
],
consideration: [...considerationAssetItems, ...considerationFeeItems],
endTime: expirationTime !== undefined
? BigInt(expirationTime).toString()
: (0, utils_2.getMaxOrderExpirationTimestamp)().toString(),
zone,
domain,
salt: BigInt(salt ?? 0).toString(),
restrictedByZone: zone !== ethers_1.ZeroAddress,
allowPartialFills: true,
}, accountAddress);
return executeAllActions();
}
/**
* Build offer order components without submitting to API
* @param options Offer parameters
* @returns OrderComponents ready for onchain validation
*/
async _buildOfferOrderComponents({ asset, accountAddress, startAmount, quantity = 1, domain, salt, expirationTime, paymentTokenAddress = (0, utils_2.getOfferPaymentToken)(this.chain), excludeOptionalCreatorFees = true, zone = (0, utils_2.getSignedZone)(this.chain), }) {
const order = await this._buildOfferOrder({
asset,
accountAddress,
startAmount,
quantity,
domain,
salt,
expirationTime,
paymentTokenAddress,
excludeOptionalCreatorFees,
zone,
});
return order.parameters;
}
async getFees({ collection, seller, paymentTokenAddress, startAmount, endAmount, excludeOptionalCreatorFees, isPrivateListing = false, }) {
let collectionFees = collection.fees;
if (excludeOptionalCreatorFees) {
collectionFees = collectionFees.filter((fee) => fee.required);
}
if (isPrivateListing) {
collectionFees = collectionFees.filter((fee) => this.isNotMarketplaceFee(fee));
}
const collectionFeesBasisPoints = (0, utils_2.totalBasisPointsForFees)(collectionFees);
const sellerBasisPoints = constants_2.INVERSE_BASIS_POINT - collectionFeesBasisPoints;
const getConsiderationItem = (basisPoints, recipient) => {
return {
token: paymentTokenAddress,
amount: this.getAmountWithBasisPointsApplied(startAmount, basisPoints),
endAmount: this.getAmountWithBasisPointsApplied(endAmount ?? startAmount, basisPoints),
recipient,
};
};
const considerationItems = [];
if (seller) {
considerationItems.push(getConsiderationItem(sellerBasisPoints, seller));
}
if (collectionFeesBasisPoints > 0) {
for (const fee of collectionFees) {
considerationItems.push(getConsiderationItem((0, utils_2.basisPointsForFee)(fee), fee.recipient));
}
}
return considerationItems;
}
isNotMarketplaceFee(fee) {
return (fee.recipient.toLowerCase() !== (0, utils_2.getFeeRecipient)(this.chain).toLowerCase());
}
getNFTItems(nfts, quantities = []) {
return nfts.map((nft, index) => ({
itemType: (0, utils_2.getAssetItemType)(nft.token_standard.toUpperCase()),
token: (0, utils_2.getAddressAfterRemappingSharedStorefrontAddressToLazyMintAdapterAddress)(nft.contract),
identifier: nft.identifier ?? undefined,
amount: quantities[index].toString() ?? "1",
}));
}
/**
* 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.startAmount Value of the offer in units, not base units e.g. not wei, of the payment token (or WETH if no payment token address specified)
* @param options.quantity The number of assets to bid for (if fungible or semi-fungible). Defaults to 1.
* @param options.domain An optional domain to be hashed and included in the first four bytes of the random salt.
* @param options.salt Arbitrary salt. If not passed in, a random salt will be generated with the first four bytes being the domain hash or empty.
* @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.excludeOptionalCreatorFees If true, optional creator fees will be excluded from the offer. Default: true.
* @param options.zone The zone to use for the order. If unspecified, defaults to the chain's signed zone for order protection.
*
* @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 startAmount is not greater than 0.
* @throws Error if paymentTokenAddress is not WETH on anything other than Ethereum mainnet.
*/
async createOffer({ asset, accountAddress, startAmount, quantity = 1, domain, salt, expirationTime, paymentTokenAddress = (0, utils_2.getOfferPaymentToken)(this.chain), excludeOptionalCreatorFees = true, zone = (0, utils_2.getSignedZone)(this.chain), }) {
const order = await this._buildOfferOrder({
asset,
accountAddress,
startAmount,
quantity,
domain,
salt,
expirationTime,
paymentTokenAddress,
excludeOptionalCreatorFees,
zone,
});
return this.api.postOrder(order, {
protocol: "seaport",
protocolAddress: utils_1.DEFAULT_SEAPORT_CONTRACT_ADDRESS,
side: types_2.OrderSide.OFFER,
});
}
/**
* 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.startAmount Value of the listing at the start of the auction in units, not base units e.g. not wei, of the payment token (or WETH if no payment token address specified)
* @param options.endAmount Value of the listing at the end of the auction. If specified, price will change linearly between startAmount and endAmount as time progresses.
* @param options.quantity The number of assets to list (if fungible or semi-fungible). Defaults to 1.
* @param options.domain An optional domain to be hashed and included in the first four bytes of the random salt. This can be used for on-chain order attribution to assist with analytics.
* @param options.salt Arbitrary salt. If not passed in, a random salt will be generated with the first four bytes being the domain hash or empty.
* @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.englishAuction If true, the order will be listed as an English auction.
* @param options.excludeOptionalCreatorFees If true, optional creator fees will be excluded from the listing. Default: false.
* @param options.zone The zone to use for the order. For order protection, pass SIGNED_ZONE. If unspecified, 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 startAmount is not greater than 0.
* @throws Error if paymentTokenAddress is not WETH on anything other than Ethereum mainnet.
*/
async createListing({ asset, accountAddress, startAmount, endAmount, quantity = 1, domain, salt, listingTime, expirationTime, paymentTokenAddress = (0, utils_2.getListingPaymentToken)(this.chain), buyerAddress, englishAuction, excludeOptionalCreatorFees = false, zone = ethers_1.ZeroAddress, }) {
const order = await this._buildListingOrder({
asset,
accountAddress,
startAmount,
endAmount,
quantity,
domain,
salt,
listingTime,
expirationTime,
paymentTokenAddress,
buyerAddress,
englishAuction,
excludeOptionalCreatorFees,
zone,
});
return this.api.postOrder(order, {
protocol: "seaport",
protocolAddress: utils_1.DEFAULT_SEAPORT_CONTRACT_ADDRESS,
side: types_2.OrderSide.LISTING,
});
}
/**
* 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 Value of the offer in units, not base units e.g. not wei, of the payment token (or WETH if no payment token address specified).
* @param options.quantity The number of assets to bid for (if fungible or semi-fungible).
* @param options.domain An optional domain to be hashed and included in the first four bytes of the random salt. This can be used for on-chain order attribution to assist with analytics.
* @param options.salt Arbitrary salt. If not passed in, a random salt will be generated with the first four bytes being the domain hash or empty.
* @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.excludeOptionalCreatorFees If true, optional creator fees will be excluded from the offer. Default: false.
* @param options.offerProtectionEnabled Build the offer on OpenSea's signed zone to provide offer protections from receiving an item which is disabled from trading.
* @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.
* @returns The {@link CollectionOffer} that was created.
*/
async createCollectionOffer({ collectionSlug, accountAddress, amount, quantity, domain, salt, expirationTime, paymentTokenAddress = (0, utils_2.getOfferPaymentToken)(this.chain), excludeOptionalCreatorFees = false, offerProtectionEnabled = true, traitType, traitValue, }) {
await this._requireAccountIsAvailable(accountAddress);
const collection = await this.api.getCollection(collectionSlug);
const buildOfferResult = await this.api.buildOffer(accountAddress, quantity, collectionSlug, offerProtectionEnabled, traitType, traitValue);
const item = buildOfferResult.partialParameters.consideration[0];
const convertedConsiderationItem = {
itemType: item.itemType,
token: item.token,
identifier: item.identifierOrCriteria,
amount: item.startAmount,
};
const { basePrice } = await this._getPriceParameters(types_2.OrderSide.LISTING, paymentTokenAddress, expirationTime ?? (0, utils_2.getMaxOrderExpirationTimestamp)(), amount);
const considerationFeeItems = await this.getFees({
collection,
paymentTokenAddress,
startAmount: basePrice,
endAmount: basePrice,
excludeOptionalCreatorFees,
});
const considerationItems = [
convertedConsiderationItem,
...considerationFeeItems,
];
const payload = {
offerer: accountAddress,
offer: [
{
token: paymentTokenAddress,
amount: basePrice.toString(),
},
],
consideration: considerationItems,
endTime: expirationTime?.toString() ??
(0, utils_2.getMaxOrderExpirationTimestamp)().toString(),
zone: buildOfferResult.partialParameters.zone,
domain,
salt: BigInt(salt ?? 0).toString(),
restrictedByZone: true,
allowPartialFills: true,
};
const { executeAllActions } = await this.seaport_v1_6.createOrder(payload, accountAddress);
const order = await executeAllActions();
return this.api.postCollectionOffer(order, collectionSlug, traitType, traitValue);
}
/**
* Fulfill a private order for a designated address.
* @param options
* @param options.order The order to fulfill
* @param options.accountAddress Address of the wallet taking the order.
* @param options.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.
* @param options.overrides Transaction overrides, ignored if not set.
* @returns Transaction hash of the order.
*/
async fulfillPrivateOrder({ order, accountAddress, domain, overrides, }) {
if (!order.taker?.address) {
throw new Error("Order is not a private listing - must have a taker address");
}
const counterOrder = (0, privateListings_1.constructPrivateListingCounterOrder)(order.protocolData, order.taker.address);
const fulfillments = (0, privateListings_1.getPrivateListingFulfillments)(order.protocolData);
const seaport = this.getSeaport(order.protocolAddress);
const transaction = await seaport
.matchOrders({
orders: [order.protocolData, counterOrder],
fulfillments,
overrides: {
...overrides,
value: counterOrder.parameters.offer[0].startAmount,
},
accountAddress,
domain,
})
.transact();
const transactionReceipt = await transaction.wait();
if (!transactionReceipt) {
throw new Error("Missing transaction receipt");
}
await this._confirmTransaction(transactionReceipt.hash, types_2.EventType.MatchOrders, "Fulfilling order");
return transactionReceipt.hash;
}
/**
* Fulfill an order for an asset. The order can be either a listing or an offer.
* @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.recipientAddress The optional address to receive the order's item(s) or currencies. If not specified, defaults to accountAddress.
* @param options.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.
* @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 attempting to fulfill the order with a recipient address which does not match a private listing.
*/
async fulfillOrder({ order, accountAddress, recipientAddress, domain, overrides, }) {
await this._requireAccountIsAvailable(accountAddress);
const protocolAddress = order.protocolAddress ?? order.protocol_address;
(0, utils_2.requireValidProtocol)(protocolAddress);
const orderHash = order.orderHash ?? order.order_hash;
const side = order.side ??
([types_1.OrderType.BASIC, types_1.OrderType.ENGLISH].includes(order.type)
? types_2.OrderSide.LISTING
: types_2.OrderSide.OFFER);
let extraData = undefined;
const protocolData = order.protocolData ?? order.protocol_data;
if (orderHash) {
const result = await this.api.generateFulfillmentData(accountAddress, orderHash, protocolAddress, side);
// If the order is using offer protection, the extraData
// must be included with the order to successfully fulfill.
const inputData = result.fulfillment_data.transaction.input_data;
if ("orders" in inputData && "extraData" in inputData.orders[0]) {
extraData = inputData.orders[0].extraData;
}
const signature = result.fulfillment_data.orders[0].signature;
protocolData.signature = signature;
}
const isPrivateListing = "taker" in order ? !!order.taker : false;
if (isPrivateListing) {
if (recipientAddress) {
throw new Error("Private listings cannot be fulfilled with a recipient address");
}
return this.fulfillPrivateOrder({
order: order,
accountAddress,
domain,
overrides,
});
}
const seaport = this.getSeaport(protocolAddress);
const { executeAllActions } = await seaport.fulfillOrder({
order: protocolData,
accountAddress,
recipientAddress,
extraData,
domain,
overrides,
});
const transaction = await executeAllActions();
const transactionHash = ethers_1.ethers.Transaction.from(transaction).hash;
if (!transactionHash) {
throw new Error("Missing transaction hash");
}
await this._confirmTransaction(transactionHash, types_2.EventType.MatchOrders, "Fulfilling order");
return transactionHash;
}
/**
* Utility function to get the Seaport client based on the address.
* @param protocolAddress The Seaport address.
*/
getSeaport(protocolAddress) {
const checksummedProtocolAddress = ethers_1.ethers.getAddress(protocolAddress);
switch (checksummedProtocolAddress) {
case constants_1.CROSS_CHAIN_SEAPORT_V1_6_ADDRESS:
case constants_2.GUNZILLA_SEAPORT_1_6_ADDRESS:
return this.seaport_v1_6;
default:
throw new Error(`Unsupported protocol address: ${protocolAddress}`);
}
}
/**
* Cancel orders onchain, preventing them from being fulfilled.
* @param options
* @param options.orders The orders to cancel
* @param options.accountAddress The account address cancelling the orders.
* @param options.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.
* @param options.overrides Transaction overrides, ignored if not set.
* @returns Transaction hash of the order.
*/
async cancelSeaportOrders({ orders, accountAddress, domain, protocolAddress = utils_1.DEFAULT_SEAPORT_CONTRACT_ADDRESS, overrides, }) {
const seaport = this.getSeaport(protocolAddress);
const transaction = await seaport
.cancelOrders(orders, accountAddress, domain, overrides)
.transact();
return transaction.hash;
}
/**
* Cancel an order onchain, preventing it from ever being fulfilled.
* @param options
* @param options.order The order to cancel
* @param options.accountAddress The account address that will be cancelling the order.
* @param options.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.
*
* @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}.
*/
async cancelOrder({ order, accountAddress, domain, }) {
await this._requireAccountIsAvailable(accountAddress);
(0, utils_2.requireValidProtocol)(order.protocolAddress);
this._dispatch(types_2.EventType.CancelOrder, { orderV2: order, accountAddress });
// Transact and get the transaction hash
const transactionHash = await this.cancelSeaportOrders({
orders: [order.protocolData.parameters],
accountAddress,
domain,
protocolAddress: order.protocolAddress,
});
// Await transaction confirmation
await this._confirmTransaction(transactionHash, types_2.EventType.CancelOrder, "Cancelling order");
}
_getSeaportVersion(protocolAddress) {
const protocolAddressChecksummed = ethers_1.ethers.getAddress(protocolAddress);
switch (protocolAddressChecksummed) {
case constants_1.CROSS_CHAIN_SEAPORT_V1_6_ADDRESS:
case constants_2.GUNZILLA_SEAPORT_1_6_ADDRESS:
return "1.6";
default:
throw new Error("Unknown or unsupported protocol address");
}
}
/**
* Get the offerer signature for canceling an order offchain.
* The signature will only be valid if the signer address is the address of the order's offerer.
*/
async _getOffererSignature(protocolAddress, orderHash, chain) {
const chainId = (0, utils_2.getChainId)(chain);
const name = "Seaport";
const version = this._getSeaportVersion(protocolAddress);
if (typeof this._signerOrProvider.signTypedData == "undefined") {
throw new Error("Please pass an ethers Signer into this sdk to derive an offerer signature");
}
return this._signerOrProvider.signTypedData({ chainId, name, version, verifyingContract: protocolAddress }, { OrderHash: [{ name: "orderHash", type: "bytes32" }] }, { orderHash });
}
/**
* 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 user associated with the API Key will be checked instead.
* 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.
*/
async offchainCancelOrder(protocolAddress, orderHash, chain = this.chain, offererSignature, useSignerToDeriveOffererSignature) {
if (useSignerToDeriveOffererSignature) {
offererSignature = await this._getOffererSignature(protocolAddress, orderHash, chain);
}
return this.api.offchainCancelOrder(protocolAddress, orderHash, chain, offererSignature);
}
/**
* 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}.
*/
async isOrderFulfillable({ order, accountAddress, }) {
(0, utils_2.requireValidProtocol)(order.protocolAddress);
const seaport = this.getSeaport(order.protocolAddress);
try {
const isValid = await seaport
.validate([order.protocolData], accountAddress)
.staticCall();
return !!isValid;
}
catch (error) {
if ((0, utils_2.hasErrorCode)(error) && error.code === "CALL_EXCEPTION") {
return false;
}
throw error;
}
}
/**
* 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.
*/
async getBalance({ accountAddress, asset, }) {
switch (asset.tokenStandard) {
case types_2.TokenStandard.ERC20: {
const contract = contracts_1.ERC20__factory.connect(asset.tokenAddress, this.provider);
return await contract.balanceOf.staticCall(accountAddress);
}
case types_2.TokenStandard.ERC1155: {
if (asset.tokenId === undefined || asset.tokenId === null) {
throw new Error("Missing ERC1155 tokenId for getBalance");
}
const contract = contracts_1.ERC1155__factory.connect(asset.tokenAddress, this.provider);
return await contract.balanceOf.staticCall(accountAddress, asset.tokenId);
}
case types_2.TokenStandard.ERC721: {
if (asset.tokenId === undefined || asset.tokenId === null) {
throw new Error("Missing ERC721 tokenId for getBalance");
}
const contract = contracts_1.ERC721__factory.connect(asset.tokenAddress, this.provider);
try {
const owner = await contract.ownerOf.staticCall(asset.tokenId);
return BigInt(owner.toLowerCase() == accountAddress.toLowerCase());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
this.logger(`Failed to get ownerOf ERC721: ${error.message ?? error}`);
return 0n;
}
}
default:
throw new Error("Unsupported token standard for getBalance");
}
}
/**
* 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.
*/
async transfer({ asset, amount, fromAddress, toAddress, overrides, }) {
await this._requireAccountIsAvailable(fromAddress);
overrides = { ...overrides, from: fromAddress };
let transaction;
switch (asset.tokenStandard) {
case types_2.TokenStandard.ERC20: {
if (!amount) {
throw new Error("Missing ERC20 amount for transfer");
}
const contract = contracts_1.ERC20__factory.connect(asset.tokenAddress, this._signerOrProvider);
transaction = contract.transfer(toAddress, amount, overrides);
break;
}
case types_2.TokenStandard.ERC1155: {
if (asset.tokenId === undefined || asset.tokenId === null) {
throw new Error("Missing ERC1155 tokenId for transfer");
}
if (!amount) {
throw new Error("Missing ERC1155 amount for transfer");
}
const contract = contracts_1.ERC1155__factory.connect(asset.tokenAddress, this._signerOrProvider);
transaction = contract.safeTransferFrom(fromAddress, toAddress, asset.tokenId, amount, "", overrides);
break;
}
case types_2.TokenStandard.ERC721: {
if (asset.tokenId === undefined || asset.tokenId === null) {
throw new Error("Missing ERC721 tokenId for transfer");
}
const contract = contracts_1.ERC721__factory.connect(asset.tokenAddress, this._signerOrProvider);
transaction = contract.transferFrom(fromAddress, toAddress, asset.tokenId, overrides);
break;
}
default:
throw new Error("Unsupported token standard for transfer");
}
try {
const transactionResponse = await transaction;
await this._confirmTransaction(transactionResponse.hash, types_2.EventType.Transfer, "Transferring asset");
}
catch (error) {
console.error(error);
this._dispatch(types_2.EventType.TransactionDenied, {
error,
accountAddress: fromAddress,
});
}
}
/**
* 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}.
*/
async approveOrder(order, domain) {
await this._requireAccountIsAvailable(order.maker.address);
(0, utils_2.requireValidProtocol)(order.protocolAddress);
this._dispatch(types_2.EventType.ApproveOrder, {
orderV2: order,
accountAddress: order.maker.address,
});
const seaport = this.getSeaport(order.protocolAddress);
const transaction = await seaport
.validate([order.protocolData], order.maker.address, domain)
.transact();
await this._confirmTransaction(transaction.hash, types_2.EventType.ApproveOrder, "Approving order");
return transaction.hash;
}
/**
* 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.
*/
async validateOrderOnchain(orderComponents, accountAddress) {
await this._requireAccountIsAvailable(accountAddress);
this._dispatch(types_2.EventType.ApproveOrder, {
orderV2: { protocolData: orderComponents },
accountAddress,
});
const seaport = this.getSeaport(utils_1.DEFAULT_SEAPORT_CONTRACT_ADDRESS);
const transaction = await seaport
.validate([{ parameters: orderComponents, signature: "0x" }], accountAddress)
.transact();
await this._confirmTransaction(transaction.hash, types_2.EventType.ApproveOrder, "Validating order onchain");
return transaction.hash;
}
/**
* Create and validate a listing onchain using Seaport's validate() method. This combines
* order building with onchain validation in a single call.
* @param options Listing parameters
* @returns Transaction hash of the validation transaction
*/
async createListingAndValidateOnchain({ asset, accountAddress, startAmount, endAmount, quantity = 1, domain, salt, listingTime, expirationTime, paymentTokenAddress, buyerAddress, englishAuction, excludeOptionalCreatorFees = false, zone, }) {
const orderComponents = await this._buildListingOrderComponents({
asset,
accountAddress,
startAmount,
endAmount,
quantity,
domain,
salt,
listingTime,
expirationTime,
paymentTokenAddress,
buyerAddress,
englishAuction,
excludeOptionalCreatorFees,
zone,
});
return this.validateOrderOnchain(orderComponents, accountAddress);
}
/**
* Create and validate an offer onchain using Seaport's validate() method. This combines
* order building with onchain validation in a single call.
* @param options Offer parameters
* @returns Transaction hash of the validation transaction
*/
async createOfferAndValidateOnchain({ asset, accountAddress, startAmount, quantity = 1, domain, salt, expirationTime, paymentTokenAddress, excludeOptionalCreatorFees = true, zone, }) {
const orderComponents = await this._buildOfferOrderComponents({
asset,
accountAddress,
startAmount,
quantity,
domain,
salt,
expirationTime,
paymentTokenAddress,
excludeOptionalCreatorFees,
zone,
});
return this.validateOrderOnchain(orderComponents, accountAddress);
}
/**
* Compute the `basePrice` and `endPrice` parameters to be used to price an order.
* Also validates the expiration time and auction type.
* @param tokenAddress Address of the ERC-20 token to use for trading. Use the null address for ETH.
* @param expirationTime When the auction expires, or 0 if never.
* @param startAmount The base value for the order, in the token's main units (e.g. ETH instead of wei)
* @param endAmount The end value for the order, in the token's main units (e.g. ETH instead of wei)
*/
async _getPriceParameters(orderSide, tokenAddress, expirationTime, startAmount, endAmount) {
tokenAddress = tokenAddress.toLowerCase();
const isEther = tokenAddress === ethers_1.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 startAmountWei = ethers_1.ethers.parseUnits(startAmount.toString(), decimals);
const endAmountWei = endAmount
? ethers_1.ethers.parseUnits(endAmount.toString(), decimals)
: undefined;
const priceDiffWei = endAmountWei !== undefined ? startAmountWei - endAmountWei : 0n;
const basePrice = startAmountWei;
const endPrice = endAmountWei;
// Validation
if (startAmount == null || startAmountWei < 0) {
throw new Error("Starting price must be a number >= 0");
}
if (isEther && orderSide === types_2.OrderSide.OFFER) {
throw new Error("Offers must use wrapped ETH or an ERC-20 token.");
}
if (priceDiffWei < 0) {
throw new Error("End price must be less than or equal to the start price.");
}
if (priceDiffWei > 0 && BigInt(expirationTime) === 0n) {
throw new Error("Expiration time must be set if order will change in price.");
}
return { basePrice, endPrice };
}
_dispatch(event, data) {
this._emitter.emit(event, data);
}
/** Get the accounts available from the signer or provider. */
async _getAvailableAccounts() {
const availableAccounts = [];
if ("address" in this._signerOrProvider) {
availableAccounts.push(this._signerOrProvider.address);
}
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());
}
return availableAccounts;
}
/**
* Throws an error if an account is not available through the provider.
* @param accountAddress The account address to check is available.
*/
async _requireAccountIsAvailable(accountAddress) {
const accountAddressChecksummed = ethers_1.ethers.getAddress(accountAddress);
const availableAccounts = await this._getAvailableAccounts();
if (availableAccounts.includes(accountAddressChecksumme