From a86f16c4b98943b7560f5de28de9d621b6082f6a Mon Sep 17 00:00:00 2001 From: nonast <29281463+nonast@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:40:51 +0800 Subject: [PATCH] Add foundry-output-claim typescript sdk example --- docs/examples/typescript/package.json | 3 +- .../src/stardust/foundry-output-claim.ts | 158 ++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 docs/examples/typescript/src/stardust/foundry-output-claim.ts diff --git a/docs/examples/typescript/package.json b/docs/examples/typescript/package.json index 2df2fc73025..b47a015bf49 100644 --- a/docs/examples/typescript/package.json +++ b/docs/examples/typescript/package.json @@ -7,7 +7,8 @@ "alias-migration": "ts-node src/stardust/alias-migration.ts", "alias-output-claim": "ts-node src/stardust/alias-output-claim.ts", "basic-output-claim": "ts-node src/stardust/basic-output-claim.ts", - "check-basic-output-unlock-conditions": "ts-node src/stardust/check-basic-output-unlock-conditions.ts" + "check-basic-output-unlock-conditions": "ts-node src/stardust/check-basic-output-unlock-conditions.ts", + "foundry-output-claim": "ts-node src/stardust/foundry-output-claim.ts" }, "keywords": [], "author": "", diff --git a/docs/examples/typescript/src/stardust/foundry-output-claim.ts b/docs/examples/typescript/src/stardust/foundry-output-claim.ts new file mode 100644 index 00000000000..9d710f630f7 --- /dev/null +++ b/docs/examples/typescript/src/stardust/foundry-output-claim.ts @@ -0,0 +1,158 @@ +/** Copyright (c) 2024 IOTA Stiftung + * SPDX-License-Identifier: Apache-2.0 + * + * Example demonstrating the claim of a CoinManagerTreasuryCap related to a + * foundry output. In order to work, it requires a network with test objects + * generated from iota-genesis-builder/src/stardust/test_outputs. + */ +import {getFullnodeUrl, IotaClient, IotaObjectData} from "@iota/iota-sdk/client"; +import {Ed25519Keypair} from "@iota/iota-sdk/keypairs/ed25519"; +import {fundAddress} from "../utils"; +import {bcs} from "@iota/iota-sdk/bcs"; +import * as assert from "node:assert"; +import {Transaction} from "@iota/iota-sdk/transactions"; + +const MAIN_ADDRESS_MNEMONIC = "few hood high omit camp keep burger give happy iron evolve draft few dawn pulp jazz box dash load snake gown bag draft car"; +const STARDUST_PACKAGE_ID = "0x107a"; +const COIN_MANAGER_MODULE_NAME = "coin_manager"; +const COIN_MANAGER_TREASURY_CAP_STRUCT_NAME = "CoinManagerTreasuryCap"; +const IOTA_FRAMEWORK_ADDRESS = "0x2"; + +async function main() { + // Build a client to connect to the local IOTA network. + const iotaClient = new IotaClient({url: getFullnodeUrl('localnet')}); + + // Derive keypair from mnemonic. + const keypair = Ed25519Keypair.deriveKeypair(MAIN_ADDRESS_MNEMONIC); + const sender = keypair.toIotaAddress(); + console.log(`Sender address: ${sender}`); + + // Fund the address. + await fundAddress(iotaClient, sender); + + // Fetch the alias output object. + const aliasOutputObjectId = "0xa58e9b6b85863e2fa50710c4594f701b2f5e2c6ff5e3c2b10cf09e6b18d740da"; + const aliasOutputObject = await iotaClient.getObject({id: aliasOutputObjectId}); + if (!aliasOutputObject) { + throw new Error("Alias output object not found"); + } + + // Get the dynamic field owned by the Alias Output, i.e., only the Alias + // object. + // The dynamic field name for the Alias object is "alias", of type vector. + const dfName = { + type: bcs.TypeTag.serialize({ + vector: { + u8: true, + }, + }).parse(), + value: "alias" + }; + + const aliasObject = await iotaClient.getDynamicFieldObject({ parentId: aliasOutputObjectId, name: dfName}); + if (!aliasObject) { + throw new Error("Alias object not found"); + } + + // Get the object id of the Alias object. + const aliasObjectId = aliasObject.data?.objectId; + + // Get the objects owned by the alias object and filter in the ones with + // CoinManagerTreasuryCap as type. + + const aliasOwnedObjects = await iotaClient.getOwnedObjects({ + owner: aliasObjectId ? aliasObjectId.toString() : "", + options: { + showBcs: true, + showType: true + }, + }); + + // Only one page should exist. + assert.ok(!aliasOwnedObjects.hasNextPage, "Only one page should exist"); + + // Get the CoinManagerTreasuryCaps from the query by filtering. + const ownedCoinManagerTreasuryCaps = aliasOwnedObjects.data + .filter(object => { + return isCoinManagerTreasuryCap(object.data as IotaObjectData); + }); + + // Get only the first coin manager treasury cap. + const coinManagerTreasuryCap = ownedCoinManagerTreasuryCaps[0]?.data; + if (!coinManagerTreasuryCap) { + throw new Error("CoinManagerTreasuryCap not found"); + } + + const coinManagerTreasuryCapId = coinManagerTreasuryCap.objectId; + const foundryTokenTypeStructTag = coinManagerTreasuryCap.type + const foundryTokenType = foundryTokenTypeStructTag?.split("<")[1].split(">")[0] || ""; + + // Create a PTB to claim the CoinManagerTreasuryCap related to the foundry + // output from the alias output. + const tx = new Transaction(); + const gasTypeTag = "0x2::iota::IOTA"; + const args = [tx.object(aliasOutputObjectId)]; + const extractedAliasOutputAssets = tx.moveCall({ + target: `${STARDUST_PACKAGE_ID}::alias_output::extract_assets`, + typeArguments: [gasTypeTag], + arguments: args, + }); + + const extractedBaseToken = extractedAliasOutputAssets[0]; + const extractedNativeTokensBag = extractedAliasOutputAssets[1]; + const alias = extractedAliasOutputAssets[2]; + + // Extract the IOTA balance. + const iotaCoin = tx.moveCall({ + target: '0x2::coin::from_balance', + typeArguments: [gasTypeTag], + arguments: [extractedBaseToken], + }); + + // Transfer the IOTA balance to the sender. + tx.transferObjects([iotaCoin], tx.pure.address(sender)); + + // In this example the native tokens bag is empty, so it can be destroyed. + tx.moveCall({ + target: '0x2::bag::destroy_empty', + typeArguments: [], + arguments: [extractedNativeTokensBag], + }); + + const coinManagerTreasuryCapObject = tx.moveCall({ + target: `${STARDUST_PACKAGE_ID}::address_unlock_condition::unlock_alias_address_owned_coinmanager_treasury`, + typeArguments: [foundryTokenType], + arguments: [alias, tx.object(coinManagerTreasuryCapId)], + }); + + // Transfer the coin manager treasury cap. + tx.transferObjects([coinManagerTreasuryCapObject], tx.pure.address(sender)); + + // Transfer the alias asset. + tx.transferObjects([alias], tx.pure.address(sender)); + + // Set the gas budget for the transaction. + tx.setGasBudget(10_000_000); + + // Sign and execute the transaction. + const result = await iotaClient.signAndExecuteTransaction({ signer: keypair, transaction: tx }); + + // Get the response of the transaction. + const response = await iotaClient.waitForTransaction({ digest: result.digest }); + console.log(`Transaction digest: ${response.digest}`); +} + +/** + * Check if the object is a CoinManagerTreasuryCap. + */ +function isCoinManagerTreasuryCap(object: IotaObjectData): boolean { + const splitType = object.type?.split("::"); + return splitType !== undefined && + splitType[0] === IOTA_FRAMEWORK_ADDRESS && + splitType[1] === COIN_MANAGER_MODULE_NAME && + splitType[2].split("<")[0] === COIN_MANAGER_TREASURY_CAP_STRUCT_NAME; +} + +main().catch(error => { + console.error(`Error: ${error.message}`); +}); \ No newline at end of file