Skip to content
9 changes: 9 additions & 0 deletions crates/iota-sdk-types/src/type_tag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,15 @@ impl StructTag {
}
}

pub fn upgrade_cap() -> Self {
Self {
address: Address::FRAMEWORK,
module: Identifier::new("package").unwrap(),
name: Identifier::new("UpgradeCap").unwrap(),
type_params: vec![],
}
}

pub fn address(&self) -> Address {
self.address
}
Expand Down
187 changes: 187 additions & 0 deletions crates/iota-sdk/examples/publish_upgrade.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright (c) 2025 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

//! This example requires running a localnet.
//! ```
//! iota start --with-faucet --with-graphql --committee-size 1 --force-regenesis
//! ```
use eyre::{Result, bail};
use iota_crypto::{IotaSigner, ed25519::Ed25519PrivateKey};
use iota_graphql_client::{Client, faucet::FaucetClient};
use iota_transaction_builder::{MovePackageData, TransactionBuilder, res};
use iota_types::{Address, Input, ObjectId, ObjectOut, StructTag};
use rand::rngs::OsRng;

#[tokio::main]
async fn main() -> Result<()> {
// Parse the compiled `first_package` example from the monorepo created with
// `iota move build --dump-bytecode-as-base64`
let data = serde_json::from_str::<MovePackageData>(SERIALIZED_FIRST_PACKAGE)?;
let Some(compiled_package_digest) = data.digest else {
bail!("Missing compiled package digest");
};
println!("Compiled Package Digest: {compiled_package_digest}");

// Create a random private key to derive a sender address and for signing
let private_key = Ed25519PrivateKey::generate(OsRng);
let public_key = private_key.public_key();
let sender = public_key.derive_address();
println!("Sender: {sender}");

// Fund the sender address for gas payment
let faucet = FaucetClient::new_localnet();
let Some(receipt) = faucet.request_and_wait(sender).await? else {
bail!("Failed to request coins from faucet");
};
println!(
"Available Balance: {}",
receipt.sent.iter().map(|coin| coin.amount).sum::<u64>()
);

// Get a gas coin id
let client = Client::new_localnet();

// Build the `publish` PTB, that consists of 2 steps
let mut builder = TransactionBuilder::new(sender).with_client(client.clone());

// 1. Create the upgrade cap
builder.publish(data.clone()).name("upgrade_cap");

// 2. Transfer the upgrade cap to the sender address
builder.transfer_objects(sender, [res("upgrade_cap")]);

// Finalize the PTB
let tx = builder.finish().await?;

// Perform a dry-run to check if everything is fine
let result = client.dry_run_tx(&tx, false).await?;
if let Some(err) = result.error {
bail!("Dry run failed: {err}");
}
let Some(effects) = result.effects else {
bail!("Dry run failed: no effects");
};
println!("Effects status (dry run): {:?}", effects.status());

// Sign and execute the transaction (publish the package)
println!("Publishing package");
let sig = private_key.sign_transaction(&tx)?;
let Some(effects) = client.execute_tx(&[sig], &tx).await? else {
bail!("Transaction failed: no effects");
};
println!("Effects status (publish): {:?}", effects.status());

// Wait some time for the indexer to process the tx
tokio::time::sleep(std::time::Duration::from_secs(3)).await;

// Resolve UpgradeCap and PackageId via the client
let mut upgrade_cap = None::<ObjectId>;
let mut package_id = None::<ObjectId>;

for changed_obj in effects.as_v1().changed_objects.iter() {
match changed_obj.output_state {
ObjectOut::ObjectWrite { owner, .. } => {
let object_id = changed_obj.object_id;
let Some(obj) = client.object(object_id, None).await? else {
bail!("Missing object {object_id}");
};
if obj.as_struct().type_ == StructTag::upgrade_cap() {
println!("UpgradeCap: {object_id}");
println!("UpgradeCapOwner: {}", owner.into_address());
upgrade_cap.replace(object_id);
}
}
ObjectOut::PackageWrite { version, .. } => {
let pkg_id = changed_obj.object_id;
println!("PackageId: {pkg_id}");
println!("Package version: {version}");
package_id.replace(pkg_id);
}
_ => continue,
}
}

let Some(upgrade_cap_id) = upgrade_cap else {
bail!("Missing upgrade cap");
};
let Some(package_id) = package_id else {
bail!("Missing package id");
};

let Some(upgrade_cap_ref) = client
.object(upgrade_cap_id, None)
.await?
.map(|obj| obj.object_ref())
else {
bail!("Missing upgrade cap object");
};

// Build the `upgrade` PTB, that consists of 3 steps
let mut builder = TransactionBuilder::new(sender).with_client(client.clone());

let upgrade_cap_arg = builder.input(Input::ImmutableOrOwned(upgrade_cap_ref));
let upgrade_policy_arg = builder.pure(0u8);
let compiled_package_digest_arg = builder.pure(compiled_package_digest);

// 1. Create the upgrade ticket
builder
.move_call(Address::FRAMEWORK, "package", "authorize_upgrade")
.arguments([
upgrade_cap_arg,
upgrade_policy_arg,
compiled_package_digest_arg,
])
.name("upgrade_ticket");

// 2. Get the upgrade receipt
builder
.upgrade(package_id, res("upgrade_ticket"), data)
.name("upgrade_receipt");

// 3. Finalize the upgrade
builder
.move_call(Address::FRAMEWORK, "package", "commit_upgrade")
.arguments((upgrade_cap_arg, res("upgrade_receipt")));

// Finalize the PTB
let tx = builder.finish().await?;

// Perform a dry-run to check if everything is fine
let result = client.dry_run_tx(&tx, false).await?;
if let Some(err) = result.error {
bail!("Dry run failed: {err}");
}
let Some(effects) = result.effects else {
bail!("Dry run failed: no effects");
};
println!("Effects status (dry run): {:?}", effects.status());

// Sign and execute the transaction (upgrade the package)
println!("Upgrading package");
let sig = private_key.sign_transaction(&tx)?;
let Some(effects) = client.execute_tx(&[sig], &tx).await? else {
bail!("Transaction failed: no effects");
};
println!("Effects status (upgrade): {:?}", effects.status());

// Wait some time for the indexer to process the tx
tokio::time::sleep(std::time::Duration::from_secs(3)).await;

// Print the new package version (should now be 2)
for changed_obj in effects.as_v1().changed_objects.iter() {
match changed_obj.output_state {
ObjectOut::PackageWrite { version, .. } => {
let pkg_id = changed_obj.object_id;
println!("PackageId: {pkg_id}");
println!("Package version: {version}")
}
_ => continue,
}
}

Ok(())
}

