Skip to main content

11. Mint assets in game

💡Info
The complete code for this step can be found in the branch named step_11.

After players have successfully logged into Immutable Passport and set up their wallets, they will receive a fox (i.e. Immutable Runner Fox NFT) and any Immutable coins (i.e. Immutable Runner Token) they collect while playing each level of the game. Using the minting backend server, you will mint these in-game assets directly to their wallet.

Mint fox and coins

Mint fox

To simplify this tutorial, we won't verify whether the player already has a fox or Immutable Runner Fox NFT in their wallet. Once the player logs in and sets up their wallet, you'll mint them a new fox every time.

Update the 'Next' button in SetupWalletScreen.cs to navigate to the mint screen instead of the next level.

Assets/Shared/Scripts/UI/SetupWalletScreen.cs

private void OnNextButtonClicked()
{
m_MintEvent.Raise();
}

Add two functions: one to retrieve the player's wallet address and another to mint a fox using the backend server.

Assets/Shared/Scripts/UI/MintScreen.cs

using System.Net.Http;
using Immutable.Passport;
using Cysharp.Threading.Tasks;

namespace HyperCasual.Runner
{
/// <summary>
/// This View contains celebration screen functionalities
/// </summary>
public class MintScreen : View
{
// omitted

/// <summary>
/// Gets the wallet address of the player.
/// </summary>
private async UniTask<string> GetWalletAddress()
{
List<string> accounts = await Passport.Instance.ZkEvmRequestAccounts();
return accounts[0]; // Get the first wallet address
}

/// <summary>
/// Mints a fox (i.e. Immutable Runner Fox) to the player's wallet
/// </summary>
/// <returns>True if minted a fox successfully to player's wallet. Otherwise, false.</returns>
private async UniTask<bool> MintFox()
{
Debug.Log("Minting fox...");
try
{
string address = await GetWalletAddress(); // Get the player's wallet address to mint the fox to

if (address != null)
{
var nvc = new List<KeyValuePair<string, string>>
{
// Set 'to' to the player's wallet address
new KeyValuePair<string, string>("to", address)
};
using var client = new HttpClient();
string url = $"http://localhost:3000/mint/fox"; // Endpoint to mint fox
using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = new FormUrlEncodedContent(nvc) };
using var res = await client.SendAsync(req);
return res.IsSuccessStatusCode;
}

return false;
}
catch (Exception ex)
{
Debug.Log($"Failed to mint fox: {ex.Message}");
return false;
}
}

// omitted
}
}

Mint coins

Add another function to mint collected coins.

Assets/Shared/Scripts/UI/MintScreen.cs

using System.Numerics;

namespace HyperCasual.Runner
{
/// <summary>
/// This View contains celebration screen functionalities
/// </summary>
public class MintScreen : View
{
// omitted

/// <summary>
/// Mints collected coins (i.e. Immutable Runner Token) to the player's wallet
/// </summary>
/// <returns>True if minted coins successfully to player's wallet. Otherwise, false.</returns>
private async UniTask<bool> MintCoins()
{
Debug.Log("Minting coins...");
try
{
int coinsCollected = GetNumCoinsCollected(); // Get number of coins collected
if (coinsCollected == 0) // Don't mint any coins if player did not collect any
{
return true;
}

string address = await GetWalletAddress(); // Get the player's wallet address to mint the coins to
if (address != null)
{
// Calculate the quantity to mint
// Need to take into account Immutable Runner Token decimal value i.e. 18
BigInteger quantity = BigInteger.Multiply(new BigInteger(coinsCollected), BigInteger.Pow(10, 18));
Debug.Log($"Quantity: {quantity}");
var nvc = new List<KeyValuePair<string, string>>
{
// Set 'to' to the player's wallet address
new KeyValuePair<string, string>("to", address),
// Set 'quanity' to the number of coins collected
new KeyValuePair<string, string>("quantity", quantity.ToString())
};
using var client = new HttpClient();
string url = $"http://localhost:3000/mint/token"; // Endpoint to mint token
using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = new FormUrlEncodedContent(nvc) };
using var res = await client.SendAsync(req);
return res.IsSuccessStatusCode;
}

return false;
}
catch (Exception ex)
{
Debug.Log($"Failed to mint coins: {ex.Message}");
return false;
}
}

// omitted
}
}

Putting everything together

Update the Mint() function to mint the fox and coins to the player using your added functions.

Assets/Shared/Scripts/UI/MintScreen.cs

// If there's an error minting, these values will be used when the player clicks the "Try again" button
private bool mintedFox = false;
private bool mintedCoins = false;

