From c9e8a24e93296bc61ff6c040a1230a5c76bcaaab Mon Sep 17 00:00:00 2001
From: Kai Hirota <34954529+kaihirota@users.noreply.github.com>
Date: Mon, 14 Oct 2024 18:20:17 +1100
Subject: [PATCH] create immutable X sample game
---
.../Shared/Scripts/UI/LevelCompleteScreen.cs | 91 +-------
Assets/Shared/Scripts/UI/MainMenu.cs | 25 +--
Assets/Shared/Scripts/UI/MintScreen.cs | 132 ++++--------
Assets/Shared/Scripts/UI/SetupWalletScreen.cs | 16 +-
.../Shared/Scripts/UI/UnlockedSkinScreen.cs | 84 ++++++--
mint-backend/dest/index.js | 154 +++++++++-----
mint-backend/package.json | 23 ++-
mint-backend/src/index.ts | 194 ++++++++++++------
mint-backend/tsconfig.json | 2 +-
9 files changed, 372 insertions(+), 349 deletions(-)
diff --git a/Assets/Shared/Scripts/UI/LevelCompleteScreen.cs b/Assets/Shared/Scripts/UI/LevelCompleteScreen.cs
index 28d0d2220..478250c3e 100644
--- a/Assets/Shared/Scripts/UI/LevelCompleteScreen.cs
+++ b/Assets/Shared/Scripts/UI/LevelCompleteScreen.cs
@@ -6,9 +6,6 @@
using System;
using System.Collections.Generic;
using Immutable.Passport;
-using Cysharp.Threading.Tasks;
-using System.Numerics;
-using System.Net.Http;
namespace HyperCasual.Runner
{
@@ -106,7 +103,7 @@ public int CoinCount
}
}
- public async void OnEnable()
+ public void OnEnable()
{
// Set listener to 'Next' button
m_NextButton.RemoveListener(OnNextButtonClicked);
@@ -120,79 +117,10 @@ public async void OnEnable()
m_TryAgainButton.RemoveListener(OnTryAgainButtonClicked);
m_TryAgainButton.AddListener(OnTryAgainButtonClicked);
- 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);
- }
- }
-
- ///
- /// Mints collected coins (i.e. Immutable Runner Token) to the player's wallet
- ///
- 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 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>
- {
- // Set 'to' to the player's wallet address
- new KeyValuePair("to", address),
- // Set 'quanity' to the number of coins collected
- new KeyValuePair("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);
+ // 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);
}
private async void OnContinueWithPassportButtonClicked()
@@ -204,11 +132,7 @@ private async void OnContinueWithPassportButtonClicked()
ShowLoading(true);
// Log into Passport
-#if (UNITY_ANDROID && !UNITY_EDITOR_WIN) || (UNITY_IPHONE && !UNITY_EDITOR_WIN) || UNITY_STANDALONE_OSX
- await Passport.Instance.LoginPKCE();
-#else
await Passport.Instance.Login();
-#endif
// Successfully logged in
// Save a persistent flag in the game that the player is logged in
@@ -228,9 +152,8 @@ private async void OnContinueWithPassportButtonClicked()
}
}
- private async void OnTryAgainButtonClicked()
+ private void OnTryAgainButtonClicked()
{
- await MintCoins();
}
private void OnNextButtonClicked()
@@ -287,4 +210,4 @@ void DisplayCoins(int count)
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Shared/Scripts/UI/MainMenu.cs b/Assets/Shared/Scripts/UI/MainMenu.cs
index 052370c66..379379df3 100644
--- a/Assets/Shared/Scripts/UI/MainMenu.cs
+++ b/Assets/Shared/Scripts/UI/MainMenu.cs
@@ -38,16 +38,9 @@ async void OnEnable()
m_LogoutButton.AddListener(OnLogoutButtonClick);
// Initialise Passport
- string clientId = "YOUR_IMMUTABLE_CLIENT_ID";
+ string clientId = "";
string environment = Immutable.Passport.Model.Environment.SANDBOX;
- string redirectUri = null;
- string logoutUri = null;
-
-#if (UNITY_ANDROID && !UNITY_EDITOR_WIN) || (UNITY_IPHONE && !UNITY_EDITOR_WIN) || UNITY_STANDALONE_OSX
- redirectUri = "immutablerunner://callback";
- logoutUri = "immutablerunner://logout";
-#endif
- passport = await Passport.Init(clientId, environment, redirectUri, logoutUri);
+ passport = await Passport.Init(clientId, environment);
// Check if the player is supposed to be logged in and if there are credentials saved
if (SaveManager.Instance.IsLoggedIn && await Passport.Instance.HasCredentialsSaved())
@@ -56,20 +49,20 @@ async void OnEnable()
bool success = await Passport.Instance.Login(useCachedSession: true);
// Update the login flag
SaveManager.Instance.IsLoggedIn = success;
+
// Set up wallet if successful
if (success)
{
- await Passport.Instance.ConnectEvm();
- await Passport.Instance.ZkEvmRequestAccounts();
+ await Passport.Instance.ConnectImx();
}
- }
- else
- {
+ } else {
// No saved credentials to re-login the player, reset the login flag
SaveManager.Instance.IsLoggedIn = false;
}
ShowLoading(false);
+ ShowStartButton(true);
+
// Show the logout button if the player is logged in
ShowLogoutButton(SaveManager.Instance.IsLoggedIn);
}
@@ -95,11 +88,7 @@ async void OnLogoutButtonClick()
ShowLoading(true);
// Logout
-#if (UNITY_ANDROID && !UNITY_EDITOR_WIN) || (UNITY_IPHONE && !UNITY_EDITOR_WIN) || UNITY_STANDALONE_OSX
- await passport.LogoutPKCE();
-#else
await passport.Logout();
-#endif
// Reset the login flag
SaveManager.Instance.IsLoggedIn = false;
diff --git a/Assets/Shared/Scripts/UI/MintScreen.cs b/Assets/Shared/Scripts/UI/MintScreen.cs
index ab2aacb66..8e66c8197 100644
--- a/Assets/Shared/Scripts/UI/MintScreen.cs
+++ b/Assets/Shared/Scripts/UI/MintScreen.cs
@@ -4,11 +4,19 @@
using UnityEngine.UI;
using System;
using System.Collections.Generic;
-using System.Threading;
using System.Net.Http;
-using Immutable.Passport;
+using System.Threading;
using Cysharp.Threading.Tasks;
-using System.Numerics;
+using Immutable.Passport;
+using Immutable.Passport.Model;
+
+[Serializable]
+public class MintResult
+{
+ public string token_id;
+ public string contract_address;
+ public string tx_id;
+}
namespace HyperCasual.Runner
{
@@ -39,7 +47,6 @@ public class MintScreen : View
// 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()
{
@@ -57,7 +64,6 @@ public void OnEnable()
// Reset values
mintedFox = false;
- mintedCoins = false;
Mint();
}
@@ -74,33 +80,24 @@ private async void Mint()
// Mint fox if not minted yet
if (!mintedFox)
{
- mintedFox = await MintFox();
- }
- // Mint coins if not minted yet
- if (!mintedCoins)
- {
- mintedCoins = await MintCoins();
- }
+ MintResult mintResult = await MintFox();
- // Show minted message if minted both fox and coins successfully
- if (mintedFox && mintedCoins)
- {
+ // Show minted message if minted fox successfully
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);
+ Debug.Log($"Failed to mint or transfer: {ex.Message}");
}
+ ShowLoading(false);
+
+ // Show error if failed to mint fox
+ ShowError(!mintedFox);
+
+ // Show next button if fox minted successfully
+ ShowNextButton(mintedFox);
}
///
@@ -108,15 +105,15 @@ private async void Mint()
///
private async UniTask GetWalletAddress()
{
- List accounts = await Passport.Instance.ZkEvmRequestAccounts();
- return accounts[0]; // Get the first wallet address
+ string address = await Passport.Instance.GetAddress();
+ return address;
}
///
/// Mints a fox (i.e. Immutable Runner Fox) to the player's wallet
///
/// True if minted a fox successfully to player's wallet. Otherwise, false.
- private async UniTask MintFox()
+ private async UniTask MintFox()
{
Debug.Log("Minting fox...");
try
@@ -134,60 +131,26 @@ private async UniTask MintFox()
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;
- }
- }
+ // Parse JSON and extract token_id
+ string content = await res.Content.ReadAsStringAsync();
+ Debug.Log($"Mint fox response: {content}");
- ///
- /// Mints collected coins (i.e. Immutable Runner Token) to the player's wallet
- ///
- /// True if minted coins successfully to player's wallet. Otherwise, false.
- private async UniTask 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;
- }
+ MintResult mintResult = JsonUtility.FromJson(content);
+ Debug.Log($"Minted fox with token_id: {mintResult.token_id}");
- 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>
- {
- // Set 'to' to the player's wallet address
- new KeyValuePair("to", address),
- // Set 'quanity' to the number of coins collected
- new KeyValuePair("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;
+ mintedFox = res.IsSuccessStatusCode;
+ return mintResult;
}
- return false;
+ mintedFox = false;
+ return null;
}
catch (Exception ex)
{
- Debug.Log($"Failed to mint coins: {ex.Message}");
- return false;
+ Debug.Log($"Failed to mint fox: {ex.Message}");
+ mintedFox = false;
+ return null;
}
}
@@ -221,16 +184,7 @@ private void ShowCheckoutWallet(bool show)
private void ShowMintingMessage()
{
ShowCheckoutWallet(false);
- // Get number of coins col
- int numCoins = GetNumCoinsCollected();
- if (numCoins > 0)
- {
- m_Title.text = $"Let's mint the {numCoins} coin{(numCoins > 1 ? "s" : "")} you've collected and a fox to your wallet";
- }
- else
- {
- m_Title.text = "Let's mint a fox to your wallet!";
- }
+ m_Title.text = "Let's mint a fox to your wallet!";
}
///
@@ -245,15 +199,7 @@ private int GetNumCoinsCollected()
private void ShowMintedMessage()
{
ShowCheckoutWallet(true);
- int numCoins = GetNumCoinsCollected();
- if (numCoins > 0)
- {
- m_Title.text = $"You now own {numCoins} coin{(numCoins > 1 ? "s" : "")} and a fox";
- }
- else
- {
- m_Title.text = "You now own a fox!";
- }
+ m_Title.text = "You now own a fox!";
}
private async void OnWalletClicked()
@@ -261,7 +207,7 @@ 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");
+ Application.OpenURL($"https://sandbox.immutascan.io/address/{address}?tab=1");
}
}
}
diff --git a/Assets/Shared/Scripts/UI/SetupWalletScreen.cs b/Assets/Shared/Scripts/UI/SetupWalletScreen.cs
index b8722cc51..58780db0f 100644
--- a/Assets/Shared/Scripts/UI/SetupWalletScreen.cs
+++ b/Assets/Shared/Scripts/UI/SetupWalletScreen.cs
@@ -55,9 +55,15 @@ private async void SetupWallet()
ShowSuccess(false);
// Set up provider
- await Passport.Instance.ConnectEvm();
- // Set up wallet (includes creating a wallet for new players)
- await Passport.Instance.ZkEvmRequestAccounts();
+ await Passport.Instance.ConnectImx();
+
+ Debug.Log("Checking if wallet is registered offchain...");
+ bool isRegistered = await Passport.Instance.IsRegisteredOffchain();
+ if (!isRegistered)
+ {
+ Debug.Log("Registering wallet offchain...");
+ await Passport.Instance.RegisterOffchain();
+ }
m_Title.text = "Your wallet has been successfully set up!";
ShowLoading(false);
@@ -66,8 +72,8 @@ private async void SetupWallet()
}
catch (Exception ex)
{
- // Failed to set up wallet, let the player try again
- Debug.Log($"Failed to set up wallet: {ex.Message}");
+ // Failed to create wallet, let the player try again
+ Debug.Log($"Failed to create wallet: {ex.Message}");
ShowLoading(false);
ShowError(true);
ShowSuccess(false);
diff --git a/Assets/Shared/Scripts/UI/UnlockedSkinScreen.cs b/Assets/Shared/Scripts/UI/UnlockedSkinScreen.cs
index cab7aec47..059e2e25b 100644
--- a/Assets/Shared/Scripts/UI/UnlockedSkinScreen.cs
+++ b/Assets/Shared/Scripts/UI/UnlockedSkinScreen.cs
@@ -5,10 +5,28 @@
using UnityEngine.UI;
using System;
using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading;
using System.Threading.Tasks;
+using Cysharp.Threading.Tasks;
using Immutable.Passport;
using Immutable.Passport.Model;
+
+[Serializable]
+public class GetAssetsResponse
+{
+ public Asset[] result;
+}
+
+[Serializable]
+public class Asset
+{
+ public string id;
+ public string token_id;
+ public string token_address;
+}
+
namespace HyperCasual.Runner
{
///
@@ -47,7 +65,7 @@ public CraftSkinState? CraftState
get => m_CraftState;
set
{
- CraftState = value;
+ m_CraftState = value;
switch (m_CraftState)
{
case CraftSkinState.Crafting:
@@ -115,31 +133,59 @@ public async void OnEnable()
private async void Craft()
{
- CraftState = CraftSkinState.Crafting;
+ try {
+ m_CraftState = CraftSkinState.Crafting;
- // Burn tokens and mint a new skin i.e. crafting a skin
- TransactionReceiptResponse response = await Passport.Instance.ZkEvmSendTransactionWithConfirmation(new TransactionRequest()
- {
- to = "YOUR_IMMUTABLE_RUNNER_TOKEN_CONTRACT_ADDRESS", // Immutable Runner Token contract address
- data = "0x1e957f1e", // Call craftSkin() in the contract
- value = "0"
- });
- Debug.Log($"Craft transaction hash: {response.transactionHash}");
+ // burn
+ Asset[] assets = await GetAssets();
+ if (assets.Length == 0)
+ {
+ Debug.Log("No assets to burn");
+ m_CraftState = CraftSkinState.Failed;
+ return;
+ }
- if (response.status != "1")
- {
+ CreateTransferResponseV1 transferResult = await Passport.Instance.ImxTransfer(
+ new UnsignedTransferRequest("ERC721", "1", "0x0000000000000000000000000000000000000000", assets[0].token_id, assets[0].token_address)
+ );
+ Debug.Log($"Transfer(id={transferResult.transfer_id} receiver={transferResult.receiver} status={transferResult.status})");
+
+ m_CraftState = CraftSkinState.Crafted;
+
+ // If successfully crafted skin and this screen is visible, go to collect skin screen
+ // otherwise it will be picked in the OnEnable function above when this screen reappears
+ if (m_CraftState == CraftSkinState.Crafted && gameObject.active)
+ {
+ CollectSkin();
+ }
+ } catch (Exception ex) {
+ Debug.Log($"Failed to craft skin: {ex.Message}");
m_CraftState = CraftSkinState.Failed;
- return;
}
+ }
- CraftState = CraftSkinState.Crafted;
+ private async UniTask GetAssets()
+ {
+ const string collection = "0xcf77af96b269169f149b3c23230e103bda67fd0c";
+ string address = await Passport.Instance.GetAddress();
+ Debug.Log($"Wallet address: {address}");
- // If successfully crafted skin and this screen is visible, go to collect skin screen
- // otherwise it will be picked in the OnEnable function above when this screen reappears
- if (m_CraftState == CraftSkinState.Crafted && gameObject.active)
+ if (address != null)
{
- CollectSkin();
+ using var client = new HttpClient();
+ string url = $"https://api.sandbox.x.immutable.com/v1/assets?user={address}&collection={collection}&status=imx";
+ using var req = new HttpRequestMessage(HttpMethod.Get, url);
+ using var res = await client.SendAsync(req);
+
+ // Parse JSON and extract token_id
+ string content = await res.Content.ReadAsStringAsync();
+ Debug.Log($"Get Assets response: {content}");
+
+ GetAssetsResponse body = JsonUtility.FromJson(content);
+ Debug.Log($"Get Assets result: {body.result.Length}");
+ return body.result;
}
+ return null;
}
private void CollectSkin()
@@ -150,9 +196,9 @@ private void CollectSkin()
private void OnCraftButtonClicked()
{
- m_NextLevelEvent.Raise();
// Craft in the background, while the player plays the next level
Craft();
+ // m_NextLevelEvent.Raise();
}
private void OnTryAgainButtonClicked()
diff --git a/mint-backend/dest/index.js b/mint-backend/dest/index.js
index 2748d0bcf..e438a2dd5 100644
--- a/mint-backend/dest/index.js
+++ b/mint-backend/dest/index.js
@@ -3,12 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
+exports.nextTokenId = void 0;
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const http_1 = __importDefault(require("http"));
const ethers_1 = require("ethers");
+const utils_1 = require("ethers/lib/utils");
+const providers_1 = require("@ethersproject/providers");
+const keccak256_1 = require("@ethersproject/keccak256");
+const strings_1 = require("@ethersproject/strings");
const morgan_1 = __importDefault(require("morgan"));
const dotenv_1 = __importDefault(require("dotenv"));
+const sdk_1 = require("@imtbl/sdk");
dotenv_1.default.config();
const app = (0, express_1.default)();
app.use((0, morgan_1.default)('dev')); // Logging
@@ -16,72 +22,118 @@ app.use(express_1.default.urlencoded({ extended: false })); // Parse request
app.use(express_1.default.json()); // Handle JSON
app.use((0, cors_1.default)()); // Enable CORS
const router = express_1.default.Router();
-const zkEvmProvider = new ethers_1.JsonRpcProvider('https://rpc.testnet.immutable.com');
// Contract addresses
const foxContractAddress = process.env.FOX_CONTRACT_ADDRESS;
-const tokenContractAddress = process.env.TOKEN_CONTRACT_ADDRESS;
// Private key of wallet with minter role
const privateKey = process.env.PRIVATE_KEY;
-const gasOverrides = {
- // Use parameter to set tip for EIP1559 transaction (gas fee)
- maxPriorityFeePerGas: 10e9, // 10 Gwei. This must exceed minimum gas fee expectation from the chain
- maxFeePerGas: 15e9, // 15 Gwei
-};
// Mint Immutable Runner Fox
router.post('/mint/fox', async (req, res) => {
+ if (!foxContractAddress || !privateKey) {
+ res.writeHead(500);
+ res.end();
+ return;
+ }
try {
- if (foxContractAddress && privateKey) {
- // Get the address to mint the fox to
- let to = req.body.to ?? null;
- // Get the quantity to mint if specified, default is one
- let quantity = parseInt(req.body.quantity ?? "1");
- // Connect to wallet with minter role
- const signer = new ethers_1.Wallet(privateKey).connect(zkEvmProvider);
- // Specify the function to call
- const abi = ['function mintByQuantity(address to, uint256 quantity)'];
- // Connect contract to the signer
- const contract = new ethers_1.Contract(foxContractAddress, abi, signer);
- // Mints the number of tokens specified
- const tx = await contract.mintByQuantity(to, quantity, gasOverrides);
- await tx.wait();
- return res.status(200).json({});
+ // Set up IMXClient
+ const client = new sdk_1.x.IMXClient(sdk_1.x.imxClientConfig({ environment: sdk_1.config.Environment.SANDBOX }));
+ // Set up signer
+ const provider = (0, providers_1.getDefaultProvider)('sepolia');
+ // Connect to wallet with minter role
+ const ethSigner = new ethers_1.Wallet(privateKey, provider);
+ const tokenId = await (0, exports.nextTokenId)(foxContractAddress, client);
+ console.log('Next token ID: ', tokenId);
+ // recipient
+ const recipient = req.body.to ?? null;
+ // Set up request
+ let mintRequest = {
+ auth_signature: '', // This will be filled in later
+ contract_address: foxContractAddress,
+ users: [
+ {
+ user: ethSigner.address,
+ tokens: [
+ {
+ id: tokenId.toString(),
+ blueprint: 'onchain-metadata',
+ royalties: [
+ {
+ recipient: ethSigner.address,
+ percentage: 1,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ const message = (0, keccak256_1.keccak256)((0, strings_1.toUtf8Bytes)(JSON.stringify(mintRequest)));
+ const authSignature = await ethSigner.signMessage((0, utils_1.arrayify)(message));
+ mintRequest.auth_signature = authSignature;
+ console.log('sender', ethSigner.address, 'recipient', recipient, 'tokenId', tokenId);
+ // Mint
+ const mintResponse = await client.mint(ethSigner, mintRequest);
+ console.log('Mint response: ', mintResponse);
+ try {
+ // Transfer to recipient
+ const imxProviderConfig = new sdk_1.x.ProviderConfiguration({
+ baseConfig: {
+ environment: sdk_1.config.Environment.SANDBOX,
+ },
+ });
+ const starkPrivateKey = await sdk_1.x.generateLegacyStarkPrivateKey(ethSigner);
+ const starkSigner = sdk_1.x.createStarkSigner(starkPrivateKey);
+ const imxProvider = new sdk_1.x.GenericIMXProvider(imxProviderConfig, ethSigner, starkSigner);
+ const result = await imxProvider.transfer({
+ type: 'ERC721',
+ receiver: recipient,
+ tokenAddress: foxContractAddress,
+ tokenId: mintResponse.results[0].token_id,
+ });
+ console.log('Transfer result: ', result);
+ res.writeHead(200);
+ res.end(JSON.stringify(mintResponse.results[0]));
}
- else {
- return res.status(500).json({});
+ catch (error) {
+ console.log(error);
+ res.writeHead(400);
+ res.end(JSON.stringify({ message: 'Failed to transfer to user' }));
}
}
catch (error) {
console.log(error);
- return res.status(400).json({ message: "Failed to mint to user" });
+ res.writeHead(400);
+ res.end(JSON.stringify({ message: 'Failed to mint to user' }));
}
});
-// Mint Immutable Runner Token
-router.post('/mint/token', async (req, res) => {
+app.use('/', router);
+http_1.default.createServer(app).listen(3000, () => console.log('Listening on port 3000'));
+/**
+ * Helper function to get the next token id for a collection
+ */
+const nextTokenId = async (collectionAddress, imxClient) => {
try {
- if (tokenContractAddress && privateKey) {
- // Get the address to mint the token to
- let to = req.body.to ?? null;
- // Get the quantity to mint if specified, default is one
- let quantity = BigInt(req.body.quantity ?? "1");
- // Connect to wallet with minter role
- const signer = new ethers_1.Wallet(privateKey).connect(zkEvmProvider);
- // Specify the function to call
- const abi = ['function mint(address to, uint256 quantity)'];
- // Connect contract to the signer
- const contract = new ethers_1.Contract(tokenContractAddress, abi, signer);
- // Mints the number of tokens specified
- const tx = await contract.mint(to, quantity, gasOverrides);
- await tx.wait();
- return res.status(200).json({});
- }
- else {
- return res.status(500).json({});
- }
+ let remaining = 0;
+ let cursor;
+ let tokenId = 0;
+ do {
+ // eslint-disable-next-line no-await-in-loop
+ const assets = await imxClient.listAssets({
+ collection: collectionAddress,
+ cursor,
+ });
+ remaining = assets.remaining;
+ cursor = assets.cursor;
+ for (const asset of assets.result) {
+ const id = parseInt(asset.token_id, 10);
+ if (id > tokenId) {
+ tokenId = id;
+ }
+ }
+ } while (remaining > 0);
+ return tokenId + 1;
}
catch (error) {
- console.log(error);
- return res.status(400).json({ message: "Failed to mint to user" });
+ return 0;
}
-});
-app.use('/', router);
-http_1.default.createServer(app).listen(3000, () => console.log(`Listening on port 3000`));
+};
+exports.nextTokenId = nextTokenId;
diff --git a/mint-backend/package.json b/mint-backend/package.json
index 71406e648..61c35f30a 100644
--- a/mint-backend/package.json
+++ b/mint-backend/package.json
@@ -10,24 +10,25 @@
},
"devDependencies": {
"@types/cors": "^2.8.13",
- "@types/express": "^4.17.17",
+ "@types/express": "^5.0.0",
"@types/morgan": "^1.9.9",
- "@typescript-eslint/eslint-plugin": "^7.11.0",
- "eslint": "^8.56.0",
+ "@typescript-eslint/eslint-plugin": "^8.8.0",
+ "eslint": "^9.11.1",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
- "eslint-plugin-import": "^2.29.1",
- "eslint-plugin-jsx-a11y": "^6.8.0",
- "eslint-plugin-react": "^7.34.2",
+ "eslint-plugin-import": "^2.30.0",
+ "eslint-plugin-jsx-a11y": "^6.10.0",
+ "eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^4.6.2",
- "typescript": "^5.4.3",
- "typescript-eslint": "^7.11.0"
+ "typescript": "^5.6.2",
+ "typescript-eslint": "^8.8.0"
},
"dependencies": {
+ "@ethersproject/providers": "^5.7.2",
+ "@imtbl/sdk": "1.55.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
- "ethers": "^6.11.1",
- "express": "^4.20.0",
+ "express": "^5.0.0",
"morgan": "^1.10.0"
}
-}
\ No newline at end of file
+}
diff --git a/mint-backend/src/index.ts b/mint-backend/src/index.ts
index 4d4c11037..240ed51e2 100644
--- a/mint-backend/src/index.ts
+++ b/mint-backend/src/index.ts
@@ -6,9 +6,14 @@ import express, {
} from 'express';
import cors from 'cors';
import http from 'http';
-import { JsonRpcProvider, Wallet, Contract } from 'ethers';
+import { Wallet } from 'ethers';
+import { arrayify } from 'ethers/lib/utils';
+import { getDefaultProvider } from '@ethersproject/providers';
+import { keccak256 } from '@ethersproject/keccak256';
+import { toUtf8Bytes } from '@ethersproject/strings';
import morgan from 'morgan';
import dotenv from 'dotenv';
+import { x, config } from '@imtbl/sdk';
dotenv.config();
@@ -19,89 +24,144 @@ app.use(express.json()); // Handle JSON
app.use(cors()); // Enable CORS
const router: Router = express.Router();
-const zkEvmProvider = new JsonRpcProvider('https://rpc.testnet.immutable.com');
-
// Contract addresses
const foxContractAddress = process.env.FOX_CONTRACT_ADDRESS;
-const tokenContractAddress = process.env.TOKEN_CONTRACT_ADDRESS;
+
// Private key of wallet with minter role
const privateKey = process.env.PRIVATE_KEY;
-const gasOverrides = {
- // Use parameter to set tip for EIP1559 transaction (gas fee)
- maxPriorityFeePerGas: 10e9, // 10 Gwei. This must exceed minimum gas fee expectation from the chain
- maxFeePerGas: 15e9, // 15 Gwei
-};
-
// Mint Immutable Runner Fox
router.post('/mint/fox', async (req: Request, res: Response) => {
- try {
- if (foxContractAddress && privateKey) {
- // Get the address to mint the fox to
- let to: string = req.body.to ?? null;
- // Get the quantity to mint if specified, default is one
- let quantity = parseInt(req.body.quantity ?? '1');
-
- // Connect to wallet with minter role
- const signer = new Wallet(privateKey).connect(zkEvmProvider);
-
- // Specify the function to call
- const abi = ['function mintByQuantity(address to, uint256 quantity)'];
- // Connect contract to the signer
- const contract = new Contract(foxContractAddress, abi, signer);
-
- // Mints the number of tokens specified
- const tx = await contract.mintByQuantity(to, quantity, gasOverrides);
- await tx.wait();
-
- return res.status(200).json({});
- } else {
- return res.status(500).json({});
- }
-
- } catch (error) {
- console.log(error);
- return res.status(400).json({ message: 'Failed to mint to user' });
+ if (!foxContractAddress || !privateKey) {
+ res.writeHead(500);
+ res.end();
+ return;
}
-},
-);
-// Mint Immutable Runner Token
-router.post('/mint/token', async (req: Request, res: Response) => {
try {
- if (tokenContractAddress && privateKey) {
- // Get the address to mint the token to
- let to: string = req.body.to ?? null;
- // Get the quantity to mint if specified, default is one
- let quantity = BigInt(req.body.quantity ?? '1');
-
- // Connect to wallet with minter role
- const signer = new Wallet(privateKey).connect(zkEvmProvider);
-
- // Specify the function to call
- const abi = ['function mint(address to, uint256 quantity)'];
- // Connect contract to the signer
- const contract = new Contract(tokenContractAddress, abi, signer);
-
- // Mints the number of tokens specified
- const tx = await contract.mint(to, quantity, gasOverrides);
- await tx.wait();
-
- return res.status(200).json({});
- } else {
- return res.status(500).json({});
+ // Set up IMXClient
+ const client = new x.IMXClient(
+ x.imxClientConfig({ environment: config.Environment.SANDBOX })
+ );
+
+ // Set up signer
+ const provider = getDefaultProvider('sepolia');
+
+ // Connect to wallet with minter role
+ const ethSigner = new Wallet(privateKey, provider);
+
+ const tokenId = await nextTokenId(foxContractAddress, client);
+ console.log('Next token ID: ', tokenId);
+
+ // recipient
+ const recipient: string = req.body.to ?? null;
+
+ // Set up request
+ let mintRequest = {
+ auth_signature: '', // This will be filled in later
+ contract_address: foxContractAddress,
+ users: [
+ {
+ user: ethSigner.address,
+ tokens: [
+ {
+ id: tokenId.toString(),
+ blueprint: 'onchain-metadata',
+ royalties: [
+ {
+ recipient: ethSigner.address,
+ percentage: 1,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ const message = keccak256(toUtf8Bytes(JSON.stringify(mintRequest)));
+ const authSignature = await ethSigner.signMessage(arrayify(message));
+ mintRequest.auth_signature = authSignature;
+
+ console.log('sender', ethSigner.address, 'recipient', recipient, 'tokenId', tokenId);
+
+ // Mint
+ const mintResponse = await client.mint(ethSigner, mintRequest);
+ console.log('Mint response: ', mintResponse);
+
+ try {
+ // Transfer to recipient
+ const imxProviderConfig = new x.ProviderConfiguration({
+ baseConfig: {
+ environment: config.Environment.SANDBOX,
+ },
+ });
+ const starkPrivateKey = await x.generateLegacyStarkPrivateKey(ethSigner);
+ const starkSigner = x.createStarkSigner(starkPrivateKey);
+ const imxProvider = new x.GenericIMXProvider(
+ imxProviderConfig,
+ ethSigner,
+ starkSigner
+ );
+ const result = await imxProvider.transfer({
+ type: 'ERC721',
+ receiver: recipient,
+ tokenAddress: foxContractAddress,
+ tokenId: mintResponse.results[0].token_id,
+ });
+ console.log('Transfer result: ', result);
+
+ res.writeHead(200);
+ res.end(JSON.stringify(mintResponse.results[0]));
+ } catch (error) {
+ console.log(error);
+ res.writeHead(400);
+ res.end(JSON.stringify({ message: 'Failed to transfer to user' }));
}
-
} catch (error) {
console.log(error);
- return res.status(400).json({ message: 'Failed to mint to user' });
+ res.writeHead(400);
+ res.end(JSON.stringify({ message: 'Failed to mint to user' }));
}
-},
-);
+});
app.use('/', router);
http.createServer(app).listen(
3000,
() => console.log('Listening on port 3000'),
-);
\ No newline at end of file
+);
+
+/**
+ * Helper function to get the next token id for a collection
+ */
+export const nextTokenId = async (
+ collectionAddress: string,
+ imxClient: x.IMXClient
+) => {
+ try {
+ let remaining = 0;
+ let cursor: string | undefined;
+ let tokenId = 0;
+
+ do {
+ // eslint-disable-next-line no-await-in-loop
+ const assets = await imxClient.listAssets({
+ collection: collectionAddress,
+ cursor,
+ });
+ remaining = assets.remaining;
+ cursor = assets.cursor;
+
+ for (const asset of assets.result) {
+ const id = parseInt(asset.token_id, 10);
+ if (id > tokenId) {
+ tokenId = id;
+ }
+ }
+ } while (remaining > 0);
+
+ return tokenId + 1;
+ } catch (error) {
+ return 0;
+ }
+};
\ No newline at end of file
diff --git a/mint-backend/tsconfig.json b/mint-backend/tsconfig.json
index c8b808b5b..6fd82388b 100644
--- a/mint-backend/tsconfig.json
+++ b/mint-backend/tsconfig.json
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
- "module": "Node16",
+ "module": "CommonJS",
"esModuleInterop": true,
"outDir": "./dest",
"skipLibCheck": true,