Skip to content

Commit

Permalink
feat: add use cases
Browse files Browse the repository at this point in the history
  • Loading branch information
nattb8 committed Oct 22, 2024
1 parent bcb4ac3 commit 5855843
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 8 deletions.
71 changes: 71 additions & 0 deletions Assets/Shared/Scripts/Domain/CancelListingUseCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
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;

namespace HyperCasual.Runner
{
public class CancelListingUseCase
{
private static readonly Lazy<CancelListingUseCase> s_Instance = new(() => new CancelListingUseCase());

private readonly OrderbookApi m_OrderbookApi = new(new Configuration { BasePath = Config.BASE_URL });

private CancelListingUseCase() { }

public static CancelListingUseCase Instance => s_Instance.Value;

/// <summary>
/// Cancels the specified listing.
/// </summary>
/// <param name="listingId">The unique identifier of the listing to cancel.</param>
public async UniTask CancelListing(string listingId)
{
try
{
var request = new CancelOrdersOnChainRequest(
accountAddress: SaveManager.Instance.WalletAddress, orderIds: new List<string> { listingId });

var response = await m_OrderbookApi.CancelOrdersOnChainAsync(request);
var transactionAction = response?.CancellationAction.PopulatedTransactions;

if (transactionAction?.To == null)
throw new Exception("Failed to cancel listing.");

var txResponse = await Passport.Instance.ZkEvmSendTransactionWithConfirmation(
new TransactionRequest
{
to = transactionAction.To,
data = transactionAction.Data,
value = "0"
});

if (txResponse.status != "1")
throw new Exception("Failed to cancel listing.");
}
catch (ApiException e)
{
HandleApiException(e);
throw;
}
}

/// <summary>
/// Handles API exceptions by logging relevant details.
/// </summary>
private static void HandleApiException(ApiException e)
{
Debug.LogError($"API Error: {e.Message} (Status: {e.ErrorCode})");
Debug.LogError(e.ErrorContent);
Debug.LogError(e.StackTrace);
var errorModel = JsonConvert.DeserializeObject<ErrorModel>($"{e.ErrorContent}");
if (errorModel != null) throw new Exception(errorModel.message);
}
}
}
3 changes: 3 additions & 0 deletions Assets/Shared/Scripts/Domain/CancelListingUseCase.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

170 changes: 170 additions & 0 deletions Assets/Shared/Scripts/Domain/CreateOrderUseCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
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;

namespace HyperCasual.Runner
{
public class CreateOrderUseCase
{
private static readonly Lazy<CreateOrderUseCase> s_Instance = new(() => new CreateOrderUseCase());

private readonly OrderbookApi m_OrderbookApi = new(new Configuration { BasePath = Config.BASE_URL });

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)
{
HandleApiException(e);
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 user'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;
}

/// <summary>
/// Handles API exceptions by logging relevant details.
/// </summary>
private static void HandleApiException(ApiException e)
{
Debug.LogError($"API Error: {e.Message} (Status: {e.ErrorCode})");
Debug.LogError(e.ErrorContent);
Debug.LogError(e.StackTrace);
var errorModel = JsonConvert.DeserializeObject<ErrorModel>($"{e.ErrorContent}");
if (errorModel != null) throw new Exception(errorModel.message);
}
}
}
3 changes: 3 additions & 0 deletions Assets/Shared/Scripts/Domain/CreateOrderUseCase.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions Assets/Shared/Scripts/Domain/FulfillOrderUseCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
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;

namespace HyperCasual.Runner
{
public class FulfillOrderUseCase
{
private static readonly Lazy<FulfillOrderUseCase> s_Instance = new(() => new FulfillOrderUseCase());

private readonly OrderbookApi m_OrderbookApi = new(new Configuration { BasePath = Config.BASE_URL });

private FulfillOrderUseCase() { }

public static FulfillOrderUseCase Instance => s_Instance.Value;

/// <summary>
/// Executes an order by fulfilling a listing and optionally confirming its status.
/// </summary>
/// <param name="listingId">The unique identifier of the listing to fulfill.</param>
/// <param name="fees">The taker fees</param>
public async UniTask ExecuteOrder(string listingId, List<FulfillOrderRequestTakerFeesInner> fees)
{
try
{
var request = new FulfillOrderRequest(
takerAddress: SaveManager.Instance.WalletAddress,
listingId: listingId,
takerFees: fees);

var createListingResponse = await m_OrderbookApi.FulfillOrderAsync(request);

if (createListingResponse.Actions.Count > 0)
{
foreach (var transaction in createListingResponse.Actions)
{
var transactionHash = await Passport.Instance.ZkEvmSendTransaction(new TransactionRequest
{
to = transaction.PopulatedTransactions.To,
data = transaction.PopulatedTransactions.Data,
value = "0"
});
Debug.Log($"Transaction hash: {transactionHash}");
}
}
}
catch (ApiException e)
{
HandleApiException(e);
throw;
}
}

/// <summary>
/// Handles API exceptions by logging relevant details.
/// </summary>
private static void HandleApiException(ApiException e)
{
Debug.LogError($"API Error: {e.Message} (Status: {e.ErrorCode})");
Debug.LogError(e.ErrorContent);
Debug.LogError(e.StackTrace);
var errorModel = JsonConvert.DeserializeObject<ErrorModel>($"{e.ErrorContent}");
if (errorModel != null) throw new Exception(errorModel.message);
}
}
}
3 changes: 3 additions & 0 deletions Assets/Shared/Scripts/Domain/FulfillOrderUseCase.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions Assets/Shared/Scripts/Domain/OrderbookManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ private OrderbookManager() { }
/// <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>
/// <param name="confirmListing">
/// If true, the function will continuously poll the marketplace endpoint to ensure the listing status
/// updates to "ACTIVE" upon creation. If false, the function will not verify the listing status.
Expand All @@ -48,7 +51,7 @@ private OrderbookManager() { }
/// </returns>
public async UniTask<string> CreateListing(
string contractAddress, string contractType, string tokenId,
string price, string amountToSell, bool confirmListing = true)
string price, string amountToSell, string buyContractAddress, bool confirmListing = true)
{
try
{
Expand All @@ -57,7 +60,7 @@ public async UniTask<string> CreateListing(
throw new ArgumentException("Invalid arguments: 'amountToSell' must be '1' when listing an ERC721.");
}

var listingData = await PrepareListing(contractAddress, contractType, tokenId, price, amountToSell);
var listingData = await PrepareListing(contractAddress, contractType, tokenId, price, amountToSell, buyContractAddress);

await SignAndSubmitApproval(listingData);

Expand All @@ -81,10 +84,10 @@ public async UniTask<string> CreateListing(
/// </summary>
private async UniTask<PrepareListing200Response> PrepareListing(
string contractAddress, string contractType, string tokenId,
string price, string amountToSell)
string price, string amountToSell, string buyContractAddress)
{
var sellRequest = CreateSellRequest(contractType, contractAddress, tokenId, amountToSell);
var buyRequest = new ERC20Item(price, Contract.TOKEN);
var buyRequest = new ERC20Item(price, buyContractAddress);

return await m_OrderbookApi.PrepareListingAsync(new PrepareListingRequest(
makerAddress: SaveManager.Instance.WalletAddress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,13 @@ private async void OnSellButtonClicked()

try
{
m_ListingId = await OrderbookManager.Instance.CreateListing(
m_ListingId = await CreateOrderUseCase.Instance.CreateListing(
contractAddress: m_Asset.NftWithStack.ContractAddress,
contractType: m_Asset.NftWithStack.ContractType,
tokenId: m_Asset.NftWithStack.TokenId,
price: $"{normalisedPrice}",
amountToSell: amountToSell
amountToSell: amountToSell,
buyContractAddress: Contract.TOKEN
);

Debug.Log($"Sale complete: Listing ID: {m_ListingId}");
Expand Down Expand Up @@ -278,7 +279,7 @@ private async void OnCancelButtonClicked()

try
{
await OrderbookManager.Instance.CancelListing(m_ListingId);
await CancelListingUseCase.Instance.CancelListing(m_ListingId);

m_SellButton.gameObject.SetActive(true);
m_AmountText.text = "Not listed";
Expand Down
Loading

0 comments on commit 5855843

Please sign in to comment.