From 7acbe9f5b3a9f9f5e7ec7c2a064df4845ff0a11f Mon Sep 17 00:00:00 2001 From: Pavlo Botnar Date: Thu, 22 Aug 2024 11:25:20 +0300 Subject: [PATCH] fix(docs/content, docs/example): test example scenarios for claim/unlock outputs, documentation enhancement (#1862) * feat(docs): Create an examples folder for Rust code (#1503) * feat(docs): create an example folder for Rust code * Update docs/examples/rust/Cargo.toml --------- Co-authored-by: Dr-Electron * fix(docs/examples): remove base_keystore example * feat(iota-sdk): added an alias output claim example (#1493) * feat(iota-sdk): added an alias output claim example * feat(docs): moved the alias output claim example * refactor(docs): used the extract_and_send_to function to simplify the example * fix(docs): added a license header and an example description * fix(docs): alias-output-claim comments * fix(docs): clippy * fix(docs): move the alias-output-claim example into the stardust subfolder * fix(docs): fixed the example after rebasing * fix(docs): move the alias-output-claim example * fix(docs): alias-output-claim logs improved * feat(iota-sdk): add basic output claim example * fix(iota-sdk): use the proper native token bag key * fix(iota-sdk): move basic output example in docs * fix(docs/examples): comments * fix(docs/examples): license header * fix(docs/examples): basic output address * fix(docs/examples): fix clippy * fix(docs/examples): fix from comments * fix(docs/examples): move basic output example * fix(docs/examples): remove src folder * feat(docs/examples): nft output extraction example based on sdk (#1478) * feat(docs): add nft output claiming test example based on sdk usage * feat(ci): add docs examples in rust (#1539) * fix(docs/examples): add license * fix(docs/examples): cargo lock * chore(docs/examples): add clarification comment * feat(docs/examples): add unlock condition example based on rust sdk (#1559) * feat(docs): add unlock condition example based on rust sdk Add fund_address function for sponsoring the main transaction * feat(docs/examples): Add foundry output claim example (#1575) * feat(docs/examples): ad foundry output claim * fix(docs/examples): cargo toml and clones * feat(docs/examples): Test the positive scenarios for using an NFT object (#1615) * Add example of third party simple nft package, PTB that creates custom nft from stardust::nft Add a conversion function for custom NFTs to allow migrating custom NFTs from Stardust NFTs * feat(docs/stardust): Add docs for basic output claim (#1639) * feat(docs/stardust): add docs for basic output claim * Update docs/examples/rust/stardust/check-basic-output-unlock-conditions.rs Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * Update docs/examples/rust/stardust/check-basic-output-unlock-conditions.rs Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --------- Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * feat(docs/content): documentation - claim NFT Output (#1654) * feat(docs/content): documentation - claim NFT Output * fear(docs/content) example of conversion of a stardust NFT into a custom user's. (#1669) * Add example ofr third party simple nft package, PTB that create custom nft from stardust::nft * feat(examples/docs): Add a conversion function for custom NFTs to allow migrating custom NFTs from Stardust NFTs * Add function that publishes random nft package via CLI. * feat(examples): replace package publishing approach with iota-move-builder * Fix chkecs * Minor refactoring * dprint fix * Fix review comments * feat(docs/content): add doc related to conversion of a NFT Output into sutom NFT * Review comment fixes * Fix Nft naming * Fix review comments --------- Co-authored-by: Mirko Zichichi * feat(docs): Using an Alias object test scenario (#1679) * Add example ofr third party simple nft package, PTB that create custom nft from stardust::nft * feat(examples/docs): Add a conversion function for custom NFTs to allow migrating custom NFTs from Stardust NFTs * Add function that publishes random nft package via CLI. * feat(examples): replace package publishing approach with iota-move-builder * Fix chkecs * Minor refactoring * dprint fix * Fix review comments * feat(docs/content): add doc related to conversion of a NFT Output into sutom NFT * feat(docs): extended the custom_nft package with collections * Review comment fixes * Fix Nft naming * fix(docs): NFTs conversion docs were fixed after refactoring * fix(docs): spelling issues --------- Co-authored-by: Dkwcs * refactor(docs/content): Split claiming docs in a tree (#1710) * refactor(docs/content): split claiming docs in a tree * feat(docs/content): add address unlock condition claim * fix(docs/content): claiming references * fix(docs/content): claiming references 2 * chore(docs): fix doc tree for claiming --------- Co-authored-by: Levente Pap * feat(docs): Alias usage documentation was added (#1723) * Add example ofr third party simple nft package, PTB that create custom nft from stardust::nft * feat(examples/docs): Add a conversion function for custom NFTs to allow migrating custom NFTs from Stardust NFTs * Add function that publishes random nft package via CLI. * feat(examples): replace package publishing approach with iota-move-builder * Fix chkecs * Minor refactoring * dprint fix * Fix review comments * feat(docs/content): add doc related to conversion of a NFT Output into sutom NFT * feat(docs): extended the custom_nft package with collections * Review comment fixes * Fix Nft naming * fix(docs): NFTs conversion docs were fixed after refactoring * fix(docs): spelling issues * feat(docs): added alias documentation * fix(docs): spelling issues * fix(docs): review comments --------- Co-authored-by: Dkwcs * feat(docs/content): docs implementation for address unlock condition example (#1706) * feat(docs/content): docs impl for address unlock condition * feat(docs/content): Create example doc for claiming a Foundry Output (#1724) * feat(docs/content): add doc example of claiming foundry output * fix(docs/content): enhance claiming and fix links (#1768) * feat(docs/examples): Add self sponsor example for Shimmer assets (#1772) * feat(docs/examples): add self sponsor example * fix(docs/content): make self sponsor example for Shimmer * fix(docs/examples): smr comment * fix(docs/examples): dprint * fix(docs/examples): remove double comment Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * fix(docs/examples): client server comments --------- Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * feat(docs/content): Add Shimmer self-sponsorship claim docs (#1773) * feat(docs/examples): add self sponsor example * fix(docs/content): make self sponsor example for Shimmer * fix(docs/examples): smr comment * fix(docs/examples): dprint * feat(docs/content): add shimmer self-sponsorship claim * fix(docs/content): spaces Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * fix(docs/content): code lines --------- Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * fix(docs/content): improvements of docs navigation by adding links (#1774) * fix(docs/content): improvements of docs navigation by adding links * fix(docs/content): add additional links, readable improvements. * fix(docs/content/../claiming): simplify navigation with additional links for OTW, Bag, Coin, Unlock Condition * refactor(docs/examples): Create a utility methods lib (#1821) * refactor(docs/examples): create a utility methods lib * refactor(docs/examples): update examples * refactor(docs/content): update examples code lines in docs * fix(docs/examples): move module path * fix(docs/examples): address uc name * Update docs/examples/rust/src/lib.rs Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * Update docs/content/developer/stardust/claiming/nft.mdx Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * fix(docs/examples): move utils out of lib --------- Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --------- Co-authored-by: Mirko Zichichi Co-authored-by: Dr-Electron Co-authored-by: Valerii Reutov Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Co-authored-by: Levente Pap --- .github/actions/diffs/action.yml | 1 + Cargo.lock | 16 + Cargo.toml | 1 + docs/content/developer/stardust/claiming.mdx | 43 ++- .../claiming/address-unlock-condition.mdx | 66 +++++ .../developer/stardust/claiming/alias.mdx | 100 +++++++ .../developer/stardust/claiming/basic.mdx | 71 +++++ .../developer/stardust/claiming/foundry.mdx | 79 +++++ .../developer/stardust/claiming/nft.mdx | 88 ++++++ .../stardust/claiming/self-sponsor.mdx | 59 ++++ docs/content/developer/stardust/exchanges.mdx | 4 +- .../developer/stardust/migration-process.mdx | 4 +- .../developer/stardust/move-models.mdx | 52 ++-- docs/content/sidebars/developer.js | 41 ++- docs/examples/move/custom_nft/Move.toml | 10 + .../move/custom_nft/sources/collection.move | 140 +++++++++ .../examples/move/custom_nft/sources/nft.move | 136 +++++++++ docs/examples/rust/Cargo.toml | 59 ++++ docs/examples/rust/src/lib.rs | 4 + docs/examples/rust/src/utils.rs | 194 ++++++++++++ .../rust/stardust/address-unlock-condition.rs | 271 +++++++++++++++++ .../examples/rust/stardust/alias-migration.rs | 276 ++++++++++++++++++ .../rust/stardust/alias-output-claim.rs | 212 ++++++++++++++ .../rust/stardust/basic-output-claim.rs | 209 +++++++++++++ .../check-basic-output-unlock-conditions.rs | 59 ++++ .../rust/stardust/foundry-output-claim.rs | 247 ++++++++++++++++ docs/examples/rust/stardust/nft-migration.rs | 169 +++++++++++ .../rust/stardust/nft-output-claim.rs | 207 +++++++++++++ .../rust/stardust/shimmer-self-sponsor.rs | 183 ++++++++++++ 29 files changed, 2965 insertions(+), 36 deletions(-) create mode 100644 docs/content/developer/stardust/claiming/address-unlock-condition.mdx create mode 100644 docs/content/developer/stardust/claiming/alias.mdx create mode 100644 docs/content/developer/stardust/claiming/basic.mdx create mode 100644 docs/content/developer/stardust/claiming/foundry.mdx create mode 100644 docs/content/developer/stardust/claiming/nft.mdx create mode 100644 docs/content/developer/stardust/claiming/self-sponsor.mdx create mode 100644 docs/examples/move/custom_nft/Move.toml create mode 100644 docs/examples/move/custom_nft/sources/collection.move create mode 100644 docs/examples/move/custom_nft/sources/nft.move create mode 100644 docs/examples/rust/Cargo.toml create mode 100644 docs/examples/rust/src/lib.rs create mode 100644 docs/examples/rust/src/utils.rs create mode 100644 docs/examples/rust/stardust/address-unlock-condition.rs create mode 100644 docs/examples/rust/stardust/alias-migration.rs create mode 100644 docs/examples/rust/stardust/alias-output-claim.rs create mode 100644 docs/examples/rust/stardust/basic-output-claim.rs create mode 100644 docs/examples/rust/stardust/check-basic-output-unlock-conditions.rs create mode 100644 docs/examples/rust/stardust/foundry-output-claim.rs create mode 100644 docs/examples/rust/stardust/nft-migration.rs create mode 100644 docs/examples/rust/stardust/nft-output-claim.rs create mode 100644 docs/examples/rust/stardust/shimmer-self-sponsor.rs diff --git a/.github/actions/diffs/action.yml b/.github/actions/diffs/action.yml index 77b04956d22..e1819773c50 100644 --- a/.github/actions/diffs/action.yml +++ b/.github/actions/diffs/action.yml @@ -32,6 +32,7 @@ runs: - "external-crates/**" - "narwhal/**" - "iota-execution/**" + - "docs/examples/rust/**" - ".github/workflows/hierarchy.yml" - ".github/workflows/codecov.yml" - ".github/workflows/_rust.yml" diff --git a/Cargo.lock b/Cargo.lock index afc2d7ee4c7..190b454167a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3485,6 +3485,22 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "docs-examples" +version = "0.1.4" +dependencies = [ + "anyhow", + "bcs", + "bip32", + "iota-keys", + "iota-move-build", + "iota-sdk 0.1.4", + "move-core-types", + "serde_json", + "shared-crypto", + "tokio", +] + [[package]] name = "downcast" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index d13d404cf89..80bb4dedd78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,6 +146,7 @@ members = [ "crates/typed-store", "crates/typed-store-derive", "crates/typed-store-error", + "docs/examples/rust", "iota-execution", "iota-execution/cut", "iota-execution/latest/iota-adapter", diff --git a/docs/content/developer/stardust/claiming.mdx b/docs/content/developer/stardust/claiming.mdx index 3d1e87e6d24..4c8b8384552 100644 --- a/docs/content/developer/stardust/claiming.mdx +++ b/docs/content/developer/stardust/claiming.mdx @@ -3,7 +3,42 @@ title: Claiming Stardust Assets description: Describes how to access the migrated assets and tokens --- - - Explains the resulting migrated state for simple coins (exchanges) - - Demonstrates through examples for each output type the resulting move objects - - Explains what commands need to be called in a PTB to claim the assets - - Describes transaction sponsorship from iota addresses for shimmer claiming \ No newline at end of file +As detailed in the [Stardust Move Models](move-models.mdx), Stardust assets are represented as Move objects within the ledger. Claiming these assets involves enabling original owners to utilize a [Programmable Transaction Block](../iota-101/transactions/ptb/prog-txn-blocks.mdx) to "unlock" assets such as IOTA, Shimmer, custom [`Coin`](../../references/framework/iota-framework/coin.mdx#resource-coin)s, or even `Alias` and `Nft` objects. + +This process takes advantage of Move's unique features to ensure that assets are transferred and unlocked securely and efficiently to their rightful owners. + + +## Output types to Move Objects + +Stardust assets come in the form of Outputs and each Output can be of a different type. For understanding how Outputs have been transformed into Move Objects based on their types, please refer to the [Stardust Move Models](move-models.mdx#summary) page. In here, one or more claiming examples for each Output type will be shown. + + +## Examples of Stardust asset claim transactions + +In the following, some examples of transactions for claiming the Stardust assets will be presented. Different commands in a [PTB](../iota-101/transactions/ptb/prog-txn-blocks.mdx) are used depending on the claiming scenario, that varies depending on the Stardust Output type and composition. + +### Basic Output + +Go to [Basic Output](claiming/basic.mdx). + +### Nft Output + +Go to [Nft Output](claiming/nft.mdx). + +### Alias Output + +Go to [Alias Output](claiming/alias.mdx). + +### Foundry Output + +Go to [Foundry Output](claiming/foundry.mdx). + +### Output unlockable by an Alias/Nft Address + +Go to [Output unlockable by an Alias/Nft Address](claiming/address-unlock-condition.mdx). + +## Sponsoring your Shimmer claiming + +In the case in which some assets are owned by a Shimmer address that helds no IOTA tokens (needed to pay for gas), then a claiming can benefit by the process of self-sponsoring. This means that an address owning IOTA tokens can be used by the same user to sponsor a transaction claiming Shimmer assets. + +Go to [Self-sponsor Shimmer Claimings](claiming/self-sponsor.mdx). \ No newline at end of file diff --git a/docs/content/developer/stardust/claiming/address-unlock-condition.mdx b/docs/content/developer/stardust/claiming/address-unlock-condition.mdx new file mode 100644 index 00000000000..f61a5f737fa --- /dev/null +++ b/docs/content/developer/stardust/claiming/address-unlock-condition.mdx @@ -0,0 +1,66 @@ +--- + title: Claiming an Output unlockable by an Alias/Nft Address +--- + +In Stardust outputs presented an Address Unlock Condition or similarly, in the case of the Alias Output, a [Governor Address Unlock Condition](https://github.com/iotaledger/tips/blob/main/tips/TIP-0018/tip-0018.md#governor-address-unlock-condition). In the new ledger, this mechanism is represented as an address owning the associated Output object. Most of the times the address is directly managed through a keypair by a user, but sometimes this address could represent another object. In this case, that object owns the interested Output object. Coming from the [Stardust migration](../migration-process.mdx#stardust-migration), only `Alias` and `Nft` objects can own other Output objects. + +## Claim of an Output owned by another Alias/Nft object + +For this example, we're using an `AliasOutput` to extract an `Alias` object that owns an `NftOutput`. + +1. The first step is to fetch the `AliasOutput` object that is needed for claiming the `NftOutput`. + + + + +```rust file=/docs/examples/rust/stardust/address-unlock-condition.rs#L70-L88 +``` + + + + + + + +2. By using the dynamic field function with the "alias" dynamic field key filter, we gather the `Alias` object itself. + + + + +```rust file=/docs/examples/rust/stardust/address-unlock-condition.rs#L90-L103 +``` + + + + + + + +3. Some objects are owned by the `Alias` object. In this case we filter them by type using the `NftOutput` type tag. +Applying the filter to get `NftOutput`s owned by the `Alias`. + + + + +```rust file=/docs/examples/rust/stardust/address-unlock-condition.rs#L105-L128 +``` + + + + + + + +4. Create PTB that firstly extracts the assets from the `AliasOutput` and then it uses the extracted `Alias` to "address unlock" the `NftOutput` using the funсtion `unlock_alias_address_owned_nft`. + + + + +```rust file=/docs/examples/rust/stardust/address-unlock-condition.rs#L130-L239 +``` + + + + + + \ No newline at end of file diff --git a/docs/content/developer/stardust/claiming/alias.mdx b/docs/content/developer/stardust/claiming/alias.mdx new file mode 100644 index 00000000000..16c03a6c89a --- /dev/null +++ b/docs/content/developer/stardust/claiming/alias.mdx @@ -0,0 +1,100 @@ +--- + title: Claiming Alias Outputs +--- + +An address can own `AliasOutput` objects only if before the migration it was set as the Alias Governor Address. +In this case, the `AliasOutput` object is an owned object in the ledger and its owner is the Governor address. +Such address can be directly controlled by a user or by another object (either an `Alias` or `Nft` object). For the latter use case, check the [`Claiming an Output unlockable by an Alias/Nft Address`](address-unlock-condition.mdx) example. + +## Claim of an Alias Output + +A Governor address can claim the `AliasOutput` assets at any time: + +1. The first step is to fetch an `AliasOutput` object needed to be claimed. + + + + +```rust file=/docs/examples/rust/stardust/alias-output-claim.rs#L56-L81 +``` + + + + + + + + +2. Then we check the native tokens that were possibly held by this output. +A [`Bag`](../../../references/framework/iota-framework/bag) is used for holding these tokens, so in this step we are interested in obtaining the dynamic field keys that are bag indexes. +In the case of the native tokens, the keys are strings representing the [`OTW`](../../iota-101/iota-move-concepts/one-time-witness.mdx) used for the native token declaration. + + + + +```rust file=/docs/examples/rust/stardust/alias-output-claim.rs#L83-L110 +``` + + + + + + + +3. Finally, a PTB can be created using the `alias_output_object_ref` as input and the native token keys. +An `AliasOutput` is different from an `NftOutput` or a `BasicOutput` as it contains the `Alias` object. +In fact, the main purpose of claiming is extracting the `Alias` object from the `AliasOutput`. + + + + +```rust file=/docs/examples/rust/stardust/alias-output-claim.rs#L112-L180 +``` + + + + + + + +## Conversion of an Alias Output into a custom Object + +We need to have a custom package prepared that contains a logic for converting an `Alias` into a new entity usable for your project. + +In Stardust, an alias can be used for different purposes. One of them is acting as an NFT collection controller. +In the following, an example of the process of converting a Stardust `Alias` into a `CollectionControllerCap` is outlined. + +The following example extends the one described in the [Conversion of an Nft Output into a custom Nft](nft.mdx#conversion-of-an-nft-output-into-a-custom-nft) documentation: + + + + +The `collection.move` module extends the `custom_nft` package to make it possible to work with NFT collections: + +```move file=/docs/examples/move/custom_nft/sources/collection.move +``` + +Also, the `nft.move` module was extended with the following function: + +```move file=/docs/examples/move/custom_nft/sources/nft.move#L82-L97 +``` + + + + + +Once the package is prepared, we can extract and use a Stardust `Alias` in a single transaction to create a `CollectionControllerCap`. +This capability is then used in later transactions for managing new collections. + + + + +```rust file=/docs/examples/rust/stardust/alias-migration.rs#L119-L244 +``` + + + + + + + diff --git a/docs/content/developer/stardust/claiming/basic.mdx b/docs/content/developer/stardust/claiming/basic.mdx new file mode 100644 index 00000000000..f0bdf636bed --- /dev/null +++ b/docs/content/developer/stardust/claiming/basic.mdx @@ -0,0 +1,71 @@ +--- + title: Claiming Basic Outputs +--- + +An address can own `BasicOutput` objects that needs to be unlocked. In this case, some off-chain queries can be used to check the unlock conditions of the `BasicOutput` and assess if it can be unlocked. + + + + +```rust file=/docs/examples/rust/stardust/check-basic-output-unlock-conditions.rs#L20-L56 +``` + + + + +TODO + + + + +## Claim of a Basic Output +Once a Basic Output can be unlocked the claim of its assets can start + +1. The first step is to fetch the `BasicOutput` object that needs to be claimed. + + + + +```rust file=/docs/examples/rust/stardust/basic-output-claim.rs#L56-L81 +``` + + + + +TODO + + + + + +2. Then we check the native tokens that were possibly held by this output. A [`Bag`](../../../references/framework/iota-framework/bag) is used for holding these tokens, so in this step we are interested in obtaining the dynamic field keys that are used as bag index. In the case of the native tokens `Bag` the keys are strings representing the [`OTW`](../../iota-101/iota-move-concepts/one-time-witness.mdx) used for the native token [`Coin`](../../../references/framework/iota-framework/coin.mdx#resource-coin). + + + + +```rust file=/docs/examples/rust/stardust/basic-output-claim.rs#L83-L110 +``` + + + + +TODO + + + + +3. Finally, a PTB can be created using the `basic_output` as input and the `Bag` keys for iterating the native tokens extracted. + + + + +```rust file=/docs/examples/rust/stardust/basic-output-claim.rs#L113-L177 +``` + + + + +TODO + + + diff --git a/docs/content/developer/stardust/claiming/foundry.mdx b/docs/content/developer/stardust/claiming/foundry.mdx new file mode 100644 index 00000000000..8826672693b --- /dev/null +++ b/docs/content/developer/stardust/claiming/foundry.mdx @@ -0,0 +1,79 @@ +--- + title: Claiming Foundry Outputs +--- + +As seen in the [Move Models](../move-models) page, the Foundry Output does not have a direct representation in Move. So claiming a Foundry Output actually means claiming a [`CoinManagerTreasuryCap`](../../../references/framework/iota-framework/coin_manager.mdx#resource-coinmanagertreasurycap) extracted from the [`AliasOutput`](../../../references/framework/stardust/alias_output#resource-aliasoutput) originally controlling the Foundry in Stardust. This capability can be used to manage the supply of the [`Coin`](../../../references/framework/iota-framework/coin.mdx#resource-coin) created during the migration to represent the native token controlled by the Foundry. + +## Claim of a Foundry Output + +1. The first step is to fetch an `AliasOutput` object that needs to be claimed. + + + + +```rust file=/docs/examples/rust/stardust/foundry-output-claim.rs#L60-L74 +``` + + + + + + + + +2. By using the dynamic field function with the "alias" dynamic field key filter, we gather the `Alias` object itself. + + + + +```rust file=/docs/examples/rust/stardust/foundry-output-claim.rs#L76-L89 +``` + + + + + + + +3. Some objects are owned by the `Alias` object (check the [Output unlockable by an Alias/Nft Address](address-unlock-condition.mdx) page for more info). In this case we filter them by type using the `CoinManagerTreasuryCap` type tag. + + + + +```rust file=/docs/examples/rust/stardust/foundry-output-claim.rs#L91-L131 +``` + + + + + + + +4. Since each native token has its own package, a Foundry's native token as a dedicated [`OTW`](../../iota-101/iota-move-concepts/one-time-witness.mdx). Here we need to extract this `OTW` from the `CoinManagerTreasuryCap` object. + + + + +```rust file=/docs/examples/rust/stardust/foundry-output-claim.rs#L133-L142 +``` + + + + + + + +5. Create a PTB that claims the `CoinManagerTreasuryCap` related to the Foundry Output from the `AliasOutput` using the `unlock_alias_address_owned_coinmanager_treasury` function. + + + + + +```rust file=/docs/examples/rust/stardust/foundry-output-claim.rs#L144-L215 +``` + + + + + + \ No newline at end of file diff --git a/docs/content/developer/stardust/claiming/nft.mdx b/docs/content/developer/stardust/claiming/nft.mdx new file mode 100644 index 00000000000..875cc5fbbb9 --- /dev/null +++ b/docs/content/developer/stardust/claiming/nft.mdx @@ -0,0 +1,88 @@ +--- + title: Claiming Nft Outputs +--- + +An address can own `NftOutput` objects that needs to be unlocked. +In this case, some off-chain queries can be used to check the unlock conditions of the `NftOutput` and assess if it can be unlocked. +Check the example above, as it works the same as the [`Basic Output`](basic.mdx). + +## Claim of an Nft Output +Once an Nft Output can be unlocked the claim of its assets can start + +1. The first step is to fetch the `NftOutput` object that needs to be claimed. + + + + +```rust file=/docs/examples/rust/stardust/nft-output-claim.rs#L56-L80 +``` + + + + + + + + +2. Then we check the native tokens that were possibly held by this output. A [`Bag`](../../../references/framework/iota-framework/bag) is used for holding these tokens, so in this step we are interested in obtaining the dynamic field keys that are used as bag index. In the case of the native tokens `Bag` the keys are strings representing the [`OTW`](../../iota-101/iota-move-concepts/one-time-witness.mdx) used for the native token [`Coin`](../../../references/framework/iota-framework/coin.mdx#resource-coin). + + + + +```rust file=/docs/examples/rust/stardust/nft-output-claim.rs#L82-L107 +``` + + + + + + + +3. Finally, a PTB can be created using the `nft_output` as input and the `Bag` keys for iterating the native tokens extracted. +An Nft Output is different from a Basic Output as it contains the `Nft` object. +In fact, the main purpose of claiming is extracting the `Nft` object from the `NftOutput`. + + + + +```rust file=/docs/examples/rust/stardust/nft-output-claim.rs#L109-L175 +``` + + + + + + + +## Conversion of an Nft Output into a custom Nft +This topic outlines the process of converting an stardust Nft into a custom Nft. + +You need to have a prepared custom Nft package that you want to convert the Stardust Nft You need to have a prepared custom Nft package that you want to convert the Stardust Nft into. +The following is an example of a simple module for representing a custom NFT, minting it, burning it and converting it from a Stardust `Nft`: + + + + +```move file=/docs/examples/move/custom_nft/sources/nft.move +``` + + + + + + +Create a PTB that extracts the Stardust `Nft` from an `NftOutput` and then converts it into a custom NFT of the collection just published. +This conversion method extracts the Stardust `Nft` metadata and uses it for minting a new NFT. + + + + +```rust file=/docs/examples/rust/stardust/nft-migration.rs#L77-L137 +``` + + + + + + + diff --git a/docs/content/developer/stardust/claiming/self-sponsor.mdx b/docs/content/developer/stardust/claiming/self-sponsor.mdx new file mode 100644 index 00000000000..8ecd7374180 --- /dev/null +++ b/docs/content/developer/stardust/claiming/self-sponsor.mdx @@ -0,0 +1,59 @@ +--- + title: Self-sponsoring Shimmer Assets Claiming +--- + +In the case in which an address owns some assets but no IOTA coins, the self-sponsorship can be of help for claiming those coins. +[Sponsoring a transaction](../../iota-101/transactions/sponsored-transactions) means having an IOTA address (i.e., the sponsor) to pay the transaction gas fees for another address (i.e., the user). In the case of a claim a sponsor can pay for the claiming transaction gas. + +This is useful for Shimmer assets, because none of the Move objects obtained from migrating the Shimmer Stardust Outputs contain any IOTA coin. It means that addresses owning these Objects have no IOTA coins to pay for gas. + +## Claim of a Shimmer Basic Output with self-sponsorship + +1. A Shimmer derivation path uses a `coin_type` (4219) which is different from the IOTA's one (4218). A user's self-sponsoring address could be then found using the IOTA `coin_type`. + + + + +```rust file=/docs/examples/rust/stardust/shimmer-self-sponsor.rs#L44-L50 +``` + + + + +TODO + + + + + +2. A PTB for claiming a `BasicOutput` owned by the derived Shimmer address can be created similarly to what is shown in [Basic Output](basic.mdx). + + + + +```rust file=/docs/examples/rust/stardust/shimmer-self-sponsor.rs#L85-L135 +``` + + + + +TODO + + + + +3. In this case, the transaction must be signed by both the sender address (i.e., the objects' owner) and the sponsor address. + + + + +```rust file=/docs/examples/rust/stardust/shimmer-self-sponsor.rs#L151-L177 +``` + + + + +TODO + + + \ No newline at end of file diff --git a/docs/content/developer/stardust/exchanges.mdx b/docs/content/developer/stardust/exchanges.mdx index 8e0a72587b8..e90107ddd52 100644 --- a/docs/content/developer/stardust/exchanges.mdx +++ b/docs/content/developer/stardust/exchanges.mdx @@ -38,7 +38,7 @@ The most common use case for exchanges and custody providers regarding integrati - All `Coin` objects can be freely transferred by the owner of this object. - Only the owner of the `Coin` object can interact with it. -- The only restriction that can be added to a `Coin` object is the optional blocking of transfers for addresses on a `DenyList`. This only applies to `Coin` objects that have been instantiated as [Regulated Coins](../standards/coin.mdx#regulated-coins). +- The only restriction that can be added to a `Coin` object is the optional blocking of transfers for addresses on a [`DenyList`](../iota-101/create-coin/regulated.mdx#deny-list). This only applies to `Coin` objects that have been instantiated as [Regulated Coins](../standards/coin.mdx#regulated-coins). * It is not possible to add other limiting functionality to `Coin` objects directly, like vesting or time-locks; this can only be done by wrapping the unrestricted `Coin` object within another restricting object. It's safe to assume that if you receive a `Coin` object, you can use it without limitations. - A `Coin` is tied to a `CoinMetadata` object containing `name`, `symbol`, `decimals`, `description`, and an `icon_url` - The holder of the TreasuryCap handles administrative tasks of a Coin like minting new tokens or changing metadata; Without a `TreasuryCap` these actions can no longer be performed. @@ -94,4 +94,4 @@ Gas fees to interact with custom tokens are paid in IOTA, like any other interac #### Integration -Depending on your preferences, you can integrate your exchange in multiple ways. You can go from a low-level implementation to directly talking to a node's RPC server or use one of our SDKs (the official TypeScript or Rust SDK is recommended). For details on how to integrate, please check out the [Exchange Integration Guide](../exchange-integration/exchange-integration.mdx). \ No newline at end of file +Depending on your preferences, you can integrate your exchange in multiple ways. You can go from a low-level implementation to directly talking to a node's RPC server or use one of our [SDKs](../../references/iota-sdks) (the official TypeScript or Rust SDK is recommended). For details on how to integrate, please check out the [Exchange Integration Guide](../exchange-integration/exchange-integration.mdx). \ No newline at end of file diff --git a/docs/content/developer/stardust/migration-process.mdx b/docs/content/developer/stardust/migration-process.mdx index cb1455fbf8e..65849b79f6f 100644 --- a/docs/content/developer/stardust/migration-process.mdx +++ b/docs/content/developer/stardust/migration-process.mdx @@ -28,7 +28,7 @@ migration. They will only be created in the genesis ledger state via the migrati ## Foundry Outputs & Native Tokens -Foundry Outputs in Stardust represent the capability to control the supply of user-defined Native Tokens. In Move, there +[Foundry Outputs](https://github.com/iotaledger/tips/blob/main/tips/TIP-0018/tip-0018.md#foundry-output) in Stardust represent the capability to control the supply of user-defined Native Tokens. In Move, there are established ways for these operations, in particular using [`Coin`](../../references/framework/iota-framework/coin.mdx#resource-coin) and [`TreasuryCap`](../../references/framework/iota-framework/coin.mdx#resource-treasurycap). The two main goals of the @@ -54,7 +54,7 @@ The result of the foundry migration is the following: - A package representing the native token, in particular containing a one-time witness representing the unique type of the native token (to be used as `Coin`; abbreviated in the rest of this document). -- A `CoinManager` and `CoinManagerTreasuryCap` object, the latter of which is owned by the address of the alias that +- A `CoinManager` and [`CoinManagerTreasuryCap`](../../references/framework/iota-framework/coin_manager.mdx#resource-coinmanagertreasurycap) object, the latter of which is owned by the address of the alias that owned the original foundry. - A minted coin (`Coin`) containing the entire circulating supply of the native tokens owned by the `0x0` address. diff --git a/docs/content/developer/stardust/move-models.mdx b/docs/content/developer/stardust/move-models.mdx index 4d909294bb7..b573792f87c 100644 --- a/docs/content/developer/stardust/move-models.mdx +++ b/docs/content/developer/stardust/move-models.mdx @@ -7,6 +7,19 @@ description: Describes how stardust assets are represented in the object-based M This document describes what move models the Stardust UTXOs are migrated to in the Move-based ledger in IOTA Rebased. First, the Stardust Move Package is discussed that emulates the legacy UTXO models in object-based move. + ## Summary + +| Stardust Output (UTXO) | Move Container or Migrated Object | Encapsulated Move Assets Inside the Container | +|--------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Basic Output with IOTA/SMR only | [`Coin`](../../references/framework/iota-framework/coin.mdx#resource-coin) | ready to be used, no need to extract any assets | +| Basic Output with IOTA/SMR and native tokens | [`Coin`](../../references/framework/iota-framework/coin.mdx#resource-coin) and one or more [`Coin`](../../references/framework/iota-framework/coin.mdx#resource-coin) | ready to be used, no need to extract any assets | +| Basic Output with unlock conditions or features | [`BasicOutput`](../../references/framework/stardust/basic_output#resource-basicoutput) | [`Balance`](../../references/framework/iota-framework/balance#struct-balance), [`Bag`](../../references/framework/iota-framework/bag#resource-bag) of `Balance` key-ed by `"NATIVE_TOKEN"` | +| Alias Output | [`AliasOutput`](../../references/framework/iota-framework/balance#struct-balance), [`Bag`](../../references/framework/iota-framework/bag#resource-bag) of `Balance` key-ed by `"NATIVE_TOKEN"`, [`Alias`](../../references/framework/stardust/alias#resource-alias) object | +| Nft Output | [`NftOutput`](../../references/framework/iota-framework/balance#struct-balance), [`Bag`](../../references/framework/iota-framework/bag#resource-bag) of `Balance` key-ed by `"NATIVE_TOKEN"`, [`Nft`](../../references/framework/stardust/nft#resource-nft) object | +| Foundry Output | Migrated to a Move Package with [template](https://github.com/iotaledger/iota/blob/develop/crates/iota-genesis-builder/src/stardust/native_token/package_template/sources/native_token_template.move) and [`CoinManager`](../../references/framework/iota-framework/coin_manager). Base tokens of the output are sent to the controlling alias as `Coin` objects. | - | +| [Vested Rewards From the Stardust Upgrade](https://github.com/iotaledger/new_supply) | [`Timelock>`](../../references/framework/iota-framework/timelock.mdx#resource-timelock) objects. | - | + + ## Design Principles - The migration to IOTA Rebased aims to move away from the Stardust-based ledger concepts and transform assets into their pure @@ -52,10 +65,10 @@ The following sections describe how the different asset types in Stardust are re You may find more information on the original Stardust ledger and asset types in [TIP-18](https://github.com/iotaledger/tips/blob/main/tips/TIP-0018/tip-0018.md). We consider the following Stardust asset types for the migration: - - Base tokens (IOTA/SMR), - - User defined native tokens, - - Non-Fungible Tokens (NFTs), and - - Aliases + - Base tokens (IOTA/SMR). + - User defined native tokens. + - Non-Fungible Tokens (NFTs). + - Aliases. ### Base Token (IOTA/SMR) @@ -64,7 +77,7 @@ represented instead as balance structs, parameterized with the type of the token - [`Balance`](../../references/framework/iota-framework/balance#struct-balance): Represents a balance of the IOTA token. - [`Balance`](../../references/framework/iota-framework/balance#struct-balance): Represents a balance the SMR token. - The token types `IOTA` and `SMR` are defined as part of the IOTA Move Framework. The fully qualified type for them is: - - [`0x2::iota::IOTA`](../../references/framework/iota-framework/iota#struct-iota) for IOTA, and + - [`0x2::iota::IOTA`](../../references/framework/iota-framework/iota#struct-iota) for IOTA. - [`0x2::smr::SMR`](../../references/framework/iota-framework/smr#struct-smr) for SMR. Balances are simply structs in move, therefore they need to be put into an object that users can own. The IOTA Move Framework @@ -90,8 +103,8 @@ Majority of the funds on both IOTA and SMR networks are held in basic outputs, w as this is the most common way a wallet stores IOTA/SMR tokens. To make the migration as seamless as possible, all [Basic outputs](https://github.com/iotaledger/tips/blob/main/tips/TIP-0018/tip-0018.md#basic-output) from the Stardust ledger that: - contain IOTA or SMR tokens only (no native tokens), - - have only an `Address Unlock Condition` and no other [unlock conditions](https://github.com/iotaledger/tips/blob/main/tips/TIP-0018/tip-0018.md#unlock-conditions), and - - have no [output features](https://github.com/iotaledger/tips/blob/main/tips/TIP-0018/tip-0018.md#features), + - have only an `Address Unlock Condition` and no other [unlock conditions](https://github.com/iotaledger/tips/blob/main/tips/TIP-0018/tip-0018.md#unlock-conditions), + - have no [output features](https://github.com/iotaledger/tips/blob/main/tips/TIP-0018/tip-0018.md#features) will be migrated to a `Coin` or `Coin` object, respectively. The `Coin` object will be owned by the address in the `Address Unlock Condition` of the Stardust output. @@ -255,7 +268,7 @@ can be touched by anyone, however the move level code ensures that only the righ ### Deprecated Stardust Unlock Conditions The following unlock conditions from Stardust are not supported in the move ledger and are therefore not migrated: - - [State Controller Address Unlock Condition](https://github.com/iotaledger/tips/blob/main/tips/TIP-0018/tip-0018.md#state-controller-address-unlock-condition) and + - [State Controller Address Unlock Condition](https://github.com/iotaledger/tips/blob/main/tips/TIP-0018/tip-0018.md#state-controller-address-unlock-condition). - [Governor Address Unlock Condition](https://github.com/iotaledger/tips/blob/main/tips/TIP-0018/tip-0018.md#governor-address-unlock-condition): Used in Stardust exclusively for aliases. The move ledger simplifies aliases and gives ownership to the governor address of the alias, as the governor has the right to replace any state controller. @@ -361,8 +374,8 @@ There are two ways to interact with `BasicOutputs` in move: The [`AliasOutput`](../../references/framework/stardust/alias_output#resource-aliasoutput) container contains the following assets: - - Base token balance (IOTA/SMR) of the migrated Stardust output, - - A `Bag` that holds native tokens of the migrated Stardust output, and + - Base token balance (IOTA/SMR) of the migrated Stardust output. + - A `Bag` that holds native tokens of the migrated Stardust output. - The [`Alias`](../../references/framework/stardust/alias#resource-alias) object that represents the alias in the move ledger. While this latter asset is not part of the container as a field, it is added to it as a [`dynamic object field`](../iota-101/objects/dynamic-fields/dynamic-fields.mdx) during the migration process. This has the added benefit that the previous `AliasID` of the Stardust output is preserved @@ -438,8 +451,8 @@ public fun extract_assets(mut output: AliasOutput): (Balance, Bag, Alia ### NftOutput Move Model The [`NftOutput`](../../references/framework/stardust/nft_output#resource-nftoutput) container contains the: - - Base token balance (IOTA/SMR) of the migrated Stardust output, - - A `Bag` that holds native tokens of the migrated Stardust output, and + - Base token balance (IOTA/SMR) of the migrated Stardust output. + - A `Bag` that holds native tokens of the migrated Stardust output. - The [`Nft`](../../references/framework/stardust/nft#resource-nft) object that represents the NFT in the move ledger. While this latter asset is not part of the container as a field, it is added to it as a [`dynamic object field`](../iota-101/objects/dynamic-fields/dynamic-fields.mdx) during the migration process. This has the added benefit that the previous `NftID` of the Stardust output is preserved @@ -518,18 +531,3 @@ public fun extract_assets(output: nft_output::NftOutput, ctx: &mut tx_cont - The caller has to decide what address to send the `Nft` object to. - The `NftOutput` container is destroyed in the process. - ## Summary - - -| Stardust Output (UTXO) | Move Container or Migrated Object | Encapsulated Move Assets Inside the Container | -|--------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Basic Output with IOTA/SMR only | [`Coin`](../../references/framework/iota-framework/coin.mdx#resource-coin) | ready to be used, no need to extract any assets | -| Basic Output with IOTA/SMR and native tokens | [`Coin`](../../references/framework/iota-framework/coin.mdx#resource-coin) and one or more [`Coin`](../../references/framework/iota-framework/coin.mdx#resource-coin) | ready to be used, no need to extract any assets | -| Basic Output with unlock conditions or features | [`BasicOutput`](../../references/framework/stardust/basic_output#resource-basicoutput) | [`Balance`](../../references/framework/iota-framework/balance#struct-balance), [`Bag`](../../references/framework/iota-framework/bag#resource-bag) of `Balance` key-ed by `"NATIVE_TOKEN"` | -| Alias Output | [`AliasOutput`](../../references/framework/iota-framework/balance#struct-balance), [`Bag`](../../references/framework/iota-framework/bag#resource-bag) of `Balance` key-ed by `"NATIVE_TOKEN"`, [`Alias`](../../references/framework/stardust/alias#resource-alias) object | -| Nft Output | [`NftOutput`](../../references/framework/iota-framework/balance#struct-balance), [`Bag`](../../references/framework/iota-framework/bag#resource-bag) of `Balance` key-ed by `"NATIVE_TOKEN"`, [`Nft`](../../references/framework/stardust/nft#resource-nft) object | -| Foundry Output | Migrated to a Move Package with [template](https://github.com/iotaledger/iota/blob/develop/crates/iota-genesis-builder/src/stardust/native_token/package_template/sources/native_token_template.move) and [`CoinManager`](../../references/framework/iota-framework/coin_manager). Base tokens of the output are sent to the controlling alias as `Coin` objects. | - | -| [Vested Rewards From the Stardust Upgrade](https://github.com/iotaledger/new_supply) | [`Timelock>`](../../references/framework/iota-framework/timelock.mdx#resource-timelock) objects. | - | - - - diff --git a/docs/content/sidebars/developer.js b/docs/content/sidebars/developer.js index 32bdc3b9875..6bac301044c 100644 --- a/docs/content/sidebars/developer.js +++ b/docs/content/sidebars/developer.js @@ -718,7 +718,46 @@ const developer = [ 'developer/stardust/addresses', 'developer/stardust/units', 'developer/stardust/migration-process', - 'developer/stardust/claiming', + { + type: 'category', + label: 'Claiming Stardust Assets', + link: { + type: 'doc', + id: 'developer/stardust/claiming', + }, + items: [ + { + type: 'doc', + label: 'Basic Outputs', + id: 'developer/stardust/claiming/basic', + }, + { + type: 'doc', + label: 'Nft Outputs', + id: 'developer/stardust/claiming/nft', + }, + { + type: 'doc', + label: 'Alias Outputs', + id: 'developer/stardust/claiming/alias', + }, + { + type: 'doc', + label: 'Foundry Outputs', + id: 'developer/stardust/claiming/foundry', + }, + { + type: 'doc', + label: 'Output unlockable by an Alias/Nft Address', + id: 'developer/stardust/claiming/address-unlock-condition', + }, + { + type: 'doc', + label: 'Self-sponsor Shimmer Claiming', + id: 'developer/stardust/claiming/self-sponsor', + }, + ], + }, 'developer/stardust/vested', 'developer/stardust/testing', 'developer/stardust/if-tools', diff --git a/docs/examples/move/custom_nft/Move.toml b/docs/examples/move/custom_nft/Move.toml new file mode 100644 index 00000000000..d7b360635d4 --- /dev/null +++ b/docs/examples/move/custom_nft/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "custom_nft" +edition = "2024.beta" + +[dependencies] +Iota = { local = "../../../../crates/iota-framework/packages/iota-framework" } +Stardust = { local = "../../../../crates/iota-framework/packages/stardust" } + +[addresses] +custom_nft = "0x0" diff --git a/docs/examples/move/custom_nft/sources/collection.move b/docs/examples/move/custom_nft/sources/collection.move new file mode 100644 index 00000000000..e7c675c0feb --- /dev/null +++ b/docs/examples/move/custom_nft/sources/collection.move @@ -0,0 +1,140 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module custom_nft::collection { + use std::string::String; + + use iota::event; + + use stardust::alias::Alias; + + // ===== Errors ===== + + /// For when someone tries to drop a `Collection` with a wrong capability. + const EWrongCollectionControllerCap: u64 = 0; + + // ===== Structures ===== + + /// A capability allowing the bearer to create or drop an NFT collection. + /// A `stardust::alias::Alias` instance can be converted into `CollectionControllerCap` in this example, + /// since an alias address could be used as a collections controller in Stardust. + /// + /// NOTE: To simplify the example, `CollectionControllerCap` is publicly transferable, but to make sure that it can be created, + /// dropped and owned only by the related `stardust::alias::Alias` owner, we can remove the `store` ability and transfer a created + /// capability to the sender in the constructor. + public struct CollectionControllerCap has key, store { + id: UID, + } + + /// An NFT collection. + /// Can be created by a `CollectionControllerCap` owner and used to mint collection-related NFTs. + /// Can be dropped only by it's `CollectionControllerCap` owner. Once a collection is dropped, + /// it is impossible to mint new collection-related NFTs. + /// + /// NOTE: To simplify the example, `Collection` is publicly transferable, but to make sure that it can be created, + /// dropped and owned only by the related `CollectionControllerCap` owner, we can remove the `store` ability and transfer a created + /// capability to the sender in the constructor. + public struct Collection has key, store { + id: UID, + /// The related `CollectionControllerCap` ID. + cap_id: ID, + /// The collection name. + name: String, + } + + // ===== Events ===== + + /// Event marking when a `stardust::alias::Alias` has been converted into `CollectionControllerCap`. + public struct StardustAliasConverted has copy, drop { + /// The `stardust::alias::Alias` ID. + alias_id: ID, + /// The `CollectionControllerCap` ID. + cap_id: ID, + } + + /// Event marking when a `CollectionControllerCap` has been dropped. + public struct CollectionControllerCapDropped has copy, drop { + /// The `CollectionControllerCap` ID. + cap_id: ID, + } + + /// Event marking when a `Collection` has been created. + public struct CollectionCreated has copy, drop { + /// The collection ID. + collection_id: ID, + } + + /// Event marking when a `Collection` has been dropped. + public struct CollectionDropped has copy, drop { + /// The collection ID. + collection_id: ID, + } + + // ===== Public view functions ===== + + /// Get the Collection's `name` + public fun name(nft: &Collection): &String { + &nft.name + } + + // ===== Entrypoints ===== + + /// Convert a `stardust::alias::Alias` into `CollectionControllerCap`. + public fun convert_alias_to_collection_controller_cap(stardust_alias: Alias, ctx: &mut TxContext): CollectionControllerCap { + let cap = CollectionControllerCap { + id: object::new(ctx) + }; + + event::emit(StardustAliasConverted { + alias_id: object::id(&stardust_alias), + cap_id: object::id(&cap), + }); + + stardust::alias::destroy(stardust_alias); + + cap + } + + /// Drop a `CollectionControllerCap` instance. + public fun drop_collection_controller_cap(cap: CollectionControllerCap) { + event::emit(CollectionControllerCapDropped { + cap_id: object::id(&cap), + }); + + let CollectionControllerCap { id } = cap; + + object::delete(id) + } + + /// Create a `Collection` instance. + public fun create_collection(cap: &CollectionControllerCap, name: String, ctx: &mut TxContext): Collection { + let collection = Collection { + id: object::new(ctx), + cap_id: object::id(cap), + name, + }; + + event::emit(CollectionCreated { + collection_id: object::id(&collection), + }); + + collection + } + + /// Drop a `Collection` instance. + public fun drop_collection(cap: &CollectionControllerCap, collection: Collection) { + assert!(object::borrow_id(cap) == &collection.cap_id, EWrongCollectionControllerCap); + + event::emit(CollectionDropped { + collection_id: object::id(&collection), + }); + + let Collection { + id, + cap_id: _, + name: _ + } = collection; + + object::delete(id) + } +} diff --git a/docs/examples/move/custom_nft/sources/nft.move b/docs/examples/move/custom_nft/sources/nft.move new file mode 100644 index 00000000000..5377a5cb6f2 --- /dev/null +++ b/docs/examples/move/custom_nft/sources/nft.move @@ -0,0 +1,136 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module custom_nft::nft { + use std::string::String; + + use iota::event; + use iota::url::Url; + + use custom_nft::collection::Collection; + + /// An example NFT that can be minted by anybody. + public struct Nft has key, store { + id: UID, + /// The token name. + name: String, + /// The token description. + description: Option, + /// The token URL. + url: Url, + /// The related collection name. + collection_name: Option + + // Allow custom attributes. + } + + // ===== Events ===== + + /// Event marking when an `Nft` has been minted. + public struct NftMinted has copy, drop { + /// The NFT id. + object_id: ID, + /// The NFT creator. + creator: address, + /// The NFT name. + name: String, + } + + // ===== Public view functions ===== + + /// Get the NFT's `name`. + public fun name(nft: &Nft): &String { + &nft.name + } + + /// Get the NFT's `description`. + public fun description(nft: &Nft): &Option { + &nft.description + } + + /// Get the NFT's `url`. + public fun url(nft: &Nft): &Url { + &nft.url + } + + // ===== Entrypoints ===== + + /// Convert a `stardust::nft::Nft` into `Nft`. + /// + /// The developer of the `custom_nft` package could tie minting to several conditions, for example: + /// - Only accept Stardust NFTs from a certain issuer, with a certain name/collection name, `NftId` even. + /// - Only the `immutable_issuer` and `id` fields count as proof for an NFT belonging to the original collection. + /// + /// The developer could technically mint the same NFT on the running Stardust network before the mainnet switch + /// and fake the name and metadata. + public fun convert(stardust_nft: stardust::nft::Nft, ctx: &mut TxContext): Nft { + let nft_metadata = stardust_nft.immutable_metadata(); + + let nft = mint( + *nft_metadata.name(), + *nft_metadata.description(), + *nft_metadata.uri(), + *nft_metadata.collection_name(), + ctx + ); + + stardust::nft::destroy(stardust_nft); + + nft + } + + /// Mint a collection-related NFT. + public fun mint_collection_related( + collection: &Collection, + name: String, + description: String, + url: Url, + ctx: &mut TxContext + ): Nft { + mint( + name, + option::some(description), + url, + option::some(*collection.name()), + ctx + ) + } + + /// Create a new `Nft` instance. + fun mint( + name: String, + description: Option, + url: Url, + collection_name: Option, + ctx: &mut TxContext + ): Nft { + let nft = Nft { + id: object::new(ctx), + name, + description, + url, + collection_name + }; + + event::emit(NftMinted { + object_id: object::id(&nft), + creator: ctx.sender(), + name: nft.name, + }); + + nft + } + + /// Permanently delete the `Nft` instance. + public fun burn(nft: Nft) { + let Nft { + id, + name: _, + description: _, + url: _, + collection_name: _ + } = nft; + + object::delete(id) + } +} diff --git a/docs/examples/rust/Cargo.toml b/docs/examples/rust/Cargo.toml new file mode 100644 index 00000000000..f4741516b5f --- /dev/null +++ b/docs/examples/rust/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "docs-examples" +version.workspace = true +authors = ["IOTA Stiftung"] +edition = "2021" +license = "Apache-2.0" +publish = false + +[dependencies] +anyhow.workspace = true + +iota-keys.workspace = true +iota-move-build.workspace = true +iota-sdk.workspace = true +shared-crypto.workspace = true + +[dev-dependencies] +bcs.workspace = true +bip32.workspace = true +serde_json.workspace = true +tokio.workspace = true + +move-core-types.workspace = true + +[[example]] +name = "address-unlock-condition" +path = "stardust/address-unlock-condition.rs" + +[[example]] +name = "nft-output-claim" +path = "stardust/nft-output-claim.rs" + +[[example]] +name = "alias-output-claim" +path = "stardust/alias-output-claim.rs" + +[[example]] +name = "basic-output-claim" +path = "stardust/basic-output-claim.rs" + +[[example]] +name = "check-basic-output-unlock-conditions" +path = "stardust/check-basic-output-unlock-conditions.rs" + +[[example]] +name = "alias-migration" +path = "stardust/alias-migration.rs" + +[[example]] +name = "nft-migration" +path = "stardust/nft-migration.rs" + +[[example]] +name = "foundry-output-claim" +path = "stardust/foundry-output-claim.rs" + +[[example]] +name = "shimmer-self-sponsor" +path = "stardust/shimmer-self-sponsor.rs" diff --git a/docs/examples/rust/src/lib.rs b/docs/examples/rust/src/lib.rs new file mode 100644 index 00000000000..0d76a1a1e6e --- /dev/null +++ b/docs/examples/rust/src/lib.rs @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod utils; diff --git a/docs/examples/rust/src/utils.rs b/docs/examples/rust/src/utils.rs new file mode 100644 index 00000000000..b8eb717c629 --- /dev/null +++ b/docs/examples/rust/src/utils.rs @@ -0,0 +1,194 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! A set of utility functions for the examples. + +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{anyhow, Result}; +use iota_keys::keystore::{AccountKeystore, FileBasedKeystore}; +use iota_move_build::BuildConfig; +use iota_sdk::{ + rpc_types::{IotaTransactionBlockEffectsAPI, IotaTransactionBlockResponseOptions}, + types::{ + base_types::{IotaAddress, ObjectID}, + crypto::SignatureScheme::ED25519, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + transaction::{Transaction, TransactionData}, + }, + IotaClient, +}; +use shared_crypto::intent::Intent; + +/// Got from iota-genesis-builder/src/stardust/test_outputs/stardust_mix.rs +const SPONSOR_ADDRESS_MNEMONIC: &str = "okay pottery arch air egg very cave cash poem gown sorry mind poem crack dawn wet car pink extra crane hen bar boring salt"; + +/// Move Custom NFT example relative path +const CUSTOM_NFT_PACKAGE_PATH: &str = "../move/custom_nft"; + +/// Creates a temporary keystore. +pub fn setup_keystore() -> Result { + let keystore_path = PathBuf::from("iotatempdb"); + if !keystore_path.exists() { + let keystore = FileBasedKeystore::new(&keystore_path)?; + keystore.save()?; + } + // Read iota keystore + FileBasedKeystore::new(&keystore_path) +} + +/// Deletes the temporary keystore. +pub fn clean_keystore() -> Result<(), anyhow::Error> { + // Remove files + fs::remove_file("iotatempdb")?; + fs::remove_file("iotatempdb.aliases")?; + Ok(()) +} + +/// Utility function for funding an address using the transfer of a coin. +pub async fn fund_address( + iota_client: &IotaClient, + keystore: &mut FileBasedKeystore, + recipient: IotaAddress, +) -> Result<(), anyhow::Error> { + // Derive the address of the sponsor. + let sponsor = keystore.import_from_mnemonic(SPONSOR_ADDRESS_MNEMONIC, ED25519, None)?; + + println!("Sponsor address: {sponsor:?}"); + + // Get a gas coin. + let gas_coin = iota_client + .coin_read_api() + .get_coins(sponsor, None, None, None) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("No coins found for sponsor"))?; + + let pt = { + // Init a programmable transaction builder. + let mut builder = ProgrammableTransactionBuilder::new(); + // Pay all iotas from the gas object + builder.pay_all_iota(recipient); + builder.finish() + }; + + // Setup a gas budget and a gas price. + let gas_budget = 10_000_000; + let gas_price = iota_client.read_api().get_reference_gas_price().await?; + + // Create a transaction data that will be sent to the network. + let tx_data = TransactionData::new_programmable( + sponsor, + vec![gas_coin.object_ref()], + pt, + gas_budget, + gas_price, + ); + + // Sign the transaction. + let signature = keystore.sign_secure(&sponsor, &tx_data, Intent::iota_transaction())?; + + // Execute the transaction. + let transaction_response = iota_client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + println!( + "Funding transaction digest: {}", + transaction_response.digest + ); + + Ok(()) +} + +/// Utility function for publishing a custom NFT package found in the Move +/// examples. +pub async fn publish_custom_nft_package( + sender: IotaAddress, + keystore: &mut FileBasedKeystore, + iota_client: &IotaClient, +) -> Result { + // Get a gas coin + let gas_coin = iota_client + .coin_read_api() + .get_coins(sender, None, None, None) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("No coins found"))?; + + // Build custom nft package + let package_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(CUSTOM_NFT_PACKAGE_PATH); + let compiled_package = BuildConfig::default().build(package_path)?; + let modules = compiled_package + .get_modules() + .map(|module| { + let mut buf = Vec::new(); + module.serialize(&mut buf)?; + Ok(buf) + }) + .collect::>>>()?; + let dependencies = compiled_package.get_dependency_original_package_ids(); + + // Publish package + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + builder.publish_immutable(modules, dependencies); + builder.finish() + }; + + // Setup gas budget and gas price + let gas_budget = 50_000_000; + let gas_price = iota_client.read_api().get_reference_gas_price().await?; + + // Create the transaction data that will be sent to the network + let tx_data = TransactionData::new_programmable( + sender, + vec![gas_coin.object_ref()], + pt, + gas_budget, + gas_price, + ); + + // Sign the transaction + let signature = keystore.sign_secure(&sender, &tx_data, Intent::iota_transaction())?; + + // Execute transaction + let transaction_response = iota_client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + println!( + "Package publishing transaction digest: {}", + transaction_response.digest + ); + + // Extract package id from the transaction effects + let tx_effects = transaction_response + .effects + .expect("Transaction has no effects"); + let package_ref = tx_effects + .created() + .first() + .expect("There are no created objects"); + let package_id = package_ref.reference.object_id; + println!("Package ID: {package_id}"); + Ok(package_id) +} diff --git a/docs/examples/rust/stardust/address-unlock-condition.rs b/docs/examples/rust/stardust/address-unlock-condition.rs new file mode 100644 index 00000000000..e3383b79de0 --- /dev/null +++ b/docs/examples/rust/stardust/address-unlock-condition.rs @@ -0,0 +1,271 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Example demonstrating how to unlock an output owned by an alias output. +//! In order to work, it requires a network with test objects +//! generated from iota-genesis-builder/src/stardust/test_outputs. + +use std::str::FromStr; + +use anyhow::anyhow; +use bip32::DerivationPath; +use docs_examples::utils::{clean_keystore, fund_address, setup_keystore}; +use iota_keys::keystore::AccountKeystore; +use iota_sdk::{ + rpc_types::{ + IotaObjectDataFilter, IotaObjectDataOptions, IotaObjectResponseQuery, + IotaTransactionBlockResponseOptions, + }, + types::{ + base_types::ObjectID, + crypto::SignatureScheme::ED25519, + dynamic_field::DynamicFieldName, + gas_coin::GAS, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + stardust::output::NftOutput, + transaction::{Argument, ObjectArg, Transaction, TransactionData}, + TypeTag, IOTA_FRAMEWORK_ADDRESS, STARDUST_ADDRESS, + }, + IotaClientBuilder, +}; +use move_core_types::ident_str; +use shared_crypto::intent::Intent; + +/// Got from iota-genesis-builder/src/stardust/test_outputs/alias_ownership.rs +const MAIN_ADDRESS_MNEMONIC: &str = "few hood high omit camp keep burger give happy iron evolve draft few dawn pulp jazz box dash load snake gown bag draft car"; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Build an iota client for a local network + let iota_client = IotaClientBuilder::default().build_localnet().await?; + + // Setup the temporary file based keystore + let mut keystore = setup_keystore()?; + + // For this example we need to derive an address that is not at index 0. This + // because we need an alias output that owns an Nft Output. In this case, we can + // derive the address index "/2'" of the "/0'" account. + let derivation_path = DerivationPath::from_str("m/44'/4218'/0'/0'/2'")?; + println!("{derivation_path:?}"); + + // Derive the address of the first account and set it as default + let sender = + keystore.import_from_mnemonic(MAIN_ADDRESS_MNEMONIC, ED25519, Some(derivation_path))?; + + println!("Sender address - {sender:?}"); + + fund_address(&iota_client, &mut keystore, sender).await?; + + // Get a gas coin + let gas_coin = iota_client + .coin_read_api() + .get_coins(sender, None, None, None) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("No coins found"))?; + + // This object id was fetched manually. It refers to an Alias Output object that + // owns a NftOutput. + let alias_output_object_id = ObjectID::from_hex_literal( + "0x3b35e67750b8e4ccb45b2fc4a6a26a6d97e74c37a532f17177e6324ab93eaca6", + )?; + + let alias_output_object = iota_client + .read_api() + .get_object_with_options( + alias_output_object_id, + IotaObjectDataOptions::new().with_bcs(), + ) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("Alias output not found"))?; + + let alias_output_object_ref = alias_output_object.object_ref(); + + // 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 + let df_name = DynamicFieldName { + type_: TypeTag::Vector(Box::new(TypeTag::U8)), + value: serde_json::Value::String("alias".to_string()), + }; + let alias_object = iota_client + .read_api() + .get_dynamic_field_object(alias_output_object_id, df_name) + .await? + .data + .ok_or(anyhow!("alias not found"))?; + let alias_object_address = alias_object.object_ref().0; + + // Some objects are owned by the Alias object. In this case we filter them by + // type using the NftOutput type. + let owned_objects_query_filter = + IotaObjectDataFilter::StructType(NftOutput::tag(GAS::type_tag())); + let owned_objects_query = IotaObjectResponseQuery::new(Some(owned_objects_query_filter), None); + + // Get the first NftOutput found + let nft_output_object_owned_by_alias = iota_client + .read_api() + .get_owned_objects( + alias_object_address.into(), + Some(owned_objects_query), + None, + None, + ) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("Owned nft outputs not found"))? + .data + .ok_or(anyhow!("Nft output data not found"))?; + + let nft_output_object_ref = nft_output_object_owned_by_alias.object_ref(); + + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + + // Extract alias output assets + let type_arguments = vec![GAS::type_tag()]; + let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(alias_output_object_ref))?]; + if let Argument::Result(extracted_alias_output_assets) = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("alias_output").to_owned(), + ident_str!("extract_assets").to_owned(), + type_arguments, + arguments, + ) { + let extracted_base_token = Argument::NestedResult(extracted_alias_output_assets, 0); + let extracted_native_tokens_bag = + Argument::NestedResult(extracted_alias_output_assets, 1); + let alias = Argument::NestedResult(extracted_alias_output_assets, 2); + + let type_arguments = vec![GAS::type_tag()]; + let arguments = vec![extracted_base_token]; + + // Extract the IOTA balance. + let iota_coin = builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("coin").to_owned(), + ident_str!("from_balance").to_owned(), + type_arguments, + arguments, + ); + + // Transfer the IOTA balance to the sender. + builder.transfer_arg(sender, iota_coin); + + // Cleanup the bag. + let arguments = vec![extracted_native_tokens_bag]; + builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("bag").to_owned(), + ident_str!("destroy_empty").to_owned(), + vec![], + arguments, + ); + + // Unlock the nft output. + let type_arguments = vec![GAS::type_tag()]; + let arguments = vec![ + alias, + builder.obj(ObjectArg::Receiving(nft_output_object_ref))?, + ]; + + let nft_output = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("address_unlock_condition").to_owned(), + ident_str!("unlock_alias_address_owned_nft").to_owned(), + type_arguments, + arguments, + ); + + // Transferring alias asset + builder.transfer_arg(sender, alias); + + // Extract nft assets(base token, native tokens bag, nft asset itself). + let type_arguments = vec![GAS::type_tag()]; + let arguments = vec![nft_output]; + // Finally call the nft_output::extract_assets function + if let Argument::Result(extracted_assets) = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("nft_output").to_owned(), + ident_str!("extract_assets").to_owned(), + type_arguments, + arguments, + ) { + // If the nft output can be unlocked, the command will be succesful and will + // return a `base_token` (i.e., IOTA) balance and a `Bag` of native tokens and + // related nft object. + let extracted_base_token = Argument::NestedResult(extracted_assets, 0); + let extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1); + let nft_asset = Argument::NestedResult(extracted_assets, 2); + + let type_arguments = vec![GAS::type_tag()]; + let arguments = vec![extracted_base_token]; + + // Extract the IOTA balance. + let iota_coin = builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("coin").to_owned(), + ident_str!("from_balance").to_owned(), + type_arguments, + arguments, + ); + + // Transfer the IOTA balance to the sender. + builder.transfer_arg(sender, iota_coin); + + // Cleanup the bag because it is empty. + let arguments = vec![extracted_native_tokens_bag]; + builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("bag").to_owned(), + ident_str!("destroy_empty").to_owned(), + vec![], + arguments, + ); + + // Transferring nft asset + builder.transfer_arg(sender, nft_asset); + } + } + builder.finish() + }; + + // Setup gas budget and gas price + let gas_budget = 10_000_000; + let gas_price = iota_client.read_api().get_reference_gas_price().await?; + + // Create the transaction data that will be sent to the network + let tx_data = TransactionData::new_programmable( + sender, + vec![gas_coin.object_ref()], + pt, + gas_budget, + gas_price, + ); + + // Sign the transaction + let signature = keystore.sign_secure(&sender, &tx_data, Intent::iota_transaction())?; + + // Execute transaction + let transaction_response = iota_client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + println!("Transaction digest: {}", transaction_response.digest); + + // Finish and clean the temporary keystore file + clean_keystore() +} diff --git a/docs/examples/rust/stardust/alias-migration.rs b/docs/examples/rust/stardust/alias-migration.rs new file mode 100644 index 00000000000..035c6de6090 --- /dev/null +++ b/docs/examples/rust/stardust/alias-migration.rs @@ -0,0 +1,276 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Example demonstrating the conversion of a stardust Alias into a custom +//! user's NFT collections controller. In order to work, it requires a network +//! with test objects generated from +//! iota-genesis-builder/src/stardust/test_outputs. + +use std::str::FromStr; + +use anyhow::{anyhow, Result}; +use docs_examples::utils::{clean_keystore, publish_custom_nft_package, setup_keystore}; +use iota_keys::keystore::AccountKeystore; +use iota_sdk::{ + rpc_types::{IotaData, IotaObjectDataOptions, IotaTransactionBlockResponseOptions}, + types::{ + base_types::ObjectID, + crypto::SignatureScheme::ED25519, + gas_coin::GAS, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + stardust::output::AliasOutput, + transaction::{Argument, CallArg, ObjectArg, Transaction, TransactionData}, + TypeTag, IOTA_FRAMEWORK_PACKAGE_ID, STARDUST_PACKAGE_ID, + }, + IotaClientBuilder, +}; +use move_core_types::ident_str; +use shared_crypto::intent::Intent; + +/// Got from iota-genesis-builder/src/stardust/test_outputs/stardust_mix.rs +const MAIN_ADDRESS_MNEMONIC: &str = "okay pottery arch air egg very cave cash poem gown sorry mind poem crack dawn wet car pink extra crane hen bar boring salt"; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Build an iota client for a local network + let iota_client = IotaClientBuilder::default().build_localnet().await?; + + // Setup the temporary file based keystore + let mut keystore = setup_keystore()?; + + // Derive the address of the first account and set it as default + let sender = keystore.import_from_mnemonic(MAIN_ADDRESS_MNEMONIC, ED25519, None)?; + + println!("{sender:?}"); + + // Publish the package of a custom NFT collection and then get the package id. + // The custom NFT module is obtained from a Move example in the docs. + // It is the same used in the Nft migration example. + let custom_nft_package_id = + publish_custom_nft_package(sender, &mut keystore, &iota_client).await?; + + // Get a gas coin + let gas_coin = iota_client + .coin_read_api() + .get_coins(sender, None, None, None) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("No coins found"))?; + + // Get an AliasOutput object id + let alias_output_object_id = ObjectID::from_hex_literal( + "0x354a1864c8af23fde393f7603bc133f755a9405353b30878e41b929eb7e37554", + )?; + + // Get an AliasOutput object + let alias_output_object = iota_client + .read_api() + .get_object_with_options( + alias_output_object_id, + IotaObjectDataOptions::new().with_bcs(), + ) + .await? + .data + .ok_or(anyhow!("Alias output not found"))?; + let alias_output_object_ref = alias_output_object.object_ref(); + + // Convert the AliasOutput object into its Rust representation. + let alias_output = bcs::from_bytes::( + &alias_output_object + .bcs + .expect("should contain bcs") + .try_as_move() + .expect("should convert it to a move object") + .bcs_bytes, + )?; + + // Extract the keys of the native_tokens bag if it is not empty; the keys + // are the type_arg of each native token, so they can be used later in the PTB. + let mut df_type_keys: Vec = vec![]; + let native_token_bag = alias_output.native_tokens; + if native_token_bag.size > 0 { + // Get the dynamic fields owned by the native tokens bag. + let dynamic_field_page = iota_client + .read_api() + .get_dynamic_fields(*native_token_bag.id.object_id(), None, None) + .await?; + // Only one page should exist. + assert!(!dynamic_field_page.has_next_page); + + // Extract the dynamic fields keys, i.e., the native token type. + df_type_keys.extend( + dynamic_field_page + .data + .into_iter() + .map(|dyi| { + dyi.name + .value + .as_str() + .expect("should be a string") + .to_string() + }) + .collect::>(), + ); + } + + // Create a PTB that extracts the related stardust Alias from the AliasOutput + // and then calls the + // `custom_nft::collection::convert_alias_to_collection_controller_cap` function + // to convert it into an NFT collection controller, create a collection and mint + // a few NFTs. + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + + let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(alias_output_object_ref))?]; + // Call the nft_output::extract_assets function + if let Argument::Result(extracted_assets) = builder.programmable_move_call( + STARDUST_PACKAGE_ID, + ident_str!("alias_output").to_owned(), + ident_str!("extract_assets").to_owned(), + vec![GAS::type_tag()], + arguments, + ) { + // The alias output can always be unlocked by the governor address. So the + // command will be successful and will return a `base_token` (i.e., IOTA) + // balance, a `Bag` of the related native tokens and the related Alias object. + let extracted_base_token = Argument::NestedResult(extracted_assets, 0); + let mut extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1); + let alias_asset = Argument::NestedResult(extracted_assets, 2); + + // Call the conversion function to create an NFT collection controller from the + // extracted alias. + let nft_collection_controller = builder.programmable_move_call( + custom_nft_package_id, + ident_str!("collection").to_owned(), + ident_str!("convert_alias_to_collection_controller_cap").to_owned(), + vec![], + vec![alias_asset], + ); + + // Create an NFT collection + let nft_collection_name = builder + .input(CallArg::Pure(bcs::to_bytes("Collection name").unwrap())) + .unwrap(); + + let nft_collection = builder.programmable_move_call( + custom_nft_package_id, + ident_str!("collection").to_owned(), + ident_str!("create_collection").to_owned(), + vec![], + vec![nft_collection_controller, nft_collection_name], + ); + + // Mint a collection-related NFT + let nft_name = builder + .input(CallArg::Pure(bcs::to_bytes("NFT name").unwrap())) + .unwrap(); + let nft_description = builder + .input(CallArg::Pure(bcs::to_bytes("NFT description").unwrap())) + .unwrap(); + let nft_url_value = builder + .input(CallArg::Pure(bcs::to_bytes("NFT URL").unwrap())) + .unwrap(); + let nft_url = builder.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("url").to_owned(), + ident_str!("new_unsafe").to_owned(), + vec![], + vec![nft_url_value], + ); + + let nft = builder.programmable_move_call( + custom_nft_package_id, + ident_str!("nft").to_owned(), + ident_str!("mint_collection_related").to_owned(), + vec![], + vec![nft_collection, nft_name, nft_description, nft_url], + ); + + // Transfer the NFT + builder.transfer_arg(sender, nft); + + // Drop the NFT collection to make impossible to mint new related NFTs + builder.programmable_move_call( + custom_nft_package_id, + ident_str!("collection").to_owned(), + ident_str!("drop_collection").to_owned(), + vec![], + vec![nft_collection_controller, nft_collection], + ); + + // Transfer the NFT collection controller + builder.transfer_arg(sender, nft_collection_controller); + + // Extract IOTA balance + let iota_coin = builder.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("coin").to_owned(), + ident_str!("from_balance").to_owned(), + vec![GAS::type_tag()], + vec![extracted_base_token], + ); + + // Transfer IOTA balance + builder.transfer_arg(sender, iota_coin); + + // Extract the native tokens from the bag. + for type_key in df_type_keys { + let type_arguments = vec![TypeTag::from_str(&format!("0x{type_key}"))?]; + let arguments = vec![extracted_native_tokens_bag, builder.pure(sender)?]; + + // Extract a native token balance. + extracted_native_tokens_bag = builder.programmable_move_call( + STARDUST_PACKAGE_ID, + ident_str!("utilities").to_owned(), + ident_str!("extract_and_send_to").to_owned(), + type_arguments, + arguments, + ); + } + + // Cleanup bag. + builder.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("bag").to_owned(), + ident_str!("destroy_empty").to_owned(), + vec![], + vec![extracted_native_tokens_bag], + ); + } + builder.finish() + }; + + // Setup gas budget and gas price + let gas_budget = 10_000_000; + let gas_price = iota_client.read_api().get_reference_gas_price().await?; + + // Create the transaction data that will be sent to the network + let tx_data = TransactionData::new_programmable( + sender, + vec![gas_coin.object_ref()], + pt, + gas_budget, + gas_price, + ); + + // Sign the transaction + let signature = keystore.sign_secure(&sender, &tx_data, Intent::iota_transaction())?; + + // Execute transaction + let transaction_response = iota_client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + println!("Transaction digest: {}", transaction_response.digest); + + // Finish and clean the temporary keystore file + clean_keystore() +} diff --git a/docs/examples/rust/stardust/alias-output-claim.rs b/docs/examples/rust/stardust/alias-output-claim.rs new file mode 100644 index 00000000000..d8613caf3c0 --- /dev/null +++ b/docs/examples/rust/stardust/alias-output-claim.rs @@ -0,0 +1,212 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Example demonstrating the claim of an alias output. +//! In order to work, it requires a network with test objects +//! generated from iota-genesis-builder/src/stardust/test_outputs. + +use std::str::FromStr; + +use anyhow::anyhow; +use docs_examples::utils::{clean_keystore, setup_keystore}; +use iota_keys::keystore::AccountKeystore; +use iota_sdk::{ + rpc_types::{IotaData, IotaObjectDataOptions, IotaTransactionBlockResponseOptions}, + types::{ + base_types::ObjectID, + crypto::SignatureScheme::ED25519, + gas_coin::GAS, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + stardust::output::AliasOutput, + transaction::{Argument, ObjectArg, Transaction, TransactionData}, + TypeTag, IOTA_FRAMEWORK_ADDRESS, STARDUST_ADDRESS, + }, + IotaClientBuilder, +}; +use move_core_types::ident_str; +use shared_crypto::intent::Intent; + +/// Got from iota-genesis-builder/src/stardust/test_outputs/stardust_mix.rs +const MAIN_ADDRESS_MNEMONIC: &str = "okay pottery arch air egg very cave cash poem gown sorry mind poem crack dawn wet car pink extra crane hen bar boring salt"; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Build an IOTA client for a local network. + let iota_client = IotaClientBuilder::default().build_localnet().await?; + + // Setup a temporary file based keystore. + let mut keystore = setup_keystore()?; + + // Derive the address of the first account and set it as default. + let sender = keystore.import_from_mnemonic(MAIN_ADDRESS_MNEMONIC, ED25519, None)?; + + println!("Sender address: {sender:?}"); + + // Get a gas coin. + let gas_coin = iota_client + .coin_read_api() + .get_coins(sender, None, None, None) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("No coins found for sponsor"))?; + + // Get an AliasOutput object. + let alias_output_object_id = ObjectID::from_hex_literal( + "0x354a1864c8af23fde393f7603bc133f755a9405353b30878e41b929eb7e37554", + )?; + let alias_output_object = iota_client + .read_api() + .get_object_with_options( + alias_output_object_id, + IotaObjectDataOptions::new().with_bcs(), + ) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("alias not found"))?; + let alias_output_object_ref = alias_output_object.object_ref(); + + // Convert the AliasOutput object into its Rust representation. + let alias_output = bcs::from_bytes::( + &alias_output_object + .bcs + .expect("should contain bcs") + .try_as_move() + .expect("should convert it to a move object") + .bcs_bytes, + )?; + + // Extract the keys of the native_tokens bag if it is not empty; the keys + // are the type_arg of each native token, so they can be used later in the PTB. + let mut df_type_keys = vec![]; + let native_token_bag = alias_output.native_tokens; + if native_token_bag.size > 0 { + // Get the dynamic fields owned by the native tokens bag. + let dynamic_field_page = iota_client + .read_api() + .get_dynamic_fields(*native_token_bag.id.object_id(), None, None) + .await?; + // Only one page should exist. + assert!(!dynamic_field_page.has_next_page); + + // Extract the dynamic fields keys, i.e., the native token type. + df_type_keys.extend( + dynamic_field_page + .data + .into_iter() + .map(|dyi| { + dyi.name + .value + .as_str() + .expect("should be a string") + .to_string() + }) + .collect::>(), + ); + } + + // Create a PTB to claim the assets related to the alias output. + let pt = { + // Init a programmable transaction builder. + let mut builder = ProgrammableTransactionBuilder::new(); + + // Type argument for an AliasOutput coming from the IOTA network, i.e., the + // IOTA token or the Gas type tag. + let type_arguments = vec![GAS::type_tag()]; + // Then pass the AliasOutput object as an input. + let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(alias_output_object_ref))?]; + // Finally call the alias_output::extract_assets function. + if let Argument::Result(extracted_assets) = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("alias_output").to_owned(), + ident_str!("extract_assets").to_owned(), + type_arguments, + arguments, + ) { + // The alias output can always be unlocked by the governor address. So the + // command will be successful and will return a `base_token` (i.e., IOTA) + // balance, a `Bag` of the related native tokens and the related Alias object. + let extracted_base_token = Argument::NestedResult(extracted_assets, 0); + let mut extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1); + let extracted_alias = Argument::NestedResult(extracted_assets, 2); + + // Extract the IOTA balance. + let type_arguments = vec![GAS::type_tag()]; + let arguments = vec![extracted_base_token]; + let iota_coin = builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("coin").to_owned(), + ident_str!("from_balance").to_owned(), + type_arguments, + arguments, + ); + + // Transfer the IOTA balance to the sender. + builder.transfer_arg(sender, iota_coin); + + // Extract the native tokens from the bag. + for type_key in df_type_keys { + let type_arguments = vec![TypeTag::from_str(&format!("0x{type_key}"))?]; + let arguments = vec![extracted_native_tokens_bag, builder.pure(sender)?]; + + // Extract a native token balance. + extracted_native_tokens_bag = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("utilities").to_owned(), + ident_str!("extract_and_send_to").to_owned(), + type_arguments, + arguments, + ); + } + + // Cleanup the bag. + let arguments = vec![extracted_native_tokens_bag]; + builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("bag").to_owned(), + ident_str!("destroy_empty").to_owned(), + vec![], + arguments, + ); + + // Transfer the alias asset. + builder.transfer_arg(sender, extracted_alias); + } + builder.finish() + }; + + // Setup a gas budget and a gas price. + let gas_budget = 10_000_000; + let gas_price = iota_client.read_api().get_reference_gas_price().await?; + + // Create a transaction data that will be sent to the network. + let tx_data = TransactionData::new_programmable( + sender, + vec![gas_coin.object_ref()], + pt, + gas_budget, + gas_price, + ); + + // Sign the transaction. + let signature = keystore.sign_secure(&sender, &tx_data, Intent::iota_transaction())?; + + // Execute the transaction. + let transaction_response = iota_client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + println!("Transaction digest: {}", transaction_response.digest); + + // Finish and clean the temporary keystore file. + clean_keystore() +} diff --git a/docs/examples/rust/stardust/basic-output-claim.rs b/docs/examples/rust/stardust/basic-output-claim.rs new file mode 100644 index 00000000000..eaf0aecd916 --- /dev/null +++ b/docs/examples/rust/stardust/basic-output-claim.rs @@ -0,0 +1,209 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Example demonstrating the claim of a basic output. +//! In order to work, it requires a network with test objects +//! generated from iota-genesis-builder/src/stardust/test_outputs. + +use std::str::FromStr; + +use anyhow::anyhow; +use docs_examples::utils::{clean_keystore, setup_keystore}; +use iota_keys::keystore::AccountKeystore; +use iota_sdk::{ + rpc_types::{IotaData, IotaObjectDataOptions, IotaTransactionBlockResponseOptions}, + types::{ + base_types::ObjectID, + crypto::SignatureScheme::ED25519, + gas_coin::GAS, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + stardust::output::BasicOutput, + transaction::{Argument, ObjectArg, Transaction, TransactionData}, + TypeTag, IOTA_FRAMEWORK_ADDRESS, STARDUST_ADDRESS, + }, + IotaClientBuilder, +}; +use move_core_types::ident_str; +use shared_crypto::intent::Intent; + +/// Got from iota-genesis-builder/src/stardust/test_outputs/stardust_mix.rs +const MAIN_ADDRESS_MNEMONIC: &str = "rain flip mad lamp owner siren tower buddy wolf shy tray exit glad come dry tent they pond wrist web cliff mixed seek drum"; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Build an iota client for a local network + let iota_client = IotaClientBuilder::default().build_localnet().await?; + + // Setup the temporary file based keystore + let mut keystore = setup_keystore()?; + + // Derive the address of the first account and set it as default + let sender = keystore.import_from_mnemonic(MAIN_ADDRESS_MNEMONIC, ED25519, None)?; + + println!("Sender address: {sender:?}"); + + // Get a gas coin + let gas_coin = iota_client + .coin_read_api() + .get_coins(sender, None, None, None) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("No coins found for sender"))?; + + // This object id was fetched manually. It refers to a Basic Output object that + // contains some Native Tokens. + let basic_output_object_id = ObjectID::from_hex_literal( + "0xde09139ed46b9f5f876671e4403f312fad867c5ae5d300a252e4b6a6f1fa1fbd", + )?; + // Get Basic Output object + let basic_output_object = iota_client + .read_api() + .get_object_with_options( + basic_output_object_id, + IotaObjectDataOptions::new().with_bcs(), + ) + .await? + .data + .ok_or(anyhow!("Basic output not found"))?; + let basic_output_object_ref = basic_output_object.object_ref(); + + // Convert the basic output object into its Rust representation + let basic_output = bcs::from_bytes::( + &basic_output_object + .bcs + .expect("should contain bcs") + .try_as_move() + .expect("should convert it to a move object") + .bcs_bytes, + )?; + + // Extract the keys of the native_tokens bag if this is not empty; here the keys + // are the type_arg of each native token, so they can be used later in the PTB. + let mut df_type_keys = vec![]; + let native_token_bag = basic_output.native_tokens; + if native_token_bag.size > 0 { + // Get the dynamic fields owned by the native tokens bag + let dynamic_field_page = iota_client + .read_api() + .get_dynamic_fields(*native_token_bag.id.object_id(), None, None) + .await?; + // should have only one page + assert!(!dynamic_field_page.has_next_page); + + // Extract the dynamic fields keys, i.e., the native token type + df_type_keys.extend( + dynamic_field_page + .data + .into_iter() + .map(|dyi| { + dyi.name + .value + .as_str() + .expect("should be a string") + .to_owned() + }) + .collect::>(), + ); + } + + // Create a PTB to for claiming the assets of a basic output + let pt = { + // Init the builder + let mut builder = ProgrammableTransactionBuilder::new(); + + ////// Command #1: extract the base token and native tokens bag. + // Type argument for a Basic Output coming from the IOTA network, i.e., the IOTA + // token or Gas type tag + let type_arguments = vec![GAS::type_tag()]; + // Then pass the basic output object as input + let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(basic_output_object_ref))?]; + // Finally call the basic_output::extract_assets function + if let Argument::Result(extracted_assets) = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("basic_output").to_owned(), + ident_str!("extract_assets").to_owned(), + type_arguments, + arguments, + ) { + // If the basic output can be unlocked, the command will be succesful and will + // return a `base_token` (i.e., IOTA) balance and a `Bag` of native tokens + let extracted_base_token = Argument::NestedResult(extracted_assets, 0); + let mut extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1); + + ////// Command #2: extract the netive tokens from the Bag and send them to sender. + for type_key in df_type_keys { + // Type argument for a Native Token contained in the basic output bag + let type_arguments = vec![TypeTag::from_str(&format!("0x{type_key}"))?]; + // Then pass the the bag and the receiver address as input + let arguments = vec![extracted_native_tokens_bag, builder.pure(sender)?]; + extracted_native_tokens_bag = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("utilities").to_owned(), + ident_str!("extract_and_send_to").to_owned(), + type_arguments, + arguments, + ); + } + + ////// Command #3: delete the bag + let arguments = vec![extracted_native_tokens_bag]; + builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("bag").to_owned(), + ident_str!("destroy_empty").to_owned(), + vec![], + arguments, + ); + + ////// Command #4: create a coin from the extracted IOTA balance + // Type argument for the IOTA coin + let type_arguments = vec![GAS::type_tag()]; + let arguments = vec![extracted_base_token]; + let new_iota_coin = builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("coin").to_owned(), + ident_str!("from_balance").to_owned(), + type_arguments, + arguments, + ); + + ////// Command #5: send back the base token coin to the user. + builder.transfer_arg(sender, new_iota_coin) + } + builder.finish() + }; + + // Setup gas budget and gas price + let gas_budget = 50_000_000; + let gas_price = iota_client.read_api().get_reference_gas_price().await?; + + // Create the transaction data that will be sent to the network + let tx_data = TransactionData::new_programmable( + sender, + vec![gas_coin.object_ref()], + pt, + gas_budget, + gas_price, + ); + + // Sign the transaction + let signature = keystore.sign_secure(&sender, &tx_data, Intent::iota_transaction())?; + + // Execute transaction + let transaction_response = iota_client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + println!("Transaction digest: {}", transaction_response.digest); + + // Finish and clean the temporary keystore file + clean_keystore() +} diff --git a/docs/examples/rust/stardust/check-basic-output-unlock-conditions.rs b/docs/examples/rust/stardust/check-basic-output-unlock-conditions.rs new file mode 100644 index 00000000000..205d49c6f94 --- /dev/null +++ b/docs/examples/rust/stardust/check-basic-output-unlock-conditions.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Example demonstrating queries for checking the unlock conditions of a basic +//! output. In order to work, it requires a network with test objects +//! generated from iota-genesis-builder/src/stardust/test_outputs. + +use anyhow::anyhow; +use iota_sdk::{ + rpc_types::{IotaData, IotaObjectDataOptions}, + types::{base_types::ObjectID, stardust::output::BasicOutput}, + IotaClientBuilder, +}; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Build an iota client for a local network + let iota_client = IotaClientBuilder::default().build_localnet().await?; + + // This object id was fetched manually. It refers to a Basic Output object that + // contains some Native Tokens. + let basic_output_object_id = ObjectID::from_hex_literal( + "0xde09139ed46b9f5f876671e4403f312fad867c5ae5d300a252e4b6a6f1fa1fbd", + )?; + // Get Basic Output object + let basic_output_object = iota_client + .read_api() + .get_object_with_options( + basic_output_object_id, + IotaObjectDataOptions::new().with_bcs(), + ) + .await? + .data + .ok_or(anyhow!("Basic output not found"))?; + + // Convert the basic output object into its Rust representation + let basic_output = bcs::from_bytes::( + &basic_output_object + .bcs + .expect("should contain bcs") + .try_as_move() + .expect("should convert it to a move object") + .bcs_bytes, + )?; + + println!("Basic Output infos: {basic_output:?}"); + + if let Some(sdruc) = basic_output.storage_deposit_return { + println!("Storage Deposit Return Unlock Condition infos: {sdruc:?}"); + } + if let Some(tuc) = basic_output.timelock { + println!("Timelocked until: {}", tuc.unix_time); + } + if let Some(euc) = basic_output.expiration { + println!("Expiration Unlock Condition infos: {euc:?}"); + } + + Ok(()) +} diff --git a/docs/examples/rust/stardust/foundry-output-claim.rs b/docs/examples/rust/stardust/foundry-output-claim.rs new file mode 100644 index 00000000000..826cd6ea46d --- /dev/null +++ b/docs/examples/rust/stardust/foundry-output-claim.rs @@ -0,0 +1,247 @@ +// 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. + +use anyhow::anyhow; +use docs_examples::utils::{clean_keystore, fund_address, setup_keystore}; +use iota_keys::keystore::AccountKeystore; +use iota_sdk::{ + rpc_types::{ + IotaObjectDataOptions, IotaObjectResponseQuery, IotaTransactionBlockResponseOptions, + }, + types::{ + base_types::ObjectID, + coin_manager::CoinManagerTreasuryCap, + crypto::SignatureScheme::ED25519, + dynamic_field::DynamicFieldName, + gas_coin::GAS, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + transaction::{Argument, ObjectArg, Transaction, TransactionData}, + TypeTag, IOTA_FRAMEWORK_ADDRESS, STARDUST_ADDRESS, + }, + IotaClientBuilder, +}; +use move_core_types::{ident_str, language_storage::StructTag}; +use shared_crypto::intent::Intent; + +/// Got from iota-genesis-builder/src/stardust/test_outputs/alias_ownership.rs +const MAIN_ADDRESS_MNEMONIC: &str = "few hood high omit camp keep burger give happy iron evolve draft few dawn pulp jazz box dash load snake gown bag draft car"; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Build an IOTA client for a local network. + let iota_client = IotaClientBuilder::default().build_localnet().await?; + + // Setup a temporary file based keystore. + let mut keystore = setup_keystore()?; + + // Derive the address of the first account and set it as default. + let sender = keystore.import_from_mnemonic(MAIN_ADDRESS_MNEMONIC, ED25519, None)?; + + println!("Sender address: {sender:?}"); + + // Fund the sender address + fund_address(&iota_client, &mut keystore, sender).await?; + + // Get a gas coin. + let gas_coin = iota_client + .coin_read_api() + .get_coins(sender, None, None, None) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("No coins found for sponsor"))?; + + // This object id was fetched manually. It refers to an Alias Output object that + // contains a CoinManagerTreasuryCap (i.e., a Foundry representation). + let alias_output_object_id = ObjectID::from_hex_literal( + "0xa58e9b6b85863e2fa50710c4594f701b2f5e2c6ff5e3c2b10cf09e6b18d740da", + )?; + let alias_output_object = iota_client + .read_api() + .get_object_with_options( + alias_output_object_id, + IotaObjectDataOptions::new().with_bcs(), + ) + .await? + .data + .ok_or(anyhow!("alias output not found"))?; + let alias_output_object_ref = alias_output_object.object_ref(); + + // 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 + let df_name = DynamicFieldName { + type_: TypeTag::Vector(Box::new(TypeTag::U8)), + value: serde_json::Value::String("alias".to_string()), + }; + let alias_object = iota_client + .read_api() + .get_dynamic_field_object(alias_output_object_id, df_name) + .await? + .data + .ok_or(anyhow!("alias not found"))?; + let alias_object_ref = alias_object.object_ref(); + + // Get the objects owned by the alias object and filter in the ones with + // CoinManagerTreasuryCap as type. + let alias_owned_objects_page = iota_client + .read_api() + .get_owned_objects( + alias_object_ref.0.into(), + Some(IotaObjectResponseQuery::new_with_options( + IotaObjectDataOptions::new().with_bcs().with_type(), + )), + None, + None, + ) + .await?; + // Only one page should exist. + assert!(!alias_owned_objects_page.has_next_page); + // Get the CoinManagerTreasuryCaps from the query + let owned_coin_manager_treasury_caps = alias_owned_objects_page + .data + .into_iter() + .filter(|object| { + CoinManagerTreasuryCap::is_coin_manager_treasury_cap( + &object + .data + .as_ref() + .expect("the query should request the data") + .object_type() + .expect("should contain the type") + .try_into() + .expect("should convert into a struct tag"), + ) + }) + .collect::>(); + + // Get only the first coin manager treasury cap + let coin_manager_treasury_cap_object = owned_coin_manager_treasury_caps + .into_iter() + .next() + .ok_or(anyhow!("no coin manager treasury caps found"))? + .data + .ok_or(anyhow!("coin manager treasury cap data not found"))?; + let coin_manager_treasury_cap_object_ref = coin_manager_treasury_cap_object.object_ref(); + + // Extract the foundry token type from the type parameters of the coin manager + // treasury cap object + let foundry_token_type_struct_tag: StructTag = coin_manager_treasury_cap_object + .object_type() + .expect("should contain the type") + .try_into()?; + let foundry_token_type = foundry_token_type_struct_tag + .type_params + .first() + .expect("should contain the type param"); + + // Create a PTB to claim the CoinManagerTreasuryCap related to the foundry + // output from the alias output. + let pt = { + // Init a programmable transaction builder. + let mut builder = ProgrammableTransactionBuilder::new(); + + // Type argument for an AliasOutput coming from the IOTA network, i.e., the + // IOTA token or the Gas type tag. + let type_arguments = vec![GAS::type_tag()]; + // Then pass the AliasOutput object as an input. + let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(alias_output_object_ref))?]; + // Finally call the alias_output::extract_assets function. + if let Argument::Result(extracted_assets) = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("alias_output").to_owned(), + ident_str!("extract_assets").to_owned(), + type_arguments, + arguments, + ) { + // The alias output can always be unlocked by the governor address. So the + // command will be successful and will return a `base_token` (i.e., IOTA) + // balance, a `Bag` of the related native tokens and the related Alias object. + let extracted_base_token = Argument::NestedResult(extracted_assets, 0); + let extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1); + let extracted_alias = Argument::NestedResult(extracted_assets, 2); + + // Extract the IOTA balance. + let type_arguments = vec![GAS::type_tag()]; + let arguments = vec![extracted_base_token]; + let iota_coin = builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("coin").to_owned(), + ident_str!("from_balance").to_owned(), + type_arguments, + arguments, + ); + + // Transfer the IOTA balance to the sender. + builder.transfer_arg(sender, iota_coin); + + // In this example the native tokens bag is empty, so it can be destroyed. + let arguments = vec![extracted_native_tokens_bag]; + builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("bag").to_owned(), + ident_str!("destroy_empty").to_owned(), + vec![], + arguments, + ); + + // Extract the CoinManagerTreasuryCap + let type_arguments = vec![foundry_token_type.clone()]; + let arguments = vec![ + extracted_alias, + builder.obj(ObjectArg::Receiving(coin_manager_treasury_cap_object_ref))?, + ]; + let coin_manager_treasury_cap = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("address_unlock_condition").to_owned(), + ident_str!("unlock_alias_address_owned_coinmanager_treasury").to_owned(), + type_arguments, + arguments, + ); + + // Transfer the coin manager treasury cap. + builder.transfer_arg(sender, coin_manager_treasury_cap); + + // Transfer the alias asset. + builder.transfer_arg(sender, extracted_alias); + } + builder.finish() + }; + + // Setup a gas budget and a gas price. + let gas_budget = 10_000_000; + let gas_price = iota_client.read_api().get_reference_gas_price().await?; + + // Create a transaction data that will be sent to the network. + let tx_data = TransactionData::new_programmable( + sender, + vec![gas_coin.object_ref()], + pt, + gas_budget, + gas_price, + ); + + // Sign the transaction. + let signature = keystore.sign_secure(&sender, &tx_data, Intent::iota_transaction())?; + + // Execute the transaction. + let transaction_response = iota_client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + println!("Transaction digest: {}", transaction_response.digest); + + // Finish and clean the temporary keystore file. + clean_keystore() +} diff --git a/docs/examples/rust/stardust/nft-migration.rs b/docs/examples/rust/stardust/nft-migration.rs new file mode 100644 index 00000000000..f8510281131 --- /dev/null +++ b/docs/examples/rust/stardust/nft-migration.rs @@ -0,0 +1,169 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Example demonstrating the conversion of a stardust NFT into a custom user's +//! NFT. In order to work, it requires a network with test objects +//! generated from iota-genesis-builder/src/stardust/test_outputs. + +use anyhow::{anyhow, Result}; +use docs_examples::utils::{clean_keystore, publish_custom_nft_package, setup_keystore}; +use iota_keys::keystore::AccountKeystore; +use iota_sdk::{ + rpc_types::{IotaObjectDataOptions, IotaTransactionBlockResponseOptions}, + types::{ + base_types::ObjectID, + crypto::SignatureScheme::ED25519, + gas_coin::GAS, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + transaction::{Argument, ObjectArg, Transaction, TransactionData}, + IOTA_FRAMEWORK_ADDRESS, STARDUST_ADDRESS, + }, + IotaClientBuilder, +}; +use move_core_types::ident_str; +use shared_crypto::intent::Intent; + +/// Got from iota-genesis-builder/src/stardust/test_outputs/stardust_mix.rs +const MAIN_ADDRESS_MNEMONIC: &str = "okay pottery arch air egg very cave cash poem gown sorry mind poem crack dawn wet car pink extra crane hen bar boring salt"; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Build an iota client for a local network + let iota_client = IotaClientBuilder::default().build_localnet().await?; + + // Setup the temporary file based keystore + let mut keystore = setup_keystore()?; + + // Derive the address of the first account and set it as default + let sender = keystore.import_from_mnemonic(MAIN_ADDRESS_MNEMONIC, ED25519, None)?; + + println!("{sender:?}"); + + // Publish the package of a custom NFT collection and then get the package id. + // The custom NFT module is obtained from a Move example in the docs. + // It is the same used in the Alias migration example. + let custom_nft_package_id = + publish_custom_nft_package(sender, &mut keystore, &iota_client).await?; + + // Get a gas coin + let gas_coin = iota_client + .coin_read_api() + .get_coins(sender, None, None, None) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("No coins found"))?; + + // Get an NftOutput object id + let nft_output_object_id = ObjectID::from_hex_literal( + "0x6445847625cec7d1265ebb9d0da8050a2e43d2856c2746d3579df499a1a64226", + )?; + + // Get an NftOutput object + let nft_output_object = iota_client + .read_api() + .get_object_with_options( + nft_output_object_id, + IotaObjectDataOptions::new().with_bcs(), + ) + .await? + .data + .ok_or(anyhow!("Nft output not found"))?; + + let nft_output_object_ref = nft_output_object.object_ref(); + + // Create a PTB that extracts the stardust NFT from an NFTOutput and then calls + // the `custom_nft::nft::convert` function for converting it into a custom NFT + // of the just published package. + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + let type_arguments = vec![GAS::type_tag()]; + let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(nft_output_object_ref))?]; + // Call the nft_output::extract_assets function + if let Argument::Result(extracted_assets) = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("nft_output").to_owned(), + ident_str!("extract_assets").to_owned(), + type_arguments, + arguments, + ) { + // If the nft output can be unlocked, the command will be successful + // and will return a `base_token` (i.e., IOTA) balance and a + // `Bag` of native tokens and related nft object. + let extracted_base_token = Argument::NestedResult(extracted_assets, 0); + let extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1); + let nft_asset = Argument::NestedResult(extracted_assets, 2); + + // Call the conversion function to create a custom nft from the stardust nft + // asset. + let custom_nft = builder.programmable_move_call( + custom_nft_package_id, + ident_str!("nft").to_owned(), + ident_str!("convert").to_owned(), + vec![], + vec![nft_asset], + ); + + // Transfer the converted NFT + builder.transfer_arg(sender, custom_nft); + + // Extract IOTA balance + let arguments = vec![extracted_base_token]; + let type_arguments = vec![GAS::type_tag()]; + let iota_coin = builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("coin").to_owned(), + ident_str!("from_balance").to_owned(), + type_arguments, + arguments, + ); + + // Transfer IOTA balance + builder.transfer_arg(sender, iota_coin); + + // Cleanup bag. + let arguments = vec![extracted_native_tokens_bag]; + builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("bag").to_owned(), + ident_str!("destroy_empty").to_owned(), + vec![], + arguments, + ); + } + builder.finish() + }; + + // Setup gas budget and gas price + let gas_budget = 10_000_000; + let gas_price = iota_client.read_api().get_reference_gas_price().await?; + + // Create the transaction data that will be sent to the network + let tx_data = TransactionData::new_programmable( + sender, + vec![gas_coin.object_ref()], + pt, + gas_budget, + gas_price, + ); + + // Sign the transaction + let signature = keystore.sign_secure(&sender, &tx_data, Intent::iota_transaction())?; + + // Execute transaction + let transaction_response = iota_client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + println!("Transaction digest: {}", transaction_response.digest); + + // Finish and clean the temporary keystore file + clean_keystore() +} diff --git a/docs/examples/rust/stardust/nft-output-claim.rs b/docs/examples/rust/stardust/nft-output-claim.rs new file mode 100644 index 00000000000..3d80afa83f1 --- /dev/null +++ b/docs/examples/rust/stardust/nft-output-claim.rs @@ -0,0 +1,207 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Example demonstrating the claim of an NFT output. +//! In order to work, it requires a network with test objects +//! generated from iota-genesis-builder/src/stardust/test_outputs. + +use std::str::FromStr; + +use anyhow::anyhow; +use docs_examples::utils::{clean_keystore, setup_keystore}; +use iota_keys::keystore::AccountKeystore; +use iota_sdk::{ + rpc_types::{IotaData, IotaObjectDataOptions, IotaTransactionBlockResponseOptions}, + types::{ + base_types::ObjectID, + crypto::SignatureScheme::ED25519, + gas_coin::GAS, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + stardust::output::NftOutput, + transaction::{Argument, ObjectArg, Transaction, TransactionData}, + TypeTag, IOTA_FRAMEWORK_ADDRESS, STARDUST_ADDRESS, + }, + IotaClientBuilder, +}; +use move_core_types::ident_str; +use shared_crypto::intent::Intent; + +/// Got from iota-genesis-builder/src/stardust/test_outputs/stardust_mix.rs +const MAIN_ADDRESS_MNEMONIC: &str = "okay pottery arch air egg very cave cash poem gown sorry mind poem crack dawn wet car pink extra crane hen bar boring salt"; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Build an iota client for a local network + let iota_client = IotaClientBuilder::default().build_localnet().await?; + + // Setup the temporary file based keystore + let mut keystore = setup_keystore()?; + + // Derive the address of the first account and set it as default + let sender = keystore.import_from_mnemonic(MAIN_ADDRESS_MNEMONIC, ED25519, None)?; + + println!("{sender:?}"); + + // Get a gas coin + let gas_coin = iota_client + .coin_read_api() + .get_coins(sender, None, None, None) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("No coins found"))?; + + // Get an NftOutput object + let nft_output_object_id = ObjectID::from_hex_literal( + "0xad87a60921c62f84d57301ea127d1706b406cde5ec6fa4d3af2a80f424fab93a", + )?; + + let nft_output_object = iota_client + .read_api() + .get_object_with_options( + nft_output_object_id, + IotaObjectDataOptions::new().with_bcs(), + ) + .await? + .data + .ok_or(anyhow!("Nft not found"))?; + + let nft_output_object_ref = nft_output_object.object_ref(); + + let nft_output = bcs::from_bytes::( + &nft_output_object + .bcs + .expect("should contain bcs") + .try_as_move() + .expect("should convert it to a move object") + .bcs_bytes, + )?; + + let mut df_type_keys = vec![]; + let native_token_bag = nft_output.native_tokens; + if native_token_bag.size > 0 { + // Get the dynamic fieldss of the native tokens bag + let dynamic_field_page = iota_client + .read_api() + .get_dynamic_fields(*native_token_bag.id.object_id(), None, None) + .await?; + // should have only one page + assert!(!dynamic_field_page.has_next_page); + + // Extract the dynamic fields keys, i.e., the native token type + df_type_keys.extend( + dynamic_field_page + .data + .into_iter() + .map(|dyi| { + dyi.name + .value + .as_str() + .expect("should be a string") + .to_string() + }) + .collect::>(), + ); + } + + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + + // Extract nft assets(base token, native tokens bag, nft asset itself). + let type_arguments = vec![GAS::type_tag()]; + let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(nft_output_object_ref))?]; + // Finally call the nft_output::extract_assets function + if let Argument::Result(extracted_assets) = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("nft_output").to_owned(), + ident_str!("extract_assets").to_owned(), + type_arguments, + arguments, + ) { + // If the nft output can be unlocked, the command will be succesful and will + // return a `base_token` (i.e., IOTA) balance and a `Bag` of native tokens and + // related nft object. + let extracted_base_token = Argument::NestedResult(extracted_assets, 0); + let mut extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1); + let nft_asset = Argument::NestedResult(extracted_assets, 2); + + // Extract IOTA balance + let arguments = vec![extracted_base_token]; + let type_arguments = vec![GAS::type_tag()]; + let iota_coin = builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("coin").to_owned(), + ident_str!("from_balance").to_owned(), + type_arguments, + arguments, + ); + + // Transfer IOTA balance + builder.transfer_arg(sender, iota_coin); + + for type_key in df_type_keys { + let type_arguments = vec![TypeTag::from_str(&format!("0x{type_key}"))?]; + // Then pass the the bag and the receiver address as input + let arguments = vec![extracted_native_tokens_bag, builder.pure(sender)?]; + + // Extract native tokens from the bag. + // Extract native token balance + // Transfer native token balance + extracted_native_tokens_bag = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("utilities").to_owned(), + ident_str!("extract_and_send_to").to_owned(), + type_arguments, + arguments, + ); + } + + // Transferring nft asset + builder.transfer_arg(sender, nft_asset); + + // Cleanup bag. + let arguments = vec![extracted_native_tokens_bag]; + builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("bag").to_owned(), + ident_str!("destroy_empty").to_owned(), + vec![], + arguments, + ); + } + builder.finish() + }; + + // Setup gas budget and gas price + let gas_budget = 10_000_000; + let gas_price = iota_client.read_api().get_reference_gas_price().await?; + + // Create the transaction data that will be sent to the network + let tx_data = TransactionData::new_programmable( + sender, + vec![gas_coin.object_ref()], + pt, + gas_budget, + gas_price, + ); + + // Sign the transaction + let signature = keystore.sign_secure(&sender, &tx_data, Intent::iota_transaction())?; + + // Execute transaction + let transaction_response = iota_client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + println!("Transaction digest: {}", transaction_response.digest); + + // Finish and clean the temporary keystore file + clean_keystore() +} diff --git a/docs/examples/rust/stardust/shimmer-self-sponsor.rs b/docs/examples/rust/stardust/shimmer-self-sponsor.rs new file mode 100644 index 00000000000..89ddf2af64f --- /dev/null +++ b/docs/examples/rust/stardust/shimmer-self-sponsor.rs @@ -0,0 +1,183 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Example demonstrating the self-sponsor scenario for claiming a Shimmer basic +//! output. In order to work, it requires a network with test objects +//! generated from iota-genesis-builder/src/stardust/test_outputs. + +use std::str::FromStr; + +use anyhow::anyhow; +use bip32::DerivationPath; +use docs_examples::utils::{clean_keystore, setup_keystore}; +use iota_keys::keystore::AccountKeystore; +use iota_sdk::{ + rpc_types::{IotaObjectDataOptions, IotaTransactionBlockResponseOptions}, + types::{ + base_types::ObjectID, + crypto::SignatureScheme::ED25519, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + smr_coin::SMR, + transaction::{Argument, ObjectArg, Transaction, TransactionData}, + IOTA_FRAMEWORK_ADDRESS, STARDUST_ADDRESS, + }, + IotaClientBuilder, +}; +use move_core_types::ident_str; +use shared_crypto::intent::Intent; + +pub const IOTA_COIN_TYPE: u32 = 4218; +pub const SHIMMER_COIN_TYPE: u32 = 4219; + +/// Got from iota-genesis-builder/src/stardust/test_outputs/stardust_mix.rs +const MAIN_ADDRESS_MNEMONIC: &str = "crazy drum raw dirt tooth where fee base warm beach trim rule sign silk fee fee dad large creek venue coin steel hub scale"; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Build an iota client for a local network + let iota_client = IotaClientBuilder::default().build_localnet().await?; + + // Setup the temporary file based keystore + let mut keystore = setup_keystore()?; + + // For this example we need to derive addresses that are not at different + // indexes and coin_types, one for sponsoring with IOTA coin type and one for + // claiming the Basic Output with Shimmer coin type. + let sponsor_derivation_path = + DerivationPath::from_str(format!("m/44'/{IOTA_COIN_TYPE}'/0'/0'/5'").as_str())?; + let sender_derivation_path = + DerivationPath::from_str(format!("m/44'/{SHIMMER_COIN_TYPE}'/0'/0'/50'").as_str())?; + + // Derive the address of the sponsor + let sponsor = keystore.import_from_mnemonic( + MAIN_ADDRESS_MNEMONIC, + ED25519, + Some(sponsor_derivation_path), + )?; + println!("Sponsor address: {sponsor:?}"); + + // Derive the address of the sender + let sender = keystore.import_from_mnemonic( + MAIN_ADDRESS_MNEMONIC, + ED25519, + Some(sender_derivation_path), + )?; + println!("Sender address: {sender:?}"); + + // This object id was fetched manually. It refers to a Basic Output object that + // contains some Native Tokens. + let basic_output_object_id = ObjectID::from_hex_literal( + "0xbdc4dec75098700e8e82349d9f3a9f28dcd22d2b39f5fbdf8436b05430bc3690", + )?; + // Get Basic Output object + let basic_output_object = iota_client + .read_api() + .get_object_with_options( + basic_output_object_id, + IotaObjectDataOptions::new().with_bcs(), + ) + .await? + .data + .ok_or(anyhow!("Basic output not found"))?; + let basic_output_object_ref = basic_output_object.object_ref(); + + // Create a PTB to for claiming the assets of a basic output for the sender + let pt = { + // Init the builder + let mut builder = ProgrammableTransactionBuilder::new(); + + ////// Command #1: extract the base token and native tokens bag. + // Type argument for a Basic Output coming from the Shimmer network, i.e., the + // SMR coin + let type_arguments = vec![SMR::type_tag()]; + // Then pass the basic output object as input + let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(basic_output_object_ref))?]; + // Finally call the basic_output::extract_assets function + if let Argument::Result(extracted_assets) = builder.programmable_move_call( + STARDUST_ADDRESS.into(), + ident_str!("basic_output").to_owned(), + ident_str!("extract_assets").to_owned(), + type_arguments, + arguments, + ) { + // If the basic output can be unlocked, the command will be succesful and will + // return a `base_token` (i.e., SMR) balance and a `Bag` of native tokens + let extracted_base_token = Argument::NestedResult(extracted_assets, 0); + let extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1); + + ////// Command #2: delete the empty native tokens bag + let arguments = vec![extracted_native_tokens_bag]; + builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("bag").to_owned(), + ident_str!("destroy_empty").to_owned(), + vec![], + arguments, + ); + + ////// Command #3: create a coin from the extracted SMR balance + // Type argument for the SMR coin + let type_arguments = vec![SMR::type_tag()]; + let arguments = vec![extracted_base_token]; + let new_iota_coin = builder.programmable_move_call( + IOTA_FRAMEWORK_ADDRESS.into(), + ident_str!("coin").to_owned(), + ident_str!("from_balance").to_owned(), + type_arguments, + arguments, + ); + + ////// Command #5: send back the base token coin to the user. + builder.transfer_arg(sender, new_iota_coin) + } + builder.finish() + }; + + // Setup gas budget and gas price + let gas_budget = 50_000_000; + let gas_price = iota_client.read_api().get_reference_gas_price().await?; + + // Get a gas coin + let gas_coin = iota_client + .coin_read_api() + .get_coins(sponsor, None, None, None) + .await? + .data + .into_iter() + .next() + .ok_or(anyhow!("No coins found for sponsor"))?; + + // Create the transaction data that will be sent to the network and allow + // sponsoring + let tx_data = TransactionData::new_programmable_allow_sponsor( + sender, + vec![gas_coin.object_ref()], + pt, + gas_budget, + gas_price, + sponsor, + ); + + // Sender signs the transaction + let sender_signature = keystore.sign_secure(&sender, &tx_data, Intent::iota_transaction())?; + + // Sponsor signs the transaction + let sponsor_signature = keystore.sign_secure(&sponsor, &tx_data, Intent::iota_transaction())?; + + // Execute transaction; the transaction data is created using the signature of + // the sender and of the sponsor. + let transaction_response = iota_client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![sender_signature, sponsor_signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + println!("Transaction digest: {}", transaction_response.digest); + + // Finish and clean the temporary keystore file + clean_keystore() +}