Skip to main content

Fill bulk orders

This page demonstrates the capability to process multiple orders within a single transaction. This functionality is particularly beneficial for marketplaces that necessitate a shopping cart system, enabling buyers to select various assets and finalize their purchase through a singular confirmation click.


💡Listing vs Order

Immutable provides two distinct types of orders:

  1. Listings: These are orders initiated by an NFT owner who intends to sell their asset on a marketplace. Listings are considered sell orders.
  2. Bids: Representing a prospective buyer's intention to acquire an asset, bids allow users to express their interest in purchasing a specific asset. Users can place a bid on the order book, anticipating a match with a seller. Bids are considered buy orders.
It is important to note that while bids are not currently available on the zkEVM platform. However, their integration is a key part of Immutable's future development roadmap.

Marketplaces aiming to enhance user experience with a shopping cart feature can now enable their users to add multiple assets to a cart for bulk purchasing. This approach significantly streamlines the transaction process by treating the collective purchase as a single transaction. Such consolidation offers dual benefits: it reduces the number of confirmations required by the user and potentially lowers the overall gas costs associated with individual transactions.

However, as asset purchases are not immediate, market changes may affect availability at the time of finalizing the bulk order. To address this, Immutable operates on a 'best efforts' basis, proceeding with the transaction for available items even if some in the cart are no longer accessible.

If only a portion of the order is to be filled (partial fill scenario), an optional amountToFill parameter is used to specific the quantity of tokens to be filled. If not specified, the entire order will be filled.

💡Fees
Ecosystem partners such as marketplaces can assign fees to the orders they facilitate. For more information about fees, check out the following product page

Get transactions for bulk fulfillment

💡Number of Soft Cancels In a Single Transaction
You can fullfill up to 50 listings in a single transaction. If a trader needs to execute more than 50 orders, Immutable recommends batching the execution into separate transactions.

Happy path: All assets in bulk order are available at time of purchase

Implement the below code to fullfill a bulk order:

The fulfillBulkOrders call returns actions that are required to fulfill an order.

For more information on action types, have a look at our Actions page.

💡Currency
Each fulfillment request has to be in the same currency (ERC20, or Native) - your marketplace application should enforce this on the front end.
💡Approval
If the taker has purchased NFTs in the currency before, or if the listing is in the native token, no approval will be required and there will be only one fulfillment transaction in the list of actions.
import { orderbook } from '@imtbl/sdk';
import { Signer } from 'ethers'; // ethers v5

export interface Listing {
listingId: string,
amountToFill: string
}

const fulfillBulkListings = async (
client: orderbook.Orderbook,
signer: Signer,
listings: Array<Listing>
): Promise<void> => {
const fulfiller = await signer.getAddress();

// always use a try catch when interacting with the SDK as most methods throw on
// catastrophic errors
try {
// fulfillableOrders here could contain only partially the requested listings
// an example unfillableOrder is when the listing gets cancelled, or has been fulfilled.
const fulfillResponse = await client.fulfillBulkOrders(
listings.map((listing) => ({
listingId: listing.listingId,
// you could have up to 2 marketplace fees
// the use case is typically a 'referrer fee'
takerFees: [
// Optional taker marketplace fee
{
amount: '1000000',
recipientAddress: '0xFooBar', // Replace address with your own marketplace address
},
],
})),
fulfiller
);

// sufficient fulfiller balance - we can now go ahead and execute tx
if (fulfillResponse.sufficientBalance) {
const { actions, expiration, fulfillableOrders, unfulfillableOrders } =
fulfillResponse;

// depending on the application, we can either throw an error if some orders are not fulfillable
// or we can ignore these unfulfillable orders and proceed with fulfillment
if (unfulfillableOrders.length > 0) {
throw new Error(
`Not all orders are fulfillable - unfulfillable orders: ${unfulfillableOrders}`
);
}

// the fulfillment transaction will fulfill orders specified in the fulfillableOrders field
for (const action of actions) {
if (action.type === orderbook.ActionType.TRANSACTION) {
const builtTx = await action.buildTransaction();
console.log(`Submitting ${action.purpose} transaction`);
await signer.sendTransaction(builtTx);
}
}

console.log(
`Fulfilling listings ${fulfillableOrders}, transaction expiry ${expiration}`
);
}
} catch (e) {
// this indicates either a malformatted request or a server side error
// this should be handled as an error by the application
console.error(`Fulfill bulk orders request failed with ${e}`);
}
};

All fulfillment actions are transaction actions and include a type and builder method that can be used to generate the raw transaction for submission.

The purpose of these transactions are as follows:

  • APPROVAL - An approval transaction is required to be submitted before the fulfillment transaction if spending an ERC20 token and the seaport contract does not yet have the required allowance.
  • FULFILL_ORDER - The fulfillment transaction to be submitted to fulfill the order.

The taker in the code sample above is any ethers compatible Signer or Wallet instance for the user creating the listing.

The fulfillBulkOrders call also returns the expiry for the transactions. If a user submits a transaction after the expiration of the listing it will fail on chain as it will be rejected by the Seaport settlement contract.

The reject will look like a failed transaction in metamask - so it is crucial for the marketplace to display the expiration and remind users to submit in time in order to not waste gas.

The orders are now filled. You can poll Get orders for the off-chain representation of the order. It will asynchronously transition to FILLED once on-chain events have been registered by Immutable services.

When some orders are unavailable to be fulfilled

Bulk orders empower traders to select multiple assets for their transactions using a shopping cart feature. However, due to potential changes like asset cancellation or sale by the lister, some assets in the bulk fulfillment request may become unexecutable. To enhance user experience, ecosystem partners, such as marketplaces, should actively update the status of selected items and notify users about any unavailable orders.

Despite these precautions, race conditions may arise, leading to some orders being unavailable at the time of transaction submission. In such cases, the SDK provides a list of both fulfillable and unfulfillable orders. Additionally, Immutable generates a transaction that, upon signing, completes all fulfillable orders.

Ecosystem partners have the discretion to choose the user experience approach in these scenarios:

  1. All or Nothing: The marketplace informs the trader that the current cart cannot be executed as is, advising removal of certain assets.
  2. Best Effort: The transaction proceeds despite some orders being unexecutable, to avoid further delays and potential loss of other purchasable assets.
  3. Hybrid: Users are alerted that not all items in the cart are executable and given the choice to proceed or not.
💡OverFill
If an order is attempted to be filled beyond the available quantity a `best effort` fulfillment is attempted where the order is filled up to the available quantity.

Below is an example of how Immutable's orderbook processes a bulk order transaction where all selected assets are executable.

{
"fulfillableOrders": [
{
"id": "018bea91-8c1c-514d-90c8-94a409f1c420",
"accountAddress": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"buy": [
{
"type": "NATIVE",
"amount": "100000"
}
],
"sell": [
{
"type": "ERC721",
"contractAddress": "0x3dd1f7ac1b89650d22af78856acef8581b7803f7",
"tokenId": "0"
}
],
"fees": [
{
"amount": "1000",
"recipient": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"type": "ROYALTY"
},
{
"amount": "2000",
"recipient": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"type": "PROTOCOL"
},
{
"amount": "1",
"recipient": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"type": "MAKER_ECOSYSTEM"
},
{
"amount": "1",
"recipient": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"type": "TAKER_ECOSYSTEM"
}
],
"chain": {
"id": "eip155:15003",
"name": "imtbl-zkevm-devnet"
},
"createdAt": "2023-11-20T02:31:29.133612Z",
"endAt": "2023-11-20T10:50:33Z",
"protocolData": {
"counter": "0",
"orderType": "FULL_RESTRICTED",
"seaportAddress": "0x2cfa8f64e1b49a2df28532d1d30cda45117cf778",
"seaportVersion": "1.5",
"zoneAddress": "0x1bb4fb11ba021bd0104f0ee8e5f5c728bc83d7f1"
},
"salt": "0x0b491bfc80b3e6c2",
"signature": "0xf663dc528ab86cc738452f0ab44075e81344b27549f50c5cb62a852788ae6e27216834b730529ef0cc5e5aff8437eb071c9ed25adcb41ceaf0fe4715387212cf1b",
"startAt": "2023-11-20T02:30:33Z",
"status": {
"name": "ACTIVE"
},
"updatedAt": "2023-11-20T02:31:29.655854Z"
},
{
"id": "018bea91-8fa3-28ed-d6a9-65dab6ae68d3",
"accountAddress": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"buy": [
{
"type": "NATIVE",
"amount": "100000"
}
],
"sell": [
{
"type": "ERC721",
"contractAddress": "0x3dd1f7ac1b89650d22af78856acef8581b7803f7",
"tokenId": "1"
}
],
"fees": [
{
"amount": "1000",
"recipient": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"type": "ROYALTY"
},
{
"amount": "2000",
"recipient": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"type": "PROTOCOL"
},
{
"amount": "1",
"recipient": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"type": "MAKER_ECOSYSTEM"
},
{
"amount": "1",
"recipient": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"type": "TAKER_ECOSYSTEM"
}
],
"chain": {
"id": "eip155:15003",
"name": "imtbl-zkevm-devnet"
},
"createdAt": "2023-11-20T02:31:30.011609Z",
"endAt": "2023-11-20T10:50:39Z",
"protocolData": {
"counter": "0",
"orderType": "FULL_RESTRICTED",
"seaportAddress": "0x2cfa8f64e1b49a2df28532d1d30cda45117cf778",
"seaportVersion": "1.5",
"zoneAddress": "0x1bb4fb11ba021bd0104f0ee8e5f5c728bc83d7f1"
},
"salt": "0xc116dde6f14bc96b",
"signature": "0x3075febd2f79af6096c23b18eb6de8e254727c1bc7407788ef482982ad260a472cc13cae39f36c018f6b244a674b8eabff61ce90c4e2d94148d15fed2b1891541b",
"startAt": "2023-11-20T02:30:39Z",
"status": {
"name": "ACTIVE"
},
"updatedAt": "2023-11-20T02:31:30.411317Z"
}
],
"unfulfillableOrders": [],
"sufficientBalance": true
}
💡Confirmation
Fee details for each order can change between the cached orders and fulfillment. Users should confirm the fees returned in `fulfillableOrders` before submitting their transaction.

When the trader has insufficient balance to complete the transaction

In instances where a trader lacks sufficient currency to execute their assembled shopping cart, the fulfillBulkOrders function will yield a FulfillBulkOrdersInsufficientBalanceResponse. This response includes information on both fulfillable and unfulfillable orders but omits the actionable steps previously provided.

{
"fulfillableOrders": [
// ... (fulfillable order details as shown above)
]
"unfulfillableOrders": [],
"sufficientBalance": false
}

The application should now prompt the user to top up their balance and retry the fulfillment flow.