Skip to main content

Create orders in Unity

In this tutorial step, you will learn how to create orders in your game by using the Unity Orderbook package.

This allows players to list their NFTs for sale in the marketplace.
Create orders in UnityCreate orders in Unity
💡Prerequisite step
Before continuing with this step, ensure you have completed the previous display marketplace listings step of this tutorial series.

Overview

With the inventory screen ready, you can now enable players to create NFT order listings using the Unity Orderbook package. In the sample game we've added the sell button to the asset details screen, which will allow the player to set the price and create an order for the selected NFT. Once the order is confirmed the player will be able to see their NFT listed in the marketplace.

Create Order

Create an order

To create an order, there are several steps involved:

  1. Prepare the listing: Choose the NFT to list and set the sale price and ERC20 contract address.

  2. Sign and submit approval: Ensure the listing meets the necessary approval requirements.

  3. Sign the order: Have the player sign the order data using their wallet.

  4. Create the listing: Finalise the listing and post it to the Immutable Orderbook.

For more details on each step, you can read the Unity documentation for creating orders.

Below is the example code for creating an order in the Unity Orderbook package:

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;
}
}

After creating the order

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 onchain, 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.

💡Status polling
You can poll the Get Listing endpoint to check on status updates - in the near future we also plan on introducing push based (webhook) integration for order events.

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.

Next steps

Great, now you can create an order so it displays as an active listing in the marketplace! Next we will teach you how to cancel an order so it is removed from the marketplace. Click next below to continue the tutorial.