diff --git a/bindings/go/examples/publish_upgrade/main.go b/bindings/go/examples/publish_upgrade/main.go new file mode 100644 index 000000000..0b7016d3f --- /dev/null +++ b/bindings/go/examples/publish_upgrade/main.go @@ -0,0 +1,227 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "encoding/base64" + "fmt" + "log" + "time" + + sdk "bindings/iota_sdk_ffi" +) + +func main() { + // Hardcoded values from the JSON + modules := [][]byte{ + func() []byte { + b, _ := base64.StdEncoding.DecodeString("oRzrCwYAAAAKAQAIAggUAxw+BFoGBWBBB6EBwQEI4gJACqIDGgy8A5cBDdMEBgAKAQ0BEwEUAAIMAAABCAAAAAgAAQQEAAMDAgAACAABAAAJAgMAABACAwAAEgQDAAAMBQYAAAYHAQAAEQgBAAAFCQoAAQsACwACDg8BAQwCEw8BAQgDDwwNAAoOCgYJBgEHCAQAAQYIAAEDAQYIAQQHCAEDAwcIBAEIAAQDAwUHCAQDCAAFBwgEAgMHCAQBCAIBCAMBBggEAQUBCAECCQAFBkNvbmZpZwVGb3JnZQVTd29yZAlUeENvbnRleHQDVUlEDWNyZWF0ZV9jb25maWcMY3JlYXRlX3N3b3JkAmlkBGluaXQFbWFnaWMJbXlfbW9kdWxlA25ldwluZXdfc3dvcmQGb2JqZWN0D3B1YmxpY190cmFuc2ZlcgZzZW5kZXIIc3RyZW5ndGgOc3dvcmRfdHJhbnNmZXIOc3dvcmRzX2NyZWF0ZWQIdHJhbnNmZXIKdHhfY29udGV4dAV2YWx1ZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgMHCAMJAxADAQICBwgDEgMCAgIHCAMVAwAAAAABCQoAEQgGAAAAAAAAAAASAQsALhELOAACAQEAAAEECwAQABQCAgEAAAEECwAQARQCAwEAAAEECwAQAhQCBAEAAAEOCgAQAhQGAQAAAAAAAAAWCwAPAhULAxEICwELAhIAAgUBAAABCAsDEQgLAAsBEgALAjgBAgYBAAABBAsACwE4AgIHAQAAAQULAREICwASAgIAAQACAQEA") + return b + }(), + } + dependencies := []*sdk.ObjectId{ + func() *sdk.ObjectId { + id, _ := sdk.ObjectIdFromHex("0x0000000000000000000000000000000000000000000000000000000000000002") + return id + }(), + func() *sdk.ObjectId { + id, _ := sdk.ObjectIdFromHex("0x0000000000000000000000000000000000000000000000000000000000000001") + return id + }(), + } + compiledPackageDigest := []byte{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} + fmt.Printf("Compiled Package Digest: %x\n", compiledPackageDigest) + + // Create a random private key to derive a sender address and for signing + privateKey := sdk.Ed25519PrivateKeyGenerate() + publicKey := privateKey.PublicKey() + sender := publicKey.DeriveAddress() + fmt.Printf("Sender: %s\n", sender.ToHex()) + + // Fund the sender address for gas payment + faucet := sdk.FaucetClientNewLocalnet() + faucetReceipt, err := faucet.RequestAndWait(sender) + if err.(*sdk.SdkFfiError) != nil { + log.Fatalf("Failed to request coins from faucet: %v", err) + } + totalBalance := uint64(0) + for _, coin := range faucetReceipt.Sent { + totalBalance += coin.Amount + } + fmt.Printf("Available Balance: %d\n", totalBalance) + + client := sdk.GraphQlClientNewLocalnet() + + // Build the `publish` PTB, that consists of 2 steps + builder := sdk.TransactionBuilderInit(sender, client) + + // 1. Create the upgrade cap + builder.Publish(modules, dependencies, "upgrade_cap") + + // 2. Transfer the upgrade cap to the sender address + builder.TransferObjects(sender, []*sdk.PtbArgument{sdk.PtbArgumentRes("upgrade_cap")}) + + // Finalize the PTB + tx, err := builder.Finish() + if err.(*sdk.SdkFfiError) != nil { + log.Fatalf("Failed to finish transaction: %v", err) + } + + // Perform a dry-run to check if everything is fine + result, err := client.DryRunTx(tx, nil) + if err.(*sdk.SdkFfiError) != nil { + log.Fatalf("Dry run failed: %v", err) + } + if result.Error != nil { + log.Fatalf("Dry run failed: %v", *result.Error) + } + if result.Effects == nil { + log.Fatal("Dry run failed: no effects") + } + fmt.Printf("Effects status (dry run): %s\n", (*result.Effects).AsV1().Status) + + // Sign and execute the transaction (publish the package) + fmt.Println("Publishing package") + signature, err := privateKey.TrySignSimple(tx.SigningDigest()) + if err != nil { + log.Fatalf("Failed to sign: %v", err) + } + userSignature := sdk.UserSignatureNewSimple(signature) + effects, err := client.ExecuteTx([]*sdk.UserSignature{userSignature}, tx) + if err.(*sdk.SdkFfiError) != nil { + log.Fatalf("Transaction failed: %v", err) + } + if effects == nil { + log.Fatal("Transaction failed: no effects") + } + fmt.Printf("Effects status (publish): %s\n", (*effects).AsV1().Status) + + // Wait some time for the indexer to process the tx + time.Sleep(10 * time.Second) + + // Resolve UpgradeCap and PackageId via the client + var upgradeCap *sdk.ObjectId + var packageId *sdk.ObjectId + + for _, changedObj := range (*effects).AsV1().ChangedObjects { + if _, ok := changedObj.OutputState.(sdk.ObjectOutObjectWrite); ok { + objectId := changedObj.ObjectId + objPtr, err := client.Object(objectId, nil) + if err.(*sdk.SdkFfiError) != nil { + log.Fatalf("Failed to get object: %v", err) + } + obj := *objPtr + if obj.AsStructOpt() != nil { + structType := obj.AsStructOpt().StructType + packageIdent, _ := sdk.NewIdentifier("package") + upgradeCapIdent, _ := sdk.NewIdentifier("UpgradeCap") + upgradeCapType := sdk.NewStructTag(sdk.AddressFramework(), packageIdent, upgradeCapIdent, []*sdk.TypeTag{}) + if structType.String() == upgradeCapType.String() { + upgradeCap = objectId + } + } + } else if _, ok := changedObj.OutputState.(sdk.ObjectOutPackageWrite); ok { + pkgId := changedObj.ObjectId + if packageId == nil { + packageId = pkgId + } + } + } + + if upgradeCap == nil { + log.Fatal("Missing upgrade cap") + } + if packageId == nil { + log.Fatal("Missing package id") + } + + // Build the `upgrade` PTB, that consists of 3 steps + builder2 := sdk.TransactionBuilderInit(sender, client) + + upgradeCapArg := sdk.PtbArgumentObjectId(upgradeCap) + + // 1. Create the upgrade ticket + authorizeUpgrade, err := sdk.NewIdentifier("authorize_upgrade") + if err != nil { + log.Fatalf("Failed to create identifier: %v", err) + } + packageIdent, err := sdk.NewIdentifier("package") + if err != nil { + log.Fatalf("Failed to create identifier: %v", err) + } + builder2.MoveCall( + sdk.AddressFramework(), + packageIdent, + authorizeUpgrade, + []*sdk.PtbArgument{upgradeCapArg, sdk.PtbArgumentU8(0), sdk.PtbArgumentU8Vec(compiledPackageDigest)}, + nil, + []string{"upgrade_ticket"}, + ) + + // 2. Get the upgrade receipt + upgradeReceiptName := "upgrade_receipt" + builder2.Upgrade(modules, dependencies, packageId, sdk.PtbArgumentRes("upgrade_ticket"), &upgradeReceiptName) + + // 3. Finalize the upgrade + commitUpgrade, err := sdk.NewIdentifier("commit_upgrade") + if err != nil { + log.Fatalf("Failed to create identifier: %v", err) + } + builder2.MoveCall( + sdk.AddressFramework(), + packageIdent, + commitUpgrade, + []*sdk.PtbArgument{upgradeCapArg, sdk.PtbArgumentRes("upgrade_receipt")}, + nil, + nil, + ) + + // Finalize the PTB + tx2, err := builder2.Finish() + if err.(*sdk.SdkFfiError) != nil { + log.Fatalf("Failed to finish transaction: %v", err) + } + + // Perform a dry-run to check if everything is fine + result2, err := client.DryRunTx(tx2, nil) + if err.(*sdk.SdkFfiError) != nil { + log.Fatalf("Dry run failed: %v", err) + } + if result2.Error != nil { + log.Fatalf("Dry run failed: %v", *result2.Error) + } + if result2.Effects == nil { + log.Fatal("Dry run failed: no effects") + } + fmt.Printf("Effects status (dry run): %s\n", (*result2.Effects).AsV1().Status) + + // Sign and execute the transaction (upgrade the package) + fmt.Println("Upgrading package") + signature2, err := privateKey.TrySignSimple(tx2.SigningDigest()) + if err != nil { + log.Fatalf("Failed to sign: %v", err) + } + userSignature2 := sdk.UserSignatureNewSimple(signature2) + effects2, err := client.ExecuteTx([]*sdk.UserSignature{userSignature2}, tx2) + if err.(*sdk.SdkFfiError) != nil { + log.Fatalf("Transaction failed: %v", err) + } + if effects2 == nil { + log.Fatal("Transaction failed: no effects") + } + fmt.Printf("Effects status (upgrade): %s\n", (*effects2).AsV1().Status) + + // Wait some time for the indexer to process the tx + time.Sleep(10 * time.Second) + + // Print the new package version (should now be 2) + for _, changedObj := range (*effects2).AsV1().ChangedObjects { + if _, ok := changedObj.OutputState.(sdk.ObjectOutPackageWrite); ok { + pkgId := changedObj.ObjectId + version := changedObj.OutputState.(sdk.ObjectOutPackageWrite).Version + fmt.Printf("PackageId: %s\n", pkgId.ToHex()) + fmt.Printf("Package version: %d\n", version) + } + } +} diff --git a/bindings/kotlin/examples/PublishUpgrade.kt b/bindings/kotlin/examples/PublishUpgrade.kt new file mode 100644 index 000000000..b654f25d2 --- /dev/null +++ b/bindings/kotlin/examples/PublishUpgrade.kt @@ -0,0 +1,179 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import iota_sdk.* +import java.util.Base64 +import kotlinx.coroutines.runBlocking + +fun main() = runBlocking { + try { + // Hardcoded values + val dataString = + "oRzrCwYAAAAKAQAIAggUAxw+BFoGBWBBB6EBwQEI4gJACqIDGgy8A5cBDdMEBgAKAQ0BEwEUAAIMAAABCAAAAAgAAQQEAAMDAgAACAABAAAJAgMAABACAwAAEgQDAAAMBQYAAAYHAQAAEQgBAAAFCQoAAQsACwACDg8BAQwCEw8BAQgDDwwNAAoOCgYJBgEHCAQAAQYIAAEDAQYIAQQHCAEDAwcIBAEIAAQDAwUHCAQDCAAFBwgEAgMHCAQBCAIBCAMBBggEAQUBCAECCQAFBkNvbmZpZwVGb3JnZQVTd29yZAlUeENvbnRleHQDVUlEDWNyZWF0ZV9jb25maWcMY3JlYXRlX3N3b3JkAmlkBGluaXQFbWFnaWMJbXlfbW9kdWxlA25ldwluZXdfc3dvcmQGb2JqZWN0D3B1YmxpY190cmFuc2ZlcgZzZW5kZXIIc3RyZW5ndGgOc3dvcmRfdHJhbnNmZXIOc3dvcmRzX2NyZWF0ZWQIdHJhbnNmZXIKdHhfY29udGV4dAV2YWx1ZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgMHCAMJAxADAQICBwgDEgMCAgIHCAMVAwAAAAABCQoAEQgGAAAAAAAAAAASAQsALhELOAACAQEAAAEECwAQABQCAgEAAAEECwAQARQCAwEAAAEECwAQAhQCBAEAAAEOCgAQAhQGAQAAAAAAAAAWCwAPAhULAxEICwELAhIAAgUBAAABCAsDEQgLAAsBEgALAjgBAgYBAAABBAsACwE4AgIHAQAAAQULAREICwASAgIAAQACAQEA|f67f664dba13440ca1b538f8d2005bd3f5fba59800c5fa87ab25b1f0854c7a7c" + val parts = dataString.split("|") + val modulesBase64 = parts[0] + val digestHex = parts[1] + val modules = listOf(Base64.getDecoder().decode(modulesBase64)) + val dependencies = + listOf( + ObjectId.fromHex( + "0x0000000000000000000000000000000000000000000000000000000000000002" + ), + ObjectId.fromHex( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ) + ) + val compiledPackageDigest = digestHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() + println( + "Compiled Package Digest: ${compiledPackageDigest.joinToString("") { "%02x".format(it) }}" + ) + + // Create a random private key to derive a sender address and for signing + val privateKey = Ed25519PrivateKey.generate() + val publicKey = privateKey.publicKey() + val sender = publicKey.deriveAddress() + println("Sender: ${sender.toHex()}") + + // Fund the sender address for gas payment + val faucet = FaucetClient.newLocalnet() + val faucetReceipt = + faucet.requestAndWait(sender) + ?: throw Exception("Failed to request coins from faucet") + val totalBalance = faucetReceipt.sent.sumOf { it.amount.toLong() } + println("Available Balance: $totalBalance") + + val client = GraphQlClient.newLocalnet() + + // Build the `publish` PTB, that consists of 2 steps + val builder = TransactionBuilder.init(sender, client) + + // 1. Create the upgrade cap + builder.publish(modules, dependencies, "upgrade_cap") + + // 2. Transfer the upgrade cap to the sender address + builder.transferObjects(sender, listOf(PtbArgument.res("upgrade_cap"))) + + // Finalize the PTB + val tx = builder.finish() + + // Perform a dry-run to check if everything is fine + val result = client.dryRunTx(tx, false) + result.error?.let { throw Exception("Dry run failed: $it") } + val effectsPublish = result.effects ?: throw Exception("Dry run failed: no effects") + println("Effects status (dry run): ${effectsPublish.asV1().status}") + + // Sign and execute the transaction (publish the package) + println("Publishing package") + val signature = privateKey.trySignSimple(tx.signingDigest()) + val userSignature = UserSignature.newSimple(signature) + val effects = + client.executeTx(listOf(userSignature), tx) + ?: throw Exception("Transaction failed: no effects") + println("Effects status (publish): ${effects.asV1().status}") + + // Wait some time for the indexer to process the tx + kotlinx.coroutines.delay(3000) + + // Resolve UpgradeCap and PackageId via the client + var upgradeCap: ObjectId? = null + var packageId: ObjectId? = null + + for (changedObj in effects.asV1().changedObjects) { + if (changedObj.outputState is ObjectOut.ObjectWrite) { + val obj: Object = + client.`object`(changedObj.objectId, null) + ?: throw Exception("Missing object ${changedObj.objectId.toHex()}") + val upgradeCapType = + StructTag( + address = Address.framework(), + module = Identifier("package"), + name = Identifier("UpgradeCap"), + typeParams = emptyList() + ) + if (obj.asStruct().structType.toString() == upgradeCapType.toString()) { + upgradeCap = changedObj.objectId + } + } else if (changedObj.outputState is ObjectOut.PackageWrite) { + if (packageId == null) { + packageId = changedObj.objectId + } + } + } + + upgradeCap ?: throw Exception("Missing upgrade cap") + packageId ?: throw Exception("Missing package id") + + // Build the `upgrade` PTB, that consists of 3 steps + val builder2 = TransactionBuilder.init(sender, client) + + val packageIdent = Identifier("package") + val authorizeUpgrade = Identifier("authorize_upgrade") + val commitUpgrade = Identifier("commit_upgrade") + + val upgradeCapArg = PtbArgument.objectId(upgradeCap) + val upgradePolicyArg = PtbArgument.u8(0u) + val compiledPackageDigestArg = PtbArgument.u8Vec(compiledPackageDigest) + + // 1. Create the upgrade ticket + builder2.moveCall( + `package` = Address.framework(), + module = packageIdent, + function = authorizeUpgrade, + arguments = listOf(upgradeCapArg, upgradePolicyArg, compiledPackageDigestArg), + typeArgs = listOf(), + names = listOf("upgrade_ticket") + ) + + // 2. Get the upgrade receipt + builder2.upgrade( + modules, + dependencies, + packageId, + PtbArgument.res("upgrade_ticket"), + "upgrade_receipt" + ) + + // 3. Finalize the upgrade + builder2.moveCall( + `package` = Address.framework(), + module = packageIdent, + function = commitUpgrade, + arguments = listOf(upgradeCapArg, PtbArgument.res("upgrade_receipt")), + typeArgs = listOf(), + names = listOf() + ) + + // Finalize the PTB + val tx2 = builder2.finish() + + // Perform a dry-run to check if everything is fine + val result2 = client.dryRunTx(tx, false) + result2.error?.let { throw Exception("Dry run failed: $it") } + val effectsUpgrade = result2.effects ?: throw Exception("Dry run failed: no effects") + println("Effects status (dry run): ${effectsUpgrade.asV1().status}") + + // Sign and execute the transaction (upgrade the package) + println("Upgrading package") + val signature2 = privateKey.trySignSimple(tx2.signingDigest()) + val userSignature2 = UserSignature.newSimple(signature2) + val effectsUpgrade2 = + client.executeTx(listOf(userSignature2), tx2) + ?: throw Exception("Transaction failed: no effects") + println("Effects status (upgrade): ${effectsUpgrade2.asV1().status}") + + // Wait some time for the indexer to process the tx + kotlinx.coroutines.delay(3000) + + // Print the new package version (should now be 2) + for (changedObj in effectsUpgrade2.asV1().changedObjects) { + if (changedObj.outputState is ObjectOut.PackageWrite) { + val pkgId = changedObj.objectId + val version = (changedObj.outputState as ObjectOut.PackageWrite).version + println("PackageId: ${pkgId.toHex()}") + println("Package version: $version") + } + } + } catch (e: Exception) { + e.printStackTrace() + } +} diff --git a/bindings/python/examples/publish_upgrade.py b/bindings/python/examples/publish_upgrade.py new file mode 100644 index 000000000..c2860bda7 --- /dev/null +++ b/bindings/python/examples/publish_upgrade.py @@ -0,0 +1,157 @@ +# Copyright (c) 2025 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +from lib.iota_sdk_ffi import * + +import asyncio +import json +import base64 + +# Compiled `first_package` example +SERIALIZED_FIRST_PACKAGE = '{"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]}' + + +async def main(): + try: + # Parse the compiled `first_package` example from the monorepo created with + # `iota move build --dump-bytecode-as-base64` + data = json.loads(SERIALIZED_FIRST_PACKAGE) + modules = [base64.b64decode(module) for module in data["modules"]] + dependencies = [ObjectId.from_hex(dep) for dep in data["dependencies"]] + compiled_package_digest = bytes(data["digest"]) + print(f"Compiled Package Digest: {compiled_package_digest.hex()}") + + # Create a random private key to derive a sender address and for signing + private_key = Ed25519PrivateKey.generate() + public_key = private_key.public_key() + sender = public_key.derive_address() + print(f"Sender: {sender.to_hex()}") + + # Fund the sender address for gas payment + faucet = FaucetClient.new_localnet() + faucet_receipt = await faucet.request_and_wait(sender) + if faucet_receipt is None: + raise Exception("Failed to request coins from faucet") + print(f"Available Balance: {sum(coin.amount for coin in faucet_receipt.sent)}") + + client = GraphQlClient.new_localnet() + + # Build the `publish` PTB, that consists of 2 steps + builder = await TransactionBuilder.init(sender, client) + + # 1. Create the upgrade cap + builder.publish(modules, dependencies, "upgrade_cap") + + # 2. Transfer the upgrade cap to the sender address + builder.transfer_objects(sender, [PtbArgument.res("upgrade_cap")]) + + # Finalize the PTB + tx = await builder.finish() + + # Perform a dry-run to check if everything is fine + result = await client.dry_run_tx(tx, False) + if result.error is not None: + raise Exception(f"Dry run failed: {result.error}") + if result.effects is None: + raise Exception("Dry run failed: no effects") + print(f"Effects status (dry run): {result.effects.as_v1().status}") + + # Sign and execute the transaction (publish the package) + print("Publishing package") + signature = private_key.try_sign_simple(tx.signing_digest()) + user_signature = UserSignature.new_simple(signature) + effects = await client.execute_tx([user_signature], tx) + if effects is None: + raise Exception("Transaction failed: no effects") + print(f"Effects status (publish): {effects.as_v1().status}") + + # Wait some time for the indexer to process the tx + await asyncio.sleep(3) + + # Resolve UpgradeCap and PackageId via the client + upgrade_cap = None + package_id = None + + for changed_obj in effects.as_v1().changed_objects: + if hasattr(changed_obj.output_state, 'owner'): + # ObjectWrite + object_id = changed_obj.object_id + obj = await client.object(object_id, None) + if obj is None: + raise Exception(f"Missing object {object_id.to_hex()}") + upgrade_cap_type = StructTag(Address.framework(), Identifier("package"), Identifier("UpgradeCap")) + if str(obj.as_struct().struct_type) == str(upgrade_cap_type): + upgrade_cap = object_id + elif hasattr(changed_obj.output_state, 'version'): + # PackageWrite + pkg_id = changed_obj.object_id + package_id = pkg_id + + if upgrade_cap is None: + raise Exception("Missing upgrade cap") + if package_id is None: + raise Exception("Missing package id") + + # Build the `upgrade` PTB, that consists of 3 steps + builder = await TransactionBuilder.init(sender, client) + + upgrade_cap_arg = PtbArgument.object_id(upgrade_cap) + upgrade_policy_arg = PtbArgument.u8(0) + compiled_package_digest_arg = PtbArgument.u8_vec(compiled_package_digest) + + # 1. Create the upgrade ticket + builder.move_call( + Address.framework(), + Identifier("package"), + Identifier("authorize_upgrade"), + [upgrade_cap_arg, upgrade_policy_arg, compiled_package_digest_arg], + names=["upgrade_ticket"] + ) + + # 2. Get the upgrade receipt + builder.upgrade(modules, dependencies, package_id, PtbArgument.res("upgrade_ticket"), "upgrade_receipt") + + # 3. Finalize the upgrade + builder.move_call( + Address.framework(), + Identifier("package"), + Identifier("commit_upgrade"), + [upgrade_cap_arg, PtbArgument.res("upgrade_receipt")] + ) + + # Finalize the PTB + tx = await builder.finish() + + # Perform a dry-run to check if everything is fine + result = await client.dry_run_tx(tx, False) + if result.error is not None: + raise Exception(f"Dry run failed: {result.error}") + if result.effects is None: + raise Exception("Dry run failed: no effects") + print(f"Effects status (dry run): {result.effects.as_v1().status}") + + # Sign and execute the transaction (upgrade the package) + print("Upgrading package") + signature = private_key.try_sign_simple(tx.signing_digest()) + user_signature = UserSignature.new_simple(signature) + effects = await client.execute_tx([user_signature], tx) + if effects is None: + raise Exception("Transaction failed: no effects") + print(f"Effects status (upgrade): {effects.as_v1().status}") + + # Wait some time for the indexer to process the tx + await asyncio.sleep(3) + + # Print the new package version (should now be 2) + for changed_obj in effects.as_v1().changed_objects: + if hasattr(changed_obj.output_state, 'version'): + pkg_id = changed_obj.object_id + print(f"PackageId: {pkg_id.to_hex()}") + print(f"Package version: {changed_obj.output_state.version}") + + except Exception as e: + print(f"Error: {e}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/crates/iota-sdk-types/src/type_tag/mod.rs b/crates/iota-sdk-types/src/type_tag/mod.rs index 4ee67041e..628349aed 100644 --- a/crates/iota-sdk-types/src/type_tag/mod.rs +++ b/crates/iota-sdk-types/src/type_tag/mod.rs @@ -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 } diff --git a/crates/iota-sdk/examples/publish_upgrade.rs b/crates/iota-sdk/examples/publish_upgrade.rs new file mode 100644 index 000000000..69147c2cd --- /dev/null +++ b/crates/iota-sdk/examples/publish_upgrade.rs @@ -0,0 +1,186 @@ +// 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::(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::() + ); + + 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::; + let mut package_id = None::; + + 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]}"#; diff --git a/crates/iota-transaction-builder/src/builder/mod.rs b/crates/iota-transaction-builder/src/builder/mod.rs index 0fd3097fa..da431397d 100644 --- a/crates/iota-transaction-builder/src/builder/mod.rs +++ b/crates/iota-transaction-builder/src/builder/mod.rs @@ -452,14 +452,14 @@ impl TransactionBuilder { pub fn upgrade( &mut self, package_id: ObjectId, - upgrade_cap: U, + upgrade_ticket: U, kind: impl Into, ) -> &mut TransactionBuilder { 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, diff --git a/crates/iota-transaction-builder/src/publish_type.rs b/crates/iota-transaction-builder/src/publish_type.rs index 75638a4bf..edf544cc9 100644 --- a/crates/iota-transaction-builder/src/publish_type.rs +++ b/crates/iota-transaction-builder/src/publish_type.rs @@ -39,7 +39,7 @@ impl From 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")]