diff --git a/deploy/env.example b/deploy/env.example index 68d5b75..7aac511 100644 --- a/deploy/env.example +++ b/deploy/env.example @@ -11,7 +11,9 @@ DEMO_ACCOUNT_ONE_MNEMONIC="//Dave" DEMO_ACCOUNT_TWO_MNEMONIC="//Eve" DEMO_ACCOUNT_THREE_MNEMONIC="//Ferdie" +# Connection to the blockchain WS_PROVIDER_URL="ws://127.0.0.1:52279" +DATAGATE_URL="http://127.0.0.1:18890" # Amount to transfer to app agents -TRANSFER_AMOUNT=10 \ No newline at end of file +TRANSFER_AMOUNT=1000 diff --git a/deploy/index.js b/deploy/index.js index 2bde707..ce3730f 100644 --- a/deploy/index.js +++ b/deploy/index.js @@ -4,9 +4,11 @@ const dotenv = require("dotenv"); const fs = require('fs'); const path = require('path'); const { create_app_agent } = require('./utils/app_agent'); -const { create_app_agent_fungible_token } = require('./utils/fungible'); -const { create_app_agent_nft_collection } = require('./utils/nft'); -const { create_app_agent_nft_token } = require('./utils/nft'); +const { create_fungible_tokens, set_metadata_and_mint_fungible_token, create_token_transfer } = require('./utils/fungible'); +const { create_nft_collections, set_metadata_and_mint_nft, create_nft_transfers } = require('./utils/nft'); +const { processSignedTransaction, processSignedBatchTransaction } = require('./utils/utils'); + +const startTime = Date.now(); const aws_s3_assets_path = path.join(__dirname, '..', 'aws_s3_assets'); const game_a_path = path.join(aws_s3_assets_path, 'game-a/'); @@ -18,16 +20,16 @@ const game_folders = [game_a_path, game_b_path, game_c_path]; dotenv.config(); async function main() { - // Read WS_PROVIDER_URL from .env file + console.log("Read WS_PROVIDER_URL from .env file"); const wsProviderUrl = process.env.WS_PROVIDER_URL; if (!wsProviderUrl) { throw new Error("WS_PROVIDER_URL is not set in the .env file"); } - // Create a provider with the URL from .env + console.log("Create a provider with the URL from .env"); const provider = new WsProvider(wsProviderUrl); - // Instantiate the API with the provider + console.log("Instantiate the API with the provider"); const api = await ApiPromise.create({ provider, types: { @@ -44,125 +46,143 @@ async function main() { } }); - // Construct the keyring + console.log("Construct the keyring"); const keyring = new Keyring({ type: "sr25519" }); - // Load accounts from .env file + console.log("Load accounts from .env file"); const faucetAccount = keyring.addFromUri(process.env.FAUCET_ACCOUNT_MNEMONIC); - const appAgentOne = keyring.addFromUri(process.env.APP_AGENT_OWNER_ONE_MNEMONIC); - const appAgentTwo = keyring.addFromUri(process.env.APP_AGENT_OWNER_TWO_MNEMONIC); - const appAgentThree = keyring.addFromUri(process.env.APP_AGENT_OWNER_THREE_MNEMONIC); - const appAgentOwners = [appAgentOne, appAgentTwo, appAgentThree]; + const appAgentOneOwner = keyring.addFromUri(process.env.APP_AGENT_OWNER_ONE_MNEMONIC); + const appAgentTwoOwner = keyring.addFromUri(process.env.APP_AGENT_OWNER_TWO_MNEMONIC); + const appAgentThreeOwner = keyring.addFromUri(process.env.APP_AGENT_OWNER_THREE_MNEMONIC); + const appAgentOwners = [appAgentOneOwner, appAgentTwoOwner, appAgentThreeOwner]; const demo_user_one = keyring.addFromUri(process.env.DEMO_ACCOUNT_ONE_MNEMONIC); const demo_user_two = keyring.addFromUri(process.env.DEMO_ACCOUNT_TWO_MNEMONIC); const demo_user_three = keyring.addFromUri(process.env.DEMO_ACCOUNT_THREE_MNEMONIC); const transferAmount = parseInt(process.env.TRANSFER_AMOUNT) * 1e12; + const demotransferAmount = parseInt(process.env.TRANSFER_AMOUNT) * 1e10; + + console.log("Start to initialise the owners of the app agents"); + // const transfers = [ + // api.tx.balances.transferKeepAlive(appAgentOneOwner.address, transferAmount.toString()), + // api.tx.balances.transferKeepAlive(appAgentTwoOwner.address, transferAmount.toString()), + // api.tx.balances.transferKeepAlive(appAgentThreeOwner.address, transferAmount.toString()), + // api.tx.balances.transferKeepAlive(demo_user_one.address, demotransferAmount.toString()), + // api.tx.balances.transferKeepAlive(demo_user_two.address, demotransferAmount.toString()), + // api.tx.balances.transferKeepAlive(demo_user_three.address, demotransferAmount.toString()), + // ]; + + // console.log("Send the batch of transfers"); + // await processSignedBatchTransaction(api, faucetAccount, api.tx.utility.batchAll(transfers)); + // await create_balance_transfers(api, demo_user_one, demo_user_two); + // await create_balance_transfers(api, demo_user_three, demo_user_one); + + console.log("Traverse the game folders and collect entity data"); + const gameData = collectGameData(game_folders); + + // array of fungible ids + let fungibles = []; + + // array of { collectionId: collectionId, tokenId: tokenId} + let collections = []; + + console.log("Starting to process game data"); + for (const [gameIndex, game] of gameData.entries()) { + console.log(`Processing game ${gameIndex + 1}`); + const appAgentOwner = appAgentOwners[gameIndex]; + + // Create app agent and set metadata + console.log(`Creating app agent for game ${gameIndex + 1}`); + const appAgentId = await create_app_agent(api, appAgentOwner, game.appAgent.metadataUrl); + console.log(`App agent created for game ${gameIndex + 1}: ${appAgentId}`); + + // Create and configure fungible tokens + if (game.fungibles.length > 0) { + console.log(`Creating fungible tokens for game ${gameIndex + 1}`); + const fungibleIds = await create_fungible_tokens(api, appAgentOwner, appAgentId, game.fungibles.length); + fungibles = [...fungibles, ...fungibleIds]; + console.log(`Setting metadata and minting fungible tokens for game ${gameIndex + 1}`); + await set_metadata_and_mint_fungible_token(api, appAgentOwner, appAgentId, fungibleIds, game.fungibles.map(f => f.metadataUrl), demo_user_one, game.fungibles.map(f => f.decimals)); + console.log(`Fungible tokens created and configured for game ${gameIndex + 1}`); + } - // Create a batch of transfers - const transfers = [ - api.tx.balances.transferKeepAlive(appAgentOne.address, transferAmount.toString()), - api.tx.balances.transferKeepAlive(appAgentTwo.address, transferAmount.toString()), - api.tx.balances.transferKeepAlive(appAgentThree.address, transferAmount.toString()) - ]; - - // Send the batch of transfers - await new Promise((resolve, reject) => { - api.tx.utility - .batchAll(transfers) - .signAndSend(faucetAccount, ({ status, events }) => { - if (status.isInBlock || status.isFinalized) { - events.forEach(({ event }) => { - if (api.events.balances.Transfer.is(event)) { - const [from, to, amount] = event.data; - console.log(`Transferred ${amount.toNumber() / 1e12} tokens from ${from.toString()} to ${to.toString()}`); - } - }); - console.log("Initial transfers completed successfully"); - resolve(); - } - }) - .catch(reject); - }); - - // traverse the game folders and create app-agents and assets for each game - for (const [index, folder] of game_folders.entries()) { - console.log("folder:", folder); - let appagentId = null; - let appAgentOwner = appAgentOwners[index]; - - // Search for folders starting with app-agent- and print all files - const subFolders = fs.readdirSync(folder); - - for (const subFolder of subFolders) { - console.log("subFolder:", subFolder); - if (subFolder.startsWith('app-agent-')) { - const folderPath = path.join(folder, subFolder); - console.log("folderPath:", folderPath); + // Create and configure NFT collections and tokens + console.log(`Creating NFT collections for game ${gameIndex + 1}`); + const collectionIds = await create_nft_collections(api, appAgentOwner, appAgentId, game.nftCollections.length); + console.log(`NFT collections created for game ${gameIndex + 1}:`, collectionIds); + for (let i = 0; i < game.nftCollections.length; i++) { + console.log(`Setting metadata and minting NFTs for collection ${collectionIds[i]} of game ${gameIndex + 1}`); + let nftInfo = await set_metadata_and_mint_nft(api, appAgentOwner, appAgentId, collectionIds[i], game.nftCollections[i], demo_user_one.address); + collections = [...collections, ...nftInfo]; + } + console.log(`NFT collections created and configured for game ${gameIndex + 1}`); + } - let metadataUrl = readFilesInDirectory(folderPath); - console.log("metadataUrl:", metadataUrl); + console.log("All games processed. Fungibles:", fungibles); + console.log("Collections:", collections); - if (metadataUrl) { - appagentId = await create_app_agent(api, appAgentOwner, metadataUrl); - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate - } - } + console.log("Create demo transfers for fungibles"); + for (const fungible of fungibles) { + await create_token_transfer(api, fungible, demo_user_one, [demo_user_two, demo_user_three], 10); + await create_token_transfer(api, fungible, demo_user_two, [demo_user_one, demo_user_three], 5); + } - else if (subFolder.startsWith('fungible-')) { - console.log("fungible-token folder detected"); - const folderPath = path.join(folder, subFolder); - console.log("folderPath:", folderPath); + console.log("Create demo transfers for NFTs"); + for (const collection of collections) { + const recipient = Math.random() < 0.5 ? demo_user_three : demo_user_two; + await create_nft_transfers(api, collection.collectionId, collection.tokenId, demo_user_one, recipient); + } +} - let metadataUrl = readFilesInDirectory(folderPath); - console.log("metadataUrl:", metadataUrl); - create_app_agent_fungible_token(api, appAgentOwner, appagentId, demo_user_one, demo_user_two, metadataUrl); +function collectGameData(gameFolders) { + return gameFolders.map(gameFolder => { + const gameData = { + appAgent: null, + // array of { metadataUrl: metadataUrl, decimals: decimals } + fungibles: [], + // array of { metadataUrl: metadataUrl, tokens: [{ metadataUrl: metadataUrl}]} + nftCollections: [] + }; - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate - } + const subFolders = fs.readdirSync(gameFolder); - if (subFolder.startsWith('nft-collection')) { - console.log("nft-collection folder detected"); - const folderPath = path.join(folder, subFolder); - console.log("folderPath:", folderPath); + for (const subFolder of subFolders) { + const folderPath = path.join(gameFolder, subFolder); + if (subFolder.startsWith('app-agent-')) { + gameData.appAgent = { metadataUrl: getObjectMetadataURL(folderPath).url }; + } else if (subFolder.startsWith('fungible-')) { + gameData.fungibles.push({ metadataUrl: getObjectMetadataURL(folderPath).url, decimals: getObjectMetadataURL(folderPath).decimals }); + } else if (subFolder.startsWith('nft-collection')) { + const collection = { metadataUrl: null, tokens: [] }; const subsubFolders = fs.readdirSync(folderPath); - let collection_id = null; - for (const subsubFolder of subsubFolders) { + const subFolderPath = path.join(folderPath, subsubFolder); if (subsubFolder.startsWith('nft-collection')) { - const nftCollectionPath = path.join(folderPath, subsubFolder); - console.log("nftCollectionPath:", nftCollectionPath); - let metadataUrl = readFilesInDirectory(nftCollectionPath); - console.log("metadataUrl:", metadataUrl); - - collection_id = await create_app_agent_nft_collection(api, appAgentOwner, appagentId, metadataUrl); - } - - else if (subsubFolder.startsWith('nft-token')) { - const nftItemPath = path.join(folderPath, subsubFolder); - console.log("nftItemPath:", nftItemPath); - let metadataUrl = readFilesInDirectory(nftItemPath); - console.log("metadataUrl:", metadataUrl); - - await create_app_agent_nft_token(api, appAgentOwner, appagentId, collection_id, metadataUrl, demo_user_one, demo_user_three); + collection.metadataUrl = getObjectMetadataURL(subFolderPath).url; + } else if (subsubFolder.startsWith('nft-token')) { + collection.tokens.push({ metadataUrl: getObjectMetadataURL(subFolderPath).url }); } } - - } - - else { - console.log("unknown folder detected, ignoring..."); + gameData.nftCollections.push(collection); } } - } + + return gameData; + }); } -// Function to read all files in a directory -function readFilesInDirectory(directory) { +/** + * Function searches for the json file with object metadata. + * And calculates the metadata URL based on the file path. + * + * @param {string} directory - The directory where the metadata file is stored. + * @return {string|null} The metadata URL if found, otherwise null. + */ +function getObjectMetadataURL(directory) { const files = fs.readdirSync(directory); const jsonFiles = files.filter(file => file.endsWith('.json')); @@ -170,22 +190,40 @@ function readFilesInDirectory(directory) { const filePath = path.join(directory, file); const stats = fs.statSync(filePath); if (stats.isFile()) { - console.log(`File: ${filePath}`); + const fileContent = fs.readFileSync(filePath, 'utf8'); + const jsonData = JSON.parse(fileContent); + // get the decimals from the fungible metadata if it exists + let decimals = jsonData.traits.fungible ? jsonData.traits.fungible.decimals : null; - // Generate the URL based on the folder structure const relativePath = path.relative(aws_s3_assets_path, filePath); const url = `https://trait-wallet-demo-account.trait.tech/${relativePath.replace(/\\/g, '/')}`; - console.log(`Generated URL: ${url}`); - - // Return the URL instead of reading the file content - return url; + // console.log(`Generated URL: ${url}`); + return { url, decimals }; } } return null; } +async function create_balance_transfers(api, token_recipient, token_recipient_two) { + console.log("Generate free transfers between the two users"); + + for (let i = 0; i < 2; i++) { + let tx = api.tx.playerTransfers.submitTransferBalances( + token_recipient_two.address, + 1000000 + ); + await processSignedTransaction(token_recipient, tx); + } + console.log(`Free transfer created and confirmed`); +} + main() .catch(console.error) - .finally(() => process.exit()); + .finally(() => { + const endTime = Date.now(); + const executionTime = (endTime - startTime) / 1000; // Convert to seconds + console.log(`Total execution time: ${executionTime.toFixed(2)} seconds`); + process.exit(); + }); diff --git a/deploy/package-lock.json b/deploy/package-lock.json index 3708f54..20d817b 100644 --- a/deploy/package-lock.json +++ b/deploy/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@polkadot/api": "^12.4.2", "@polkadot/keyring": "^13.0.2", + "axios": "^1.7.7", "dotenv": "^16.4.5" } }, @@ -654,11 +655,37 @@ "undici-types": "~6.19.2" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -683,6 +710,14 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -721,6 +756,38 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -737,6 +804,25 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mock-socket": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", @@ -806,6 +892,11 @@ "node": ">= 8" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", diff --git a/deploy/package.json b/deploy/package.json index c5095d4..9856d84 100644 --- a/deploy/package.json +++ b/deploy/package.json @@ -11,6 +11,7 @@ "dependencies": { "@polkadot/api": "^12.4.2", "@polkadot/keyring": "^13.0.2", + "axios": "^1.7.7", "dotenv": "^16.4.5" } } diff --git a/deploy/utils/app_agent.js b/deploy/utils/app_agent.js index 366bff7..4fd18ed 100644 --- a/deploy/utils/app_agent.js +++ b/deploy/utils/app_agent.js @@ -1,46 +1,33 @@ -const { ApiPromise, WsProvider } = require("@polkadot/api"); -const { Keyring } = require("@polkadot/keyring"); -const { encodeNamed } = require("./keyless"); +const { processSignedTransaction } = require("./utils"); async function create_app_agent(api, appAgentOwner, metadataUrl) { - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate + return new Promise(async (resolve, reject) => { + try { + console.log("Start to create AppAgent for the owner: " + appAgentOwner.address); - let appagentId; - await new Promise((resolve, reject) => { - api.tx.appAgents.createAppAgent() - .signAndSend(appAgentOwner, ({ status, events }) => { - if (status.isInBlock || status.isFinalized) { - events.forEach(({ event }) => { - if (api.events.appAgents.AppAgentCreated.is(event)) { - const [newAppAgentId, ownerAddress] = event.data; - console.log(`App agent created: ID ${newAppAgentId.toString()} for owner ${ownerAddress.toString()}`); - appagentId = newAppAgentId; - resolve(); - } - }); - } - }) - .catch(reject); - }); - - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate + let appagentId; - // create the transaction to set the metadata - let set_metadata_tx = api.tx.appAgents.setAppAgentMetadata( - appagentId, - metadataUrl - ); + let tx = api.tx.appAgents.createAppAgent(); + let events = await processSignedTransaction(appAgentOwner, tx); + // console.log("events: ", events); + for (const event of events) { + if (event.receipt.event_module === "AppAgents" && event.receipt.event_name === "AppAgentCreated") { + appagentId = event.attributes.app_agent_id.toString(); + } + } - // sign and send the transaction - await set_metadata_tx.signAndSend(appAgentOwner) - .then(() => { - console.log("Metadata URL set successfully"); - }) - .catch(err => { - console.error("Error setting metadata URL:", err); - }); + console.log("Create the transaction to set the metadata"); + let set_metadata_tx = api.tx.appAgents.setAppAgentMetadata( + appagentId, + metadataUrl + ); + await processSignedTransaction(appAgentOwner, set_metadata_tx); - return appagentId; + resolve(appagentId); + } catch (error) { + reject(error); + } + }); } module.exports = { diff --git a/deploy/utils/datagate.js b/deploy/utils/datagate.js new file mode 100644 index 0000000..ce124be --- /dev/null +++ b/deploy/utils/datagate.js @@ -0,0 +1,96 @@ +const dotenv = require("dotenv"); +const axios = require('axios'); +dotenv.config(); + +function buildDatagateUrl() { + + const datagateUrl = process.env.DATAGATE_URL; + if (!datagateUrl) { + throw new Error("DATAGATE_URL is not set in the .env file"); + } + return datagateUrl + "/history/events"; +} + +function checkEventOccurrence(transaction_hash, module_name, event_name) { + return new Promise(async (resolve, reject) => { + const apiUrl = buildDatagateUrl(); + + console.log("DATAGATE:Checking event occurrence for transaction:", transaction_hash); + + const requestBody = { + "block_receipt": { + "block_index": { + "max_index": "blockchain_head" + } + }, + "tx_receipt": { + "tx_hash": transaction_hash + }, + "event": { + "module_name": module_name, + "event_name": event_name + }, + "presentation": { + "sorting": "time_ascending" + } + }; + + try { + const response = await axios.post(apiUrl, requestBody); + + // console.log("DATAGATE:Response:", response.data); + + if (response.data && response.data.data && response.data.data.length > 0) { + resolve(true); + } else { + reject(new Error("Event not found")); + } + } catch (error) { + console.error('Error calling the API:', error); + reject(error); + } + }); +} + +function getAllEvents(transaction_hash) { + return new Promise(async (resolve, reject) => { + const apiUrl = buildDatagateUrl(); + + console.log("DATAGATE:Checking event occurrence for transaction:", transaction_hash); + + const requestBody = { + "block_receipt": { + "block_index": { + "max_index": "blockchain_head" + } + }, + "tx_receipt": { + "tx_hash": transaction_hash + }, + "presentation": { + "sorting": "time_ascending" + } + }; + + try { + const response = await axios.post(apiUrl, requestBody); + + // console.log("DATAGATE:Response:", response.data); + + if (response.data && response.data.data && response.data.data.length > 0) { + resolve(response.data.data); + } else { + reject(new Error("Event not found")); + } + } catch (error) { + console.error('Error calling the API:', error); + reject(error); + } + }); +} + +module.exports = { + checkEventOccurrence, + getAllEvents +}; + diff --git a/deploy/utils/fungible.js b/deploy/utils/fungible.js index bb5f3b0..1a2e4f3 100644 --- a/deploy/utils/fungible.js +++ b/deploy/utils/fungible.js @@ -1,121 +1,118 @@ -const { ApiPromise, WsProvider } = require("@polkadot/api"); -const { Keyring } = require("@polkadot/keyring"); const { encodeNamed } = require("./keyless"); - -async function create_app_agent_fungible_token(api, appAgentOwner, appAgentId, token_recipient, token_recipient_two, metadataUrl) { - // Create fungible token - let token_admin = encodeNamed(appAgentId, "asset-admi"); - - let create_fungible_token = api.tx.assets.create( - token_admin, - 1 - ); - - let create_fungible_token_ct = api.tx.addressPools.submitClearingTransaction( - appAgentId, - [[ - [ - { AppAgentId: appAgentId }, - create_fungible_token - ] - ]] - ); - - // Wait for the event and get the token ID - let token_id; - await create_fungible_token_ct.signAndSend(appAgentOwner, { nonce: -1 }, ({ events = [], status }) => { - if (status.isInBlock || status.isFinalized) { - events.forEach(({ event: { data, method, section } }) => { - if (section === 'assets' && method === 'Created') { - const tokenId = data[0].toString(); - console.log(`Fungible token created with ID: ${tokenId}`); - token_id = tokenId; +const { processClearingTransaction, processSignedTransaction } = require("./utils"); + +async function create_fungible_tokens(api, appAgentOwner, appAgentId, tokenCount) { + return new Promise(async (resolve, reject) => { + try { + console.log("Start to create fungible tokens for the AppAgent ID " + appAgentId); + console.log("Token count: ", tokenCount); + + let token_admin = encodeNamed(appAgentId, "asset-admi"); + let tokenIds = []; + + let atomics = []; + for (let i = 0; i < tokenCount; i++) { + let create_fungible_token_call = api.tx.assets.create(token_admin, 1); + let create_fungible_token_action = [{ AppAgentId: appAgentId }, create_fungible_token_call]; + let create_fungible_token_atomic = [create_fungible_token_action]; + atomics.push(create_fungible_token_atomic); + } + + let create_fungible_token_ct = api.tx.addressPools.submitClearingTransaction( + appAgentId, + atomics + ); + + let events = await processClearingTransaction(appAgentOwner, create_fungible_token_ct); + for (const event of events) { + if (event.receipt.event_module === 'Assets' && event.receipt.event_name === 'Created') { + tokenIds.push(event.attributes.asset_id.toString()); } - }); + } + + console.log("Generated token IDs: ", tokenIds); + if (tokenIds.length != tokenCount) { + throw new Error("Not all required fungibles were created"); + } + + console.log("Resolving promise with tokenIds:", tokenIds); + resolve(tokenIds); + } catch (error) { + console.error("Error creating fungible tokens:", error); + reject(error); } - }).catch((error) => { - console.error("Error creating fungible token:", error); }); +} + +function convertDecimalsToAmount(decimals, amount) { + if (decimals === undefined || decimals === null || decimals === 0) { + return amount; + } - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate - - // mint tokens to the app agent - const mint_tokens_call = api.tx.assets.mint( - token_id, - token_recipient.address, - 1000 - ); - - let set_metadata_call = api.tx.assets.setMetadata( - token_id, - metadataUrl - ); - - let set_metadata_ct = api.tx.addressPools.submitClearingTransaction( - appAgentId, - [[ - [ - { NamedAddress: token_admin }, - mint_tokens_call - ], - [ - { AppAgentId: appAgentId }, - set_metadata_call - ] - ]] - ); - - let batch_calls = [ - create_fungible_token_ct, - set_metadata_ct - ]; - - let batch_call = api.tx.utility.batch(batch_calls); - - await new Promise(resolve => setTimeout(resolve, 10000)); // wait for the previous tx to propogate - - await batch_call.signAndSend(appAgentOwner, { nonce: -1 }) - .then(() => { - console.log(`Fungible token ${token_id} configured.`); - }).catch((err) => { - console.log(err); - console.log(`Test failed : couldn't configure fungible token ${token_id}!`); - process.exit(1); - }); - - await create_token_transfers(api, token_id, token_recipient, token_recipient_two); + return amount * Math.pow(10, decimals); +} + +async function set_metadata_and_mint_fungible_token(api, appAgentOwner, appAgentId, tokenIds, metadataUrls, token_recipient, decimals) { + return new Promise(async (resolve, reject) => { + try { + console.log("Start to create fungible token for the AppAgent ID " + appAgentId); + console.log("Decimals: ", decimals); + console.log("Token IDs: ", tokenIds); + console.log("Metadata URLs: ", metadataUrls); + + let token_admin = encodeNamed(appAgentId, "asset-admi"); + + console.log("Create atomics to mint and set metadata for each token"); + let atomics = []; + for (let i = 0; i < tokenIds.length; i++) { + console.log(`Creating atomic for token ${tokenIds[i]}`); + let mint_token_call = api.tx.assets.mint(tokenIds[i], token_recipient.address, convertDecimalsToAmount(decimals[i], 1000)); + let mint_token_action = [{ NamedAddress: token_admin }, mint_token_call]; + let set_metadata_call = api.tx.assets.setMetadata(tokenIds[i], metadataUrls[i]); + let set_metadata_action = [{ AppAgentId: appAgentId }, set_metadata_call]; + token_atomic = [mint_token_action, set_metadata_action]; + atomics.push(token_atomic); + } + console.log(`Total atomics created: ${atomics.length}`); + + let configure_fungible_ct = api.tx.addressPools.submitClearingTransaction( + appAgentId, + atomics + ); + await processClearingTransaction(appAgentOwner, configure_fungible_ct); + console.log("Fungible tokens configured successfully"); + + resolve(); + } catch (error) { + console.error("Error setting metadata and minting fungible tokens:", error); + reject(error); + } + }); } -async function create_token_transfers(api, token_id, token_recipient, token_recipient_two) { - // generate 5 free transfers between the two users - let batch_calls_two = []; - for (let i = 0; i < 5; i++) { - let free_transfer_call = api.tx.playerTransfers.submitTransferAssets( +async function create_token_transfer(api, token_id, token_sender, token_recipients, amount) { + console.log("Generate free transfers between the two users"); + console.log("Token ID: ", token_id); + console.log("Token sender: ", token_sender.address); + + for (let i = 0; i < token_recipients.length; i++) { + + let tx = api.tx.playerTransfers.submitTransferAssets( token_id, - token_recipient_two.address, - 10 + token_recipients[i].address, + amount ); - batch_calls_two.push(free_transfer_call); - } - - let batch_call_two = api.tx.utility.batch(batch_calls_two); + await processSignedTransaction(token_sender, tx); - batch_call_two.signAndSend(token_recipient, { nonce: -1 }) - .then(() => { - console.log(`Free transfer created`); - }).catch((err) => { - console.log(err); - console.log(`Test failed : free transfer creation failed!`); - process.exit(1); - }); + console.log(`Free transfer ${i + 1} created and in block`); + } - // Add a small delay between transfers - await new Promise(resolve => setTimeout(resolve, 10_000)); - console.log("App agent assets created successfully"); + console.log("All transfers completed successfully"); } - module.exports = { - create_app_agent_fungible_token -} \ No newline at end of file + create_fungible_tokens, + set_metadata_and_mint_fungible_token, + create_token_transfer +} diff --git a/deploy/utils/nft.js b/deploy/utils/nft.js index 605a550..b1eb886 100644 --- a/deploy/utils/nft.js +++ b/deploy/utils/nft.js @@ -1,189 +1,165 @@ -const { ApiPromise, WsProvider } = require("@polkadot/api"); -const { Keyring } = require("@polkadot/keyring"); const { encodeNamed } = require("./keyless"); +const { processClearingTransaction, processSignedTransaction } = require("./utils"); -async function create_app_agent_nft_collection(api, appAgentOwner, appAgentId, metadataUrl) { - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate +async function create_nft_collections(api, appAgentOwner, appAgentId, collection_count) { + return new Promise(async (resolve, reject) => { + try { + console.log("Start to create NFT Collections for the AppAgent ID " + appAgentId); - let asset_admin = encodeNamed(appAgentId, "asset-admi"); + let asset_admin = encodeNamed(appAgentId, "asset-admi"); - // send some balance to admin - let balance_call = api.tx.balances.transferKeepAlive( - asset_admin, - 10 - ); + console.log("Create Clearing transaction"); + let atomics = []; - await balance_call.signAndSend(appAgentOwner); + let create_nft_call = api.tx.nfts.create( + asset_admin, + { + settings: 0, + mintSettings: { + mintType: "issuer", + defaultItemSettings: 0 + } + } + ); + + for (let i = 0; i < collection_count; i++) { + let create_nft_action = [{ AppAgentId: appAgentId }, create_nft_call]; + let create_nft_atomic = [create_nft_action]; + atomics.push(create_nft_atomic); + } - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate + let create_nft_ct = api.tx.addressPools.submitClearingTransaction( + appAgentId, + atomics + ); + + let events = await processClearingTransaction(appAgentOwner, create_nft_ct); + + console.log("Clearing transaction successfully processed, collect IDs of created NFT collections."); + let collection_ids = []; + for (const event of events) { + if (event.receipt.event_module === 'Nfts' && event.receipt.event_name === 'Created') { + const collection_id = event.attributes.collection.toString(); + console.log("NFT Collection created with ID: " + collection_id); + collection_ids.push(collection_id); + } + } + console.log("Generated collection IDs: ", collection_ids); - // Create the NFT Collection - let create_nft_call = api.tx.nfts.create( - asset_admin, - { - settings: 0, - mintSettings: { - mintType: "issuer", - defaultItemSettings: 0 + if (collection_ids.length != collection_count) { + throw new Error("Not all NFT collections were created"); } - } - ); - let create_nft_ct = api.tx.addressPools.submitClearingTransaction( - appAgentId, - [[ - [ - { AppAgentId: appAgentId }, - create_nft_call - ] - ]] - ); + console.log("Resolving promise with collection_ids:", collection_ids); + resolve(collection_ids); + } catch (error) { + console.error("Error creating NFT Collections:", error.message); + reject(error); + } + }); +} - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate - - // Wait for the event and get the collection ID - let collection_id; - await new Promise((resolve, reject) => { - create_nft_ct.signAndSend(appAgentOwner, { nonce: -1 }, ({ events = [], status }) => { - if (status.isInBlock || status.isFinalized) { - events.forEach(({ event: { data, method, section } }) => { - if (section === 'nfts' && method === 'Created') { - collection_id = data[0].toString(); - console.log(`NFT Collection created with ID: ${collection_id}`); - resolve(); - } - }); +async function set_metadata_and_mint_nft(api, appAgentOwner, appAgentId, collectionId, collectionInfo, token_recipient) { + return new Promise(async (resolve, reject) => { + try { + console.log("Start to configure NFT Collection `" + collectionId + "` for the AppAgent ID " + appAgentId); + let asset_admin = encodeNamed(appAgentId, "asset-admi"); + + let nftInfo = []; + + console.log("Send some balance to admin"); + let balance_call = api.tx.balances.transferKeepAlive( + asset_admin, + 100000000000000 + ); + + await processSignedTransaction(appAgentOwner, balance_call); + + console.log("Build Clearing transaction to setup NFT collection"); + // As we have a small number of NFT tokens in each collection - only 10 - + // we can join all operations into a single CT. + let atomics = []; + + console.log("Create atomic to provide required permissions to the admin"); + let set_team_call = api.tx.nfts.setTeam( + collectionId, + asset_admin, + asset_admin, + asset_admin, + ); + let set_team_action = [{ AppAgentId: appAgentId }, set_team_call]; + let set_team_atomic = [set_team_action]; + atomics.push(set_team_atomic); + + console.log("Create atomic to set collection metadata"); + let set_collection_metadata_call = api.tx.nfts.setCollectionMetadata( + collectionId, + collectionInfo.metadataUrl + ); + let set_collection_metadata_action = [{ NamedAddress: asset_admin }, set_collection_metadata_call]; + let set_collection_metadata_atomic = [set_collection_metadata_action]; + atomics.push(set_collection_metadata_atomic); + + console.log("Create atomics to mint and configure NFT tokens."); + for (const token of collectionInfo.tokens) { + let metadataUrl = token.metadataUrl; + let tokenId = Math.floor(Math.random() * 1000000) + 1; + nftInfo.push({ collectionId: collectionId, tokenId: tokenId }); + + console.log("Create atomic for NFT token: CollectionId - " + collectionId + "; TokenId - " + tokenId + "; metadata URL: " + metadataUrl); + + let mint_nft_call = api.tx.nfts.mint( + collectionId, + tokenId, + token_recipient, + {} + ); + let mint_nft_action = [{ NamedAddress: asset_admin }, mint_nft_call]; + let set_metadata_call = api.tx.nfts.setMetadata( + collectionId, + tokenId, + metadataUrl + ); + let set_metadata_action = [{ NamedAddress: asset_admin }, set_metadata_call]; + let nft_atomic = [mint_nft_action, set_metadata_action]; + atomics.push(nft_atomic); } - }).catch((error) => { - console.error("Error creating NFT collection:", error); + + console.log("Sending CT to mint & configure NFT Tokens, and to set Collection metadata."); + let configure_nft_collection_ct = api.tx.addressPools.submitClearingTransaction( + appAgentId, + atomics + ); + await processClearingTransaction(appAgentOwner, configure_nft_collection_ct); + + console.log("Resolving promise with nftInfo:", nftInfo); + resolve(nftInfo); + } catch (error) { + console.error("Error in set_metadata_and_mint_nft:", error.message); reject(error); - }); + } }); - - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate - - // set the metadata - let set_team_metadata_call = api.tx.nfts.setTeam( - collection_id, - asset_admin, - asset_admin, - asset_admin, - ); - - let set_metadata_call = api.tx.nfts.setCollectionMetadata( - collection_id, - metadataUrl - ); - - let set_team_metadata_ct = api.tx.addressPools.submitClearingTransaction( - appAgentId, - [[ - [ - { NamedAddress: asset_admin }, - set_team_metadata_call - ], - [ - { AppAgentId: appAgentId }, - set_metadata_call - ] - ]] - ); - - await set_team_metadata_ct.signAndSend(appAgentOwner, { nonce: -1 }) - - console.log("App agent NFTs created successfully"); - - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate - - return collection_id; } +async function create_nft_transfers(api, collection_id, token_id, token_sender, token_recipient) { + console.log("Generate free transfers between the two users"); + console.log("Collection ID: ", collection_id); + console.log("Token ID: ", token_id); + console.log("Token Sender: ", token_sender.address); + console.log("Token Recipient: ", token_recipient.address); -async function create_app_agent_nft_token(api, appAgentOwner, appAgentId, collection_id, metadataUrl, recipient_one, recipient_two) { - let asset_admin = encodeNamed(appAgentId, "asset-admi"); - - // Generate a random token ID within a specific range (e.g., 1 to 1000) - let tokenId = Math.floor(Math.random() * 1000) + 1; - - console.log(`Generated token ID: ${tokenId}`); - - // send some balance to admin - let balance_call = api.tx.balances.transferKeepAlive( - asset_admin, - 10 * 1e12 - ); - - await balance_call.signAndSend(appAgentOwner); - - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate - - let mint_nft_call = api.tx.nfts.mint( - collection_id, - tokenId, - recipient_one.address, - {} - ); - - let set_metadata_call = api.tx.nfts.setMetadata( + const tx = api.tx.playerTransfers.submitTransferNfts( collection_id, - tokenId, - metadataUrl - ); - - let set_metadata_ct = api.tx.addressPools.submitClearingTransaction( - appAgentId, - [[ - [ - { NamedAddress: asset_admin }, - mint_nft_call - ], - [ - { NamedAddress: asset_admin }, - set_metadata_call - ] - ]] + token_id, + token_recipient.address ); - await set_metadata_ct.signAndSend(appAgentOwner, { nonce: -1 }); - - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate - - console.log("App agent NFT metadata set successfully"); - - await create_nft_transfers(api, recipient_one, recipient_two, collection_id, tokenId); - - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate -} - -async function create_nft_transfers(api, token_recipient, token_recipient_two, collection_id, token_id) { - - // generate 5 free transfers between the two users - let batch_calls_two = []; - for (let i = 0; i < 5; i++) { - let free_transfer_call = api.tx.playerTransfers.submitTransferNfts( - collection_id, - token_id, - token_recipient_two.address, - ); - batch_calls_two.push(free_transfer_call); - } - - let batch_call_two = api.tx.utility.batch(batch_calls_two); - - batch_call_two.signAndSend(token_recipient, { nonce: -1 }) - .then(() => { - console.log(`Free transfer created`); - }).catch((err) => { - console.log(err); - console.log(`Test failed : free transfer creation failed!`); - process.exit(1); - }); - - await new Promise(resolve => setTimeout(resolve, 10_000)); // wait for the previous tx to propogate - + await processSignedTransaction(token_sender, tx); + console.log(`Free transfer created and confirmed`); } module.exports = { - create_app_agent_nft_collection, - create_app_agent_nft_token -} \ No newline at end of file + create_nft_collections, + set_metadata_and_mint_nft, + create_nft_transfers +} diff --git a/deploy/utils/utils.js b/deploy/utils/utils.js new file mode 100644 index 0000000..2f5fb27 --- /dev/null +++ b/deploy/utils/utils.js @@ -0,0 +1,108 @@ +const { getAllEvents } = require('./datagate'); + +const expectedTxFinalisationTime = 45000 // 45 sec +const maxWaitTime = 6000000; // 10 minutes in milliseconds +const maxRetries = 3; +const initialBackoff = 30000; // 30 seconds + + +async function checkTxSuccessWithRetry(txHash, successModuleName, successEventName) { + console.log(`Checking success of transaction ${txHash}`); + + await new Promise(resolve => setTimeout(resolve, expectedTxFinalisationTime)); + + let retries = 0; + while (retries < maxRetries) { + const result = await getAllEvents(txHash); + if (result) { + // console.log(result); + for (const event of result) { + // console.log(event); + if (event.receipt.event_module === successModuleName && event.receipt.event_name === successEventName) { + console.log(`Event ${successModuleName}.${successEventName} confirmed for transaction ${txHash}`); + return result; + } + } + throw new Error(`Event ${successModuleName}.${successEventName} not found for transaction ${txHash}`); + } else { + retries++; + if (retries === maxRetries) { + console.log(`Failed to find the event ${successModuleName}.${successEventName} in tx ${txHash} after ${maxRetries} attempts`); + throw err; + } + const backoffTime = initialBackoff * Math.pow(2, retries - 1); + console.log(`Couldn't fetch events generated by transaction. Retrying in ${backoffTime}ms. Attempt ${retries} of ${maxRetries}`); + await new Promise(resolve => setTimeout(resolve, backoffTime)); + } + } +} + +async function processClearingTransaction(signer, ct) { + return new Promise(async (resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error(`CT processing timed out after ${maxWaitTime}ms`)); + }, maxWaitTime); + + try { + const txHash = await ct.signAndSend(signer, { nonce: -1 }); + const txEvents = await checkTxSuccessWithRetry(txHash.toString(), "AddressPools", "CTProcessingCompleted"); + console.log(`CT processing completed successfully`); + // console.log(`Transaction completed successfully with events: ${txEvents}`); + clearTimeout(timeout); + resolve(txEvents); + } catch (error) { + clearTimeout(timeout); + console.error("Error in processClearingTransaction:", error); + reject(error); + } + }); +} + +async function processSignedTransaction(signer, tx) { + return new Promise(async (resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error(`Transaction timed out after ${maxWaitTime}ms`)); + }, maxWaitTime); + + try { + const txHash = await tx.signAndSend(signer, { nonce: -1 }); + const txEvents = await checkTxSuccessWithRetry(txHash.toString(), "System", "ExtrinsicSuccess"); + console.log(`Transaction processing completed successfully`); + // console.log(`Transaction completed successfully with events: ${txEvents}`); + clearTimeout(timeout); + resolve(txEvents); + } catch (error) { + clearTimeout(timeout); + console.error("Error in processSignedTransaction:", error); + reject(error); + } + }); +} + +async function processSignedBatchTransaction(signer, tx) { + return new Promise(async (resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error(`Batch transaction timed out after ${maxWaitTime}ms`)); + }, maxWaitTime); + + try { + const txHash = await tx.signAndSend(signer, { nonce: -1 }); + let txEvents = await checkTxSuccessWithRetry(txHash.toString(), "Utility", "BatchCompleted"); + console.log(`Batch transaction processing completed successfully`); + // console.log(`Transaction completed successfully with events: ${txEvents}`); + clearTimeout(timeout); + resolve(txEvents); + } catch (error) { + clearTimeout(timeout); + console.error("Error in processSignedBatchTransaction:", error); + reject(error); + } + }); +} + + +module.exports = { + processClearingTransaction, + processSignedTransaction, + processSignedBatchTransaction +} \ No newline at end of file