public void OnEnable()
{
// omitted

// Reset values
mintedFox = false;
mintedCoins = false;

Mint();
}

private async void Mint()
{
try
{
ShowMintingMessage();
ShowLoading(true);
ShowError(false);
ShowNextButton(false);

// Mint fox if not minted yet
if (!mintedFox)
{
mintedFox = await MintFox();
}
// Mint coins if not minted yet
if (!mintedCoins)
{
mintedCoins = await MintCoins();
}

// Show minted message if minted both fox and coins successfully
if (mintedFox && mintedCoins)
{
ShowMintedMessage();
}
ShowLoading(false);
// Show error if failed to mint fox or coins
ShowError(!mintedFox || !mintedCoins);
// Show next button is minted both fox and coins successfully
ShowNextButton(mintedFox && mintedCoins);
}
catch (Exception ex)
{
// Failed to mint, let the player try again
Debug.Log($"Failed to mint: {ex.Message}");
ShowLoading(false);
ShowError(true);
ShowNextButton(false);
}
}

Ensure the minting backend is up and running before you start playing.

If you manage to mint the fox and coins successfully, you should see the following output in the console:

POST /mint/fox 200 7998.590 ms - 2
POST /mint/token 200 6865.859 ms - 2
Minting

View minted assets

Players can view their fox and coins in the block explorer.

Assets/Shared/Scripts/UI/MintScreen.cs

private async void OnWalletClicked()
{
// Get the player's wallet address to mint the fox to
string address = await GetWalletAddress();
// Show the player's tokens on the block explorer page.
Application.OpenURL($"https://explorer.testnet.immutable.com/address/{address}?tab=tokens");
}
Immutable Runner own fox

Mint coins in the background

As the player progresses through the game, they will continue to collect coins, which should be minted and given to them.

Update LevelCompleteScreen.cs to mint coins to the player's wallet upon level completion.

Assets/Shared/Scripts/UI/LevelCompleteScreen.cs

using Cysharp.Threading.Tasks;
using System.Numerics;
using System.Net.Http;

namespace HyperCasual.Runner
{
/// <summary>
/// This View contains celebration screen functionalities
/// </summary>
public class LevelCompleteScreen : View
{
// omitted

public async void OnEnable()
{
// omitted

ShowError(false);
ShowLoading(false);

// If player is logged into Passport mint coins to player
if (SaveManager.Instance.IsLoggedIn)
{
// Mint collected coins to player
await MintCoins();
}
else
{
// Show 'Next' button if player is already logged into Passport
ShowNextButton(SaveManager.Instance.IsLoggedIn);
// Show "Continue with Passport" button if the player is not logged into Passport
ShowContinueWithPassportButton(!SaveManager.Instance.IsLoggedIn);
}
}

/// <summary>
/// Mints collected coins (i.e. Immutable Runner Token) to the player's wallet
/// </summary>
private async UniTask MintCoins()
{
// This function is similar to MintCoins() in MintScreen.cs. Consider refactoring duplicate code in production.
Debug.Log("Minting coins...");
bool success = false;

// Show loading
ShowLoading(true);
ShowNextButton(false);
ShowError(false);

try
{
// Don't mint any coins if player did not collect any
if (m_CoinCount == 0)
{
success = true;
}
else
{
// Get the player's wallet address to mint the coins to
List<string> accounts = await Passport.Instance.ZkEvmRequestAccounts();
string address = accounts[0];
if (address != null)
{
// Calculate the quantity to mint
// Need to take into account Immutable Runner Token decimal value i.e. 18
BigInteger quantity = BigInteger.Multiply(new BigInteger(m_CoinCount), BigInteger.Pow(10, 18));
Debug.Log($"Quantity: {quantity}");
var nvc = new List<KeyValuePair<string, string>>
{
// Set 'to' to the player's wallet address
new KeyValuePair<string, string>("to", address),
// Set 'quanity' to the number of coins collected
new KeyValuePair<string, string>("quantity", quantity.ToString())
};
using var client = new HttpClient();
string url = $"http://localhost:3000/mint/token"; // Endpoint to mint token
using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = new FormUrlEncodedContent(nvc) };
using var res = await client.SendAsync(req);
success = res.IsSuccessStatusCode;
}
}
}
catch (Exception ex)
{
Debug.Log($"Failed to mint coins: {ex.Message}");
}

ShowLoading(false);
ShowNextButton(success);
ShowError(!success);
}

private async void OnTryAgainButtonClicked()
{
await MintCoins();
}

// omitted
}
}