// Compiled `first_package` example
const SERIALIZED_FIRST_PACKAGE: &str = r#"{"modules":["oRzrCwYAAAAKAQAIAggUAxw+BFoGBWBBB6EBwQEI4gJACqIDGgy8A5cBDdMEBgAKAQ0BEwEUAAIMAAABCAAAAAgAAQQEAAMDAgAACAABAAAJAgMAABACAwAAEgQDAAAMBQYAAAYHAQAAEQgBAAAFCQoAAQsACwACDg8BAQwCEw8BAQgDDwwNAAoOCgYJBgEHCAQAAQYIAAEDAQYIAQQHCAEDAwcIBAEIAAQDAwUHCAQDCAAFBwgEAgMHCAQBCAIBCAMBBggEAQUBCAECCQAFBkNvbmZpZwVGb3JnZQVTd29yZAlUeENvbnRleHQDVUlEDWNyZWF0ZV9jb25maWcMY3JlYXRlX3N3b3JkAmlkBGluaXQFbWFnaWMJbXlfbW9kdWxlA25ldwluZXdfc3dvcmQGb2JqZWN0D3B1YmxpY190cmFuc2ZlcgZzZW5kZXIIc3RyZW5ndGgOc3dvcmRfdHJhbnNmZXIOc3dvcmRzX2NyZWF0ZWQIdHJhbnNmZXIKdHhfY29udGV4dAV2YWx1ZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgMHCAMJAxADAQICBwgDEgMCAgIHCAMVAwAAAAABCQoAEQgGAAAAAAAAAAASAQsALhELOAACAQEAAAEECwAQABQCAgEAAAEECwAQARQCAwEAAAEECwAQAhQCBAEAAAEOCgAQAhQGAQAAAAAAAAAWCwAPAhULAxEICwELAhIAAgUBAAABCAsDEQgLAAsBEgALAjgBAgYBAAABBAsACwE4AgIHAQAAAQULAREICwASAgIAAQACAQEA"],"dependencies":["0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000001"],"digest":[246,127,102,77,186,19,68,12,161,181,56,248,210,0,91,211,245,251,165,152,0,197,250,135,171,37,177,240,133,76,122,124]}"#;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say either move this to a fixture or just compile it using the iota binary in the example.

Copy link
Contributor Author

@Alex6323 Alex6323 Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the examples need to be copy and paste friendly and self-contained, but if we move this out, then it has some dependency on the environment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. But this example necessarily involves an external package, so maybe it should be an exception

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not entirely sure I understand what you mean ... the original example can now evolve separately without us to ever have to consider it again. So we aren't dependent on it anymore? And what you suggested in your first message (compiling it) is what I did, no? This is the compiled output of that package, just serialized as json/base64 using the iota CLI.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant compiling it in the example by calling iota move build

4 changes: 2 additions & 2 deletions crates/iota-transaction-builder/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,14 @@ impl<C, L> TransactionBuilder<C, L> {
pub fn upgrade<U: PTBArgument>(
&mut self,
package_id: ObjectId,
upgrade_cap: U,
upgrade_ticket: U,
kind: impl Into<PublishType>,
) -> &mut TransactionBuilder<C, Upgrade> {
let module = match kind.into() {
PublishType::Path(_path) => todo!("load the package from the path"),
PublishType::Compiled(m) => m,
};
let ticket = self.apply_argument(upgrade_cap);
let ticket = self.apply_argument(upgrade_ticket);
self.cmd_state_change(Upgrade {
modules: module.modules,
dependencies: module.dependencies,
Expand Down
2 changes: 1 addition & 1 deletion crates/iota-transaction-builder/src/publish_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl From<MovePackageData> for PublishType {

/// Type corresponding to the output of `iota move build
/// --dump-bytecode-as-base64`
#[derive(serde::Deserialize, Debug)]
#[derive(serde::Deserialize, Debug, Clone)]
pub struct MovePackageData {
/// The package modules as a series of bytes
#[serde(deserialize_with = "bcs_from_str")]
Expand Down