diff --git a/docs/examples/typescript/package.json b/docs/examples/typescript/package.json index b47a015bf49..0178f896943 100644 --- a/docs/examples/typescript/package.json +++ b/docs/examples/typescript/package.json @@ -8,7 +8,8 @@ "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", - "foundry-output-claim": "ts-node src/stardust/foundry-output-claim.ts" + "foundry-output-claim": "ts-node src/stardust/foundry-output-claim.ts", + "iota-self-sponsor": "ts-node src/stardust/iota-self-sponsor.ts" }, "keywords": [], "author": "", diff --git a/docs/examples/typescript/src/stardust/iota-self-sponsor.ts b/docs/examples/typescript/src/stardust/iota-self-sponsor.ts new file mode 100644 index 00000000000..3ab44c33499 --- /dev/null +++ b/docs/examples/typescript/src/stardust/iota-self-sponsor.ts @@ -0,0 +1,110 @@ +/** Copyright (c) 2024 IOTA Stiftung + * SPDX-License-Identifier: Apache-2.0 + * + * Example demonstrating the self-sponsor scenario for claiming a Iota basic + * output. In order to work, it requires a network with test objects + * generated from iota-genesis-builder/src/stardust/test_outputs. + */ +import {getFullnodeUrl, IotaClient} from "@iota/iota-sdk/client"; +import {Ed25519Keypair} from "@iota/iota-sdk/keypairs/ed25519"; +import {Transaction} from "@iota/iota-sdk/transactions"; + +const MAIN_ADDRESS_MNEMONIC = "crazy drum raw dirt tooth where fee base warm beach trim rule sign silk fee fee dad large creek venue coin steel hub scale"; +const STARDUST_PACKAGE_ID = "0x107a"; + +async function main() { + // Build a client to connect to the local IOTA network. + const iotaClient = new IotaClient({url: getFullnodeUrl('localnet')}); + + // For this example we need to derive addresses that are at different + // indexes and coin_types, one for sponsoring with IOTA coin type and one for + // claiming the Basic Output with Iota coin type. + const sponsorDerivationPath = "m/44'/4218'/0'/0'/5'" + const senderDerivationPath = "m/44'/4218'/0'/0'/50'" + + const sponsorKeypair = Ed25519Keypair.deriveKeypair(MAIN_ADDRESS_MNEMONIC, sponsorDerivationPath); + const senderKeypair = Ed25519Keypair.deriveKeypair(MAIN_ADDRESS_MNEMONIC, senderDerivationPath); + + const sponsor = sponsorKeypair.toIotaAddress(); + const sender = senderKeypair.toIotaAddress(); + + console.log(`Sender: ${sender}`); + console.log(`Sponsor: ${sponsor}`); + + // Get the Basic object containing the Native tokens. + const basicOutputObjectId = "0xd0ed7f2c50366202585ebd52a38cde6a7a7282ef3f52db52c3ba87042bca6fba"; + const basicOutputObject = await iotaClient.getObject({id: basicOutputObjectId, options: { showBcs: true }}); + if (!basicOutputObject) { + throw new Error("Basic output object not found"); + } + + // Create a PTB to claim the assets related to the basic output. + const tx = new Transaction(); + const gasTypeTag = "0x2::iota::IOTA"; + const args = [tx.object(basicOutputObjectId)]; + const extractedBasicOutputAssets = tx.moveCall({ + target: `${STARDUST_PACKAGE_ID}::basic_output::extract_assets`, + typeArguments: [gasTypeTag], + arguments: args, + }); + + // If the basic output can be unlocked, the command will be successful and will + // return a `base_token` balance and a `Bag` of native tokens. + const extractedBaseToken = extractedBasicOutputAssets[0]; + const extractedNativeTokensBag = extractedBasicOutputAssets[1]; + + // Cleanup the bag by destroying it. + tx.moveCall({ + target: `0x2::bag::destroy_empty`, + typeArguments: [], + arguments: [extractedNativeTokensBag], + }); + + // 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)); + + // Get a gas coin belonging to the sponsor. + const sponsorGasObjects = await iotaClient.getCoins({ owner: sponsor }); + const sponsorGasCoin = sponsorGasObjects.data?.[0]; + + if (!sponsorGasCoin) { + throw new Error('No coins found for sponsor'); + } + + tx.setSender(sender); + tx.setGasOwner(sponsor); + // Set sponsor’s gas object to cover fees. + tx.setGasPayment([{ + objectId: sponsorGasCoin.coinObjectId, + version: sponsorGasCoin.version, + digest: sponsorGasCoin.digest + }]); + tx.setGasBudget(10_000_000); + + // Sign the transaction with the sponsor and sender keypairs. + const sponsorSignedTransaction = await tx.sign({ client: iotaClient, signer: sponsorKeypair }); + const senderSignedTransaction = await tx.sign({ client: iotaClient, signer: senderKeypair }); + + // Build the transaction and execute it. + const builtTransaction = await tx.build({ client: iotaClient }); + const result = await iotaClient.executeTransactionBlock({ + transactionBlock: builtTransaction, + signature: [sponsorSignedTransaction.signature, senderSignedTransaction.signature] + }); + + // Get the response of the transaction. + const response = await iotaClient.waitForTransaction({ digest: result.digest }); + console.log(`Transaction digest: ${response.digest}`); + +} + +main().catch(error => { + console.error(`Error: ${error.message}`); +}); \ No newline at end of file