Create orders - listings
There are several steps required, so make sure you follow the instructions carefully to ensure a successful listing.
Overview
Creating an order is the first step in trading an NFT on the Immutable Orderbook. This process involves setting the price, contract address, and token ID for the NFT the player wishes to sell. Once orders are successfully created, they can then be filled by other players, allowing them to purchase the NFTs which has been listed. Or they can ben cancelled by the owner if they wish to remove the listing.
To list an NFT at a fixed price, you need to do the following:
- Prepare the listing.
- Sign and submit the approval transaction (if required).
- Sign the typed order data.
- Submit the listing.
Below are partial code snippets that demonstrate the essential parts of each of these steps. If you want to see the full implementation, you can take a look at CreateOrderUseCase.cs in our Unity sample game.
For more details on each step, refer to the Typescript Orderbook specification.
CreateListing
method described in this page.Create order workflow
- Passport package setup
- Orderbook package setup
- Login
- Initialise provider and connect wallet
- Link all game contracts to the Immutable Hub to enable pre-approved transactions. For detailed instructions, please refer to the guide here.
1. Prepare the listing
This step sets up the necessary data for a listing request, including the buy and sell details,
and then sends the request using PrepareListing
method of Immutable Orderbook API.
private async UniTask<PrepareListing200Response> PrepareListing(
string contractAddress, string contractType, string tokenId,
string price, string amountToSell, string buyContractAddress)
{
var sellRequest = CreateSellRequest(contractType, contractAddress, tokenId, amountToSell);
var buyRequest = new ERC20Item(price, buyContractAddress);
return await m_OrderbookApi.PrepareListingAsync(new PrepareListingRequest(
makerAddress: LOGGED_IN_WALLET, // Replace with player's wallet
sell: sellRequest,
buy: new PrepareListingRequestBuy(buyRequest)
));
}
private static PrepareListingRequestSell CreateSellRequest(
string contractType, string contractAddress, string tokenId, string amountToSell)
{
return contractType.ToUpper() switch
{
"ERC1155" => new PrepareListingRequestSell(new ERC1155Item(amountToSell, contractAddress, tokenId)),
"ERC721" => new PrepareListingRequestSell(new ERC721Item(contractAddress, tokenId)),
_ => throw new Exception($"Unsupported contract type: {contractType}")
};
}
2. Sign and submit the approval transaction
Once the listing request is prepared and sent, the response will contain actions that need to be taken.
If the player has never approved your contract to interact with the Immutable Orderbook before, the TransactionAction
will be present in the response.
This TransactionAction
must be signed and submitted to the blockchain through the Immutable SDK to grant the approvals required to create listings in the Orderbook. This action only needs to be taken once per user per collection.
If the TransactionAction
is not present, you can skip this step and continue to sign then submit the listing.
private async UniTask SignAndSubmitApproval(PrepareListing200Response listingData)
{
var transactionAction = listingData.Actions
.FirstOrDefault(action => action.ActualInstance is TransactionAction)?
.GetTransactionAction();
if (transactionAction == null) return;
var response = await Passport.Instance.ZkEvmSendTransactionWithConfirmation(
new TransactionRequest
{
to = transactionAction.PopulatedTransactions.To,
data = transactionAction.PopulatedTransactions.Data,
value = "0"
});
if (response.status != "1")
throw new Exception("Failed to sign and submit approval.");
}
3. Sign the typed order data
Once the approval transaction is signed and submitted, or if the TransactionAction
is
not present, the typed order data can be signed using ZkEvmSignTypedDataV4
.
To successfully sign the typed order data it must be serialised
into a JSON string and signed using the player's wallet.
private async UniTask<string> SignListing(PrepareListing200Response listingData)
{
var signableAction = listingData.Actions
.FirstOrDefault(action => action.ActualInstance is SignableAction)?
.GetSignableAction();
if (signableAction == null)
throw new Exception("No valid listing to sign.");
var messageJson = JsonConvert.SerializeObject(signableAction.Message, Formatting.Indented);
return await Passport.Instance.ZkEvmSignTypedDataV4(messageJson);
}
4. Create the listing
After the typed order data is signed, the listing can be created using the CreateListingAsync
method of the Immutable Orderbook API.
private async UniTask<string> ListAsset(string signature, PrepareListing200Response listingData)
{
var response = await m_OrderbookApi.CreateListingAsync(new CreateListingRequest(
new List<FeeValue>(),
listingData.OrderComponents,
listingData.OrderHash,
signature
));
return response.Result.Id;
}
After creating the listing
The order creation transaction is now being processed. Orders created will initially be in PENDING
status.
Upon further validating the required contract approvals and ownership of the NFT being listed on‑chain, the order will asynchronously transition to the ACTIVE
status.
We recommend taking an optimistic view and showing the order as ACTIVE
as soon as it is created, then polling the order status to check for updates for the most seamless player experience.
You can see all the different order status types in the table in the order statuses documentation.
For more details on each step, you can read the Typescript documentation for creating orders since the Unity Orderbook closely follows this implementation.
You can also view a full example in the Unity sample game. This builds upon the concepts presented in the Build a game with Unity tutorial.
Full sample code
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using Immutable.Orderbook.Api;
using Immutable.Orderbook.Client;
using Immutable.Orderbook.Model;
using Immutable.Passport;
using Immutable.Passport.Model;
using Newtonsoft.Json;
using UnityEngine;
using ERC1155Item = Immutable.Orderbook.Model.ERC1155Item;
using ERC20Item = Immutable.Orderbook.Model.ERC20Item;
using ERC721Item = Immutable.Orderbook.Model.ERC721Item;
public class CreateOrderUseCase
{
private static readonly Lazy<CreateOrderUseCase> s_Instance = new(() => new CreateOrderUseCase());
private readonly OrderbookApi m_OrderbookApi = new(new Configuration { BasePath = "https://api.immutable.com" });
private CreateOrderUseCase() { }
public static CreateOrderUseCase Instance => s_Instance.Value;
/// <summary>
/// Creates a new listing for the specified NFT.
/// </summary>
/// <param name="contractAddress">The address of the NFT's contract.</param>
/// <param name="contractType">The type of the contract (e.g., "ERC721" or "ERC1155").</param>
/// <param name="tokenId">The ID of the NFT.</param>
/// <param name="price">
/// The sale price of the NFT, represented as a string amount in IMR (scaled by 10^18).
/// </param>
/// <param name="amountToSell">
/// The quantity of the NFT to sell. "1" for ERC721 tokens and a higher number for ERC1155 tokens.
/// </param>
/// <param name="buyContractAddress">
/// The contract address of the token used to purchase the NFT.
/// </param>
/// <returns>
/// A <see cref="UniTask{String}"/> that returns the listing ID if the sale is successfully created.
/// </returns>
public async UniTask<string> CreateListing(
string contractAddress, string contractType, string tokenId,
string price, string amountToSell, string buyContractAddress)
{
try
{
if (contractType == "ERC721" && amountToSell != "1")
{
throw new ArgumentException("Invalid arguments: 'amountToSell' must be '1' when listing an ERC721.");
}
var listingData = await PrepareListing(contractAddress, contractType, tokenId, price, amountToSell, buyContractAddress);
await SignAndSubmitApproval(listingData);
var signature = await SignListing(listingData);
var listingId = await ListAsset(signature, listingData);
return listingId;
}
catch (ApiException e)
{
Debug.LogError($"API Error: {e.Message} (Status: {e.ErrorCode})");
Debug.LogError(e.ErrorContent);
Debug.LogError(e.StackTrace);
throw;
}
}
/// <summary>
/// Prepares a listing for the specified NFT and purchase details.
/// </summary>
private async UniTask<PrepareListing200Response> PrepareListing(
string contractAddress, string contractType, string tokenId,
string price, string amountToSell, string buyContractAddress)
{
var sellRequest = CreateSellRequest(contractType, contractAddress, tokenId, amountToSell);
var buyRequest = new ERC20Item(price, buyContractAddress);
return await m_OrderbookApi.PrepareListingAsync(new PrepareListingRequest(
makerAddress: LOGGED_IN_WALLET, // Replace with player's wallet
sell: sellRequest,
buy: new PrepareListingRequestBuy(buyRequest)
));
}
/// <summary>
/// Creates the appropriate sell request based on the contract type.
/// </summary>
private static PrepareListingRequestSell CreateSellRequest(
string contractType, string contractAddress, string tokenId, string amountToSell)
{
return contractType.ToUpper() switch
{
"ERC1155" => new PrepareListingRequestSell(new ERC1155Item(amountToSell, contractAddress, tokenId)),
"ERC721" => new PrepareListingRequestSell(new ERC721Item(contractAddress, tokenId)),
_ => throw new Exception($"Unsupported contract type: {contractType}")
};
}
/// <summary>
/// Signs and submits approval if required by the listing.
/// </summary>
private async UniTask SignAndSubmitApproval(PrepareListing200Response listingData)
{
var transactionAction = listingData.Actions
.FirstOrDefault(action => action.ActualInstance is TransactionAction)?
.GetTransactionAction();
if (transactionAction == null) return;
var response = await Passport.Instance.ZkEvmSendTransactionWithConfirmation(
new TransactionRequest
{
to = transactionAction.PopulatedTransactions.To,
data = transactionAction.PopulatedTransactions.Data,
value = "0"
});
if (response.status != "1")
throw new Exception("Failed to sign and submit approval.");
}
/// <summary>
/// Signs the listing with the gamer's wallet.
/// </summary>
private async UniTask<string> SignListing(PrepareListing200Response listingData)
{
var signableAction = listingData.Actions
.FirstOrDefault(action => action.ActualInstance is SignableAction)?
.GetSignableAction();
if (signableAction == null)
throw new Exception("No valid listing to sign.");
var messageJson = JsonConvert.SerializeObject(signableAction.Message, Formatting.Indented);
return await Passport.Instance.ZkEvmSignTypedDataV4(messageJson);
}
/// <summary>
/// Finalises the listing and returns the listing ID.
/// </summary>
private async UniTask<string> ListAsset(string signature, PrepareListing200Response listingData)
{
var response = await m_OrderbookApi.CreateListingAsync(new CreateListingRequest(
new List<FeeValue>(),
listingData.OrderComponents,
listingData.OrderHash,
signature
));
return response.Result.Id;
}
}