Create orders - listings
This page explains how to create a listing using the Immutable Unity Orderbook package, enabling you to list an NFT for sale on the Orderbook.
- 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.
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, as determined by the API).
- Sign the typed order data.
- Create the listing.
The complete sample code is available at the bottom of this page.
Prepare the listing
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: SaveManager.Instance.WalletAddress,
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}")
};
}
Sign and submit the approval transaction
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.");
}
Sign the typed order data
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);
}
Create the listing
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;
}
Orders created will initially be in PENDING
status. Upon further validating blockchain approval events (if necessary) and balance checks (i.e. listing owner indeed owns NFT),
it will become ACTIVE
. You can read more about order statuses here.
For more details on each step, refer to the this.
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: SaveManager.Instance.WalletAddress,
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;
}
}
You can also view a full example in the sample game here. This builds upon the concepts presented in the Build a game with Unity tutorial.