opensea-js
Version:
TypeScript SDK for the OpenSea marketplace helps developers build new experiences using NFTs and our marketplace data
301 lines • 16.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.FulfillmentManager = void 0;
const Seaport_1 = require("@opensea/seaport-js/lib/abi/Seaport");
const ethers_1 = require("ethers");
const privateListings_1 = require("../orders/privateListings");
const types_1 = require("../orders/types");
const utils_1 = require("../orders/utils");
const types_2 = require("../types");
const utils_2 = require("../utils/utils");
const FULFILL_BASIC_ORDER_ALIAS = "fulfillBasicOrder_efficient_6GL6yc";
/**
* Manager for order fulfillment and validation operations.
* Handles fulfilling orders, validating orders onchain, and approving orders.
*/
class FulfillmentManager {
constructor(context, ordersManager) {
this.context = context;
this.ordersManager = ordersManager;
}
/**
* 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);
// Compute ETH value from original order's consideration items
// This handles both standard private listings and zero-payment listings (e.g., rewards)
const value = (0, privateListings_1.computePrivateListingValue)(order.protocolData, order.taker.address);
const seaport = (0, utils_2.getSeaportInstance)(order.protocolAddress, this.context.seaport);
const transaction = await seaport
.matchOrders({
orders: [order.protocolData, counterOrder],
fulfillments,
overrides: {
...overrides,
value,
},
accountAddress,
domain,
})
.transact();
const transactionReceipt = await transaction.wait();
if (!transactionReceipt) {
throw new Error("Missing transaction receipt");
}
await this.context.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.
* 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.
*/
async fulfillOrder({ order, accountAddress, assetContractAddress, tokenId, unitsToFill, recipientAddress, includeOptionalCreatorFees = false, overrides, }) {
await this.context.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 ??
("type" in order &&
[types_1.OrderType.BASIC, types_1.OrderType.ENGLISH].includes(order.type)
? types_2.OrderSide.LISTING
: types_2.OrderSide.OFFER);
const isPrivateListing = "taker" in order ? !!order.taker : false;
if (isPrivateListing) {
return this.fulfillPrivateOrder({
order: order,
accountAddress,
overrides,
});
}
// Get fulfillment data from the API
if (!orderHash) {
throw new Error("Order hash is required to fulfill an order");
}
// Convert unitsToFill to string, defaulting to "1" if not provided
const unitsToFillStr = unitsToFill !== undefined ? unitsToFill.toString() : "1";
const fulfillmentData = await this.context.api.generateFulfillmentData(accountAddress, orderHash, protocolAddress, side, assetContractAddress, tokenId, unitsToFillStr, recipientAddress, includeOptionalCreatorFees);
// Use the transaction data returned by the API
const transaction = fulfillmentData.fulfillment_data.transaction;
const inputData = transaction.input_data;
// Use Seaport ABI to encode the transaction
const seaportInterface = new ethers_1.ethers.Interface(Seaport_1.SeaportABI);
// Extract function name and build parameters array in correct order
const rawFunctionName = transaction.function.split("(")[0];
const functionName = rawFunctionName === FULFILL_BASIC_ORDER_ALIAS
? "fulfillBasicOrder"
: rawFunctionName;
let params;
// Order parameters based on the function being called
if (functionName === "fulfillAdvancedOrder" &&
"advancedOrder" in inputData) {
params = [
inputData.advancedOrder,
inputData.criteriaResolvers || [],
inputData.fulfillerConduitKey ||
"0x0000000000000000000000000000000000000000000000000000000000000000",
inputData.recipient,
];
}
else if ((functionName === "fulfillBasicOrder" ||
rawFunctionName === FULFILL_BASIC_ORDER_ALIAS) &&
"basicOrderParameters" in inputData) {
params = [inputData.basicOrderParameters];
}
else if (functionName === "fulfillOrder" && "order" in inputData) {
params = [
inputData.order,
inputData.fulfillerConduitKey ||
"0x0000000000000000000000000000000000000000000000000000000000000000",
inputData.recipient,
];
}
else {
// Fallback: try to use values in object order
params = Object.values(inputData);
}
const encodedData = seaportInterface.encodeFunctionData(functionName, params);
// Send the transaction using the signer from context
const signer = this.context.signerOrProvider;
const tx = await signer.sendTransaction({
to: transaction.to,
value: transaction.value,
data: encodedData,
...overrides,
});
await this.context.confirmTransaction(tx.hash, types_2.EventType.MatchOrders, "Fulfilling order");
return tx.hash;
}
/**
* 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 = (0, utils_2.getSeaportInstance)(order.protocolAddress, this.context.seaport);
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;
}
}
/**
* 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.context.requireAccountIsAvailable(order.maker.address);
(0, utils_2.requireValidProtocol)(order.protocolAddress);
this.context.dispatch(types_2.EventType.ApproveOrder, {
orderV2: order,
accountAddress: order.maker.address,
});
const seaport = (0, utils_2.getSeaportInstance)(order.protocolAddress, this.context.seaport);
const transaction = await seaport
.validate([order.protocolData], order.maker.address, domain)
.transact();
await this.context.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.context.requireAccountIsAvailable(accountAddress);
this.context.dispatch(types_2.EventType.ApproveOrder, {
orderV2: { protocolData: orderComponents },
accountAddress,
});
const seaport = (0, utils_2.getSeaportInstance)(utils_1.DEFAULT_SEAPORT_CONTRACT_ADDRESS, this.context.seaport);
const transaction = await seaport
.validate([{ parameters: orderComponents, signature: "0x" }], accountAddress)
.transact();
await this.context.confirmTransaction(transaction.hash, types_2.EventType.ApproveOrder, "Validating order onchain");
return transaction.hash;
}
/**
* 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.
*/
async createListingAndValidateOnchain({ asset, accountAddress, amount, quantity = 1, domain, salt, listingTime, expirationTime, paymentTokenAddress, buyerAddress, includeOptionalCreatorFees = false, zone, }) {
const orderComponents = await this.ordersManager.buildListingOrderComponents({
asset,
accountAddress,
amount,
quantity,
domain,
salt,
listingTime,
expirationTime,
paymentTokenAddress,
buyerAddress,
includeOptionalCreatorFees,
zone,
});
return this.validateOrderOnchain(orderComponents, accountAddress);
}
/**
* 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.
*/
async createOfferAndValidateOnchain({ asset, accountAddress, amount, quantity = 1, domain, salt, expirationTime, paymentTokenAddress, zone, }) {
const orderComponents = await this.ordersManager.buildOfferOrderComponents({
asset,
accountAddress,
amount,
quantity,
domain,
salt,
expirationTime,
paymentTokenAddress,
zone,
});
return this.validateOrderOnchain(orderComponents, accountAddress);
}
}
exports.FulfillmentManager = FulfillmentManager;
//# sourceMappingURL=fulfillment.js.map