From d17c441cc3c6fe3b85760a53e54067f80d4c44e8 Mon Sep 17 00:00:00 2001 From: Yang Hau Date: Fri, 10 Oct 2025 12:19:43 +0700 Subject: [PATCH] feat: Return ObjectChange in ExecuteTx and DryRun In the previous JSON RPC, iota_executeTransactionBlock and dry run would return object changes and balance changes. However, thee fields are not returned in the current GraphQL sdk. --- .../go/examples/execute_transaction/main.go | 129 ++++++++++++++++++ .../execute_transaction_dry_run/main.go | 102 ++++++++++++++ .../go/examples/prepare_split_coins/main.go | 8 +- bindings/go/iota_sdk_ffi/iota_sdk_ffi.go | 6 + bindings/kotlin/lib/iota_sdk/iota_sdk_ffi.kt | 15 +- bindings/python/lib/iota_sdk_ffi.py | 19 ++- crates/iota-graphql-client/src/lib.rs | 61 ++++++++- .../src/query_types/execute_tx.rs | 10 +- .../src/query_types/mod.rs | 13 +- .../src/query_types/transaction.rs | 44 ++++++ .../iota-sdk-ffi/src/types/transaction/v1.rs | 6 + crates/iota-sdk-types/src/effects/v1.rs | 6 + 12 files changed, 401 insertions(+), 18 deletions(-) create mode 100644 bindings/go/examples/execute_transaction/main.go create mode 100644 bindings/go/examples/execute_transaction_dry_run/main.go diff --git a/bindings/go/examples/execute_transaction/main.go b/bindings/go/examples/execute_transaction/main.go new file mode 100644 index 000000000..1b5e11384 --- /dev/null +++ b/bindings/go/examples/execute_transaction/main.go @@ -0,0 +1,129 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "log" + + sdk "bindings/iota_sdk_ffi" +) + +func main() { + client := sdk.GraphQlClientNewTestnet() + + // some fix values on Testnet + privateKeyHex := "0032e67709569b6a1ef551ea7a745aac943b2158fc862ae7c6d08dd1744e15d818" + senderAddressHex := "0x786dff8a4ee13d45b502c8f22f398e3517e6ec78aa4ae564c348acb07fad7f50" + coinObjectIdHex := "0x7714345721694a20c6e2e04a6a289b1c08710cda863f7dd49b4661cde524103d" + gasObjectIdHex := "0xd7913b6c9c1e67c281ad13bffcb4d172ffb8f5a85581e5d5984eacb310365be8" + + // Load keypair from hex using SimpleKeypair + privateKeyBytes, err := sdk.HexDecode(privateKeyHex) + if err != nil { + log.Fatalf("Failed to decode private key: %v", err) + } + + simpleKeypair, err := sdk.SimpleKeypairFromBytes(privateKeyBytes) + if err != nil { + log.Fatalf("Failed to load private key: %v", err) + } + + fromAddress, _ := sdk.AddressFromHex(senderAddressHex) + toAddress, _ := sdk.AddressFromHex(senderAddressHex) + coinObjId, _ := sdk.ObjectIdFromHex(coinObjectIdHex) + gasCoinObjId, _ := sdk.ObjectIdFromHex(gasObjectIdHex) + + log.Printf("Building transaction from %s to %s\n", fromAddress.ToHex(), toAddress.ToHex()) + + // Build the transaction + builder := sdk.TransactionBuilderInit(fromAddress, client) + builder.TransferObjects(toAddress, []*sdk.PtbArgument{sdk.PtbArgumentObjectId(coinObjId)}) + builder.Gas(gasCoinObjId).GasBudget(1000000000) + + // Finish building to get the transaction + txn, err := builder.Finish() + if err.(*sdk.SdkFfiError) != nil { + log.Fatalf("Failed to build transaction: %v", err) + } + + log.Printf("Transaction built, signing with keypair...\n") + + // Sign the transaction using SimpleKeypair + simpleSignature, err := simpleKeypair.TrySign(txn.SigningDigest()) + if err != nil { + log.Fatalf("Failed to sign transaction: %v", err) + } + + // Convert SimpleSignature to UserSignature + userSignature := sdk.UserSignatureNewSimple(simpleSignature) + + log.Printf("Transaction signed, executing on-chain...\n") + + // Execute the transaction on-chain + effects, err := client.ExecuteTx([]*sdk.UserSignature{userSignature}, txn) + if err.(*sdk.SdkFfiError) != nil { + log.Fatalf("ExecuteTx failed: %v", err) + } + + if effects == nil { + log.Println("No transaction effects returned") + return + } + + log.Printf("Transaction executed successfully!\n") + + // Access and print transaction effects + PrintObjectChanges(*effects) +} + +func PrintObjectChanges(effects *sdk.TransactionEffects) { + log.Println("=== Object Changes (from ExecuteTx) ===") + + if !effects.IsV1() { + log.Panicln("Effects version is not V1") + } + + effectsV1 := effects.AsV1() + log.Printf("Total changed objects: %d\n", len(effectsV1.ChangedObjects)) + + for i, change := range effectsV1.ChangedObjects { + log.Printf("Object #%d:\n", i+1) + log.Printf(" Object ID: %s\n", change.ObjectId.ToHex()) + + // Check creation/deletion status using IdOperation + switch change.IdOperation { + case sdk.IdOperationCreated: + log.Println(" Status: CREATED") + case sdk.IdOperationDeleted: + log.Println(" Status: DELETED") + case sdk.IdOperationNone: + log.Println(" Status: MODIFIED") + } + + // Object type (if available) + if change.ObjectType != nil { + log.Printf(" Type: %s\n", *change.ObjectType) + } else { + log.Printf(" Type: %v\n", change.ObjectType) + } + + // Input state (state before transaction) + switch input := change.InputState.(type) { + case sdk.ObjectInMissing: + log.Println(" Input State: Missing (new object)") + case sdk.ObjectInData: + log.Printf(" Input State: Version=%d, Owner=%s\n", input.Version, input.Owner.String()) + } + + // Output state (state after transaction) + switch output := change.OutputState.(type) { + case sdk.ObjectOutMissing: + log.Println(" Output State: Missing (deleted)") + case sdk.ObjectOutObjectWrite: + log.Printf(" Output State: ObjectWrite, Owner=%s\n", output.Owner.String()) + case sdk.ObjectOutPackageWrite: + log.Printf(" Output State: PackageWrite, Version=%d\n", output.Version) + } + } +} diff --git a/bindings/go/examples/execute_transaction_dry_run/main.go b/bindings/go/examples/execute_transaction_dry_run/main.go new file mode 100644 index 000000000..b5a2097c3 --- /dev/null +++ b/bindings/go/examples/execute_transaction_dry_run/main.go @@ -0,0 +1,102 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "log" + + sdk "bindings/iota_sdk_ffi" +) + +// === DryRun with ObjectChange Example === +// This example demonstrates the ObjectChange feature using DryRun, +// which simulates transaction execution without actual on-chain changes. +func main() { + + // Initialize client + client := sdk.GraphQlClientNewDevnet() + + // Use actual addresses from devnet (these are examples) + fromAddress, _ := sdk.AddressFromHex("0x611830d3641a68f94a690dcc25d1f4b0dac948325ac18f6dd32564371735f32c") + + toAddress, _ := sdk.AddressFromHex("0x0000a4984bd495d4346fa208ddff4f5d5e5ad48c21dec631ddebc99809f16900") + + coinObjId, _ := sdk.ObjectIdFromHex("0xd04077fe3b6fad13b3d4ed0d535b7ca92afcac8f0f2a0e0925fb9f4f0b30c699") + + gasCoinObjId, _ := sdk.ObjectIdFromHex("0x0b0270ee9d27da0db09651e5f7338dfa32c7ee6441ccefa1f6e305735bcfc7ab") + + builder := sdk.TransactionBuilderInit(fromAddress, client) + builder.TransferObjects(toAddress, []*sdk.PtbArgument{sdk.PtbArgumentObjectId(coinObjId)}) + builder.Gas(gasCoinObjId).GasBudget(1000000000) + + dryRunResult, err := builder.DryRun(false) + if err.(*sdk.SdkFfiError) != nil { + log.Fatalf("Dry run failed: %v", err) + } + + if dryRunResult.Error != nil { + log.Fatalf("Dry run returned an error: %s\n", *dryRunResult.Error) + } + + log.Printf("Dry run succeeded!\n") + + // Access transaction effects from dry run + if dryRunResult.Effects != nil { + PrintObjectChanges(*dryRunResult.Effects) + } else { + log.Println("No transaction effects available in dry run result") + } +} + +func PrintObjectChanges(effects *sdk.TransactionEffects) { + log.Println("=== Object Changes (from DryRun) ===") + + if !effects.IsV1() { + log.Println("Effects version is not V1") + return + } + + effectsV1 := effects.AsV1() + log.Printf("Total changed objects: %d\n", len(effectsV1.ChangedObjects)) + + for i, change := range effectsV1.ChangedObjects { + log.Printf("Object #%d:\n", i+1) + log.Printf(" Object ID: %s\n", change.ObjectId.ToHex()) + + // Check creation/deletion status using IdOperation + switch change.IdOperation { + case sdk.IdOperationCreated: + log.Println(" Status: CREATED") + case sdk.IdOperationDeleted: + log.Println(" Status: DELETED") + case sdk.IdOperationNone: + log.Println(" Status: MODIFIED") + } + + // Object type (if available) + if change.ObjectType != nil { + log.Printf(" Type: %s\n", *change.ObjectType) + } else { + log.Printf(" Type: %v\n", change.ObjectType) + } + + // Input state (state before transaction) + switch input := change.InputState.(type) { + case sdk.ObjectInMissing: + log.Println(" Input State: Missing (new object)") + case sdk.ObjectInData: + log.Printf(" Input State: Version=%d, Owner=%s\n", input.Version, input.Owner.String()) + } + + // Output state (state after transaction) + switch output := change.OutputState.(type) { + case sdk.ObjectOutMissing: + log.Println(" Output State: Missing (deleted)") + case sdk.ObjectOutObjectWrite: + log.Printf(" Output State: ObjectWrite, Owner=%s\n", output.Owner.String()) + case sdk.ObjectOutPackageWrite: + log.Printf(" Output State: PackageWrite, Version=%d\n", output.Version) + } + } +} diff --git a/bindings/go/examples/prepare_split_coins/main.go b/bindings/go/examples/prepare_split_coins/main.go index 54dfc8375..419caab8d 100644 --- a/bindings/go/examples/prepare_split_coins/main.go +++ b/bindings/go/examples/prepare_split_coins/main.go @@ -30,7 +30,6 @@ func main() { sender, []*sdk.PtbArgument{sdk.PtbArgumentRes("coin1"), sdk.PtbArgumentRes("coin2"), sdk.PtbArgumentRes("coin3")}, ) - builder.Gas(coinObjId).GasBudget(1000000000) txn, err := builder.Finish() if err.(*sdk.SdkFfiError) != nil { @@ -44,14 +43,13 @@ func main() { log.Printf("Signing Digest: %v", sdk.HexEncode(txn.SigningDigest())) log.Printf("Txn Bytes: %v", sdk.Base64Encode(txnBytes)) - res, err := builder.DryRun(false) + skipChecks := bool(false) + res, err := client.DryRunTx(txn, &skipChecks) if err.(*sdk.SdkFfiError) != nil { - log.Fatalf("Failed to split coins: %v", err) + log.Fatalf("Failed to dry run split coins: %v", err) } - if res.Error != nil { log.Fatalf("Failed to split coins: %v", *res.Error) } - log.Print("Split coins dry run was successful!") } diff --git a/bindings/go/iota_sdk_ffi/iota_sdk_ffi.go b/bindings/go/iota_sdk_ffi/iota_sdk_ffi.go index bfdbc524b..0300d374d 100644 --- a/bindings/go/iota_sdk_ffi/iota_sdk_ffi.go +++ b/bindings/go/iota_sdk_ffi/iota_sdk_ffi.go @@ -25290,6 +25290,9 @@ type ChangedObject struct { // This information isn't required by the protocol but is useful for // providing more detailed semantics on object changes. IdOperation IdOperation + // Optional object type information. This is not part of the BCS protocol + // data but can be populated from other sources when available. + ObjectType *string } func (r *ChangedObject) Destroy() { @@ -25297,6 +25300,7 @@ func (r *ChangedObject) Destroy() { FfiDestroyerObjectIn{}.Destroy(r.InputState); FfiDestroyerObjectOut{}.Destroy(r.OutputState); FfiDestroyerIdOperation{}.Destroy(r.IdOperation); + FfiDestroyerOptionalString{}.Destroy(r.ObjectType); } type FfiConverterChangedObject struct {} @@ -25313,6 +25317,7 @@ func (c FfiConverterChangedObject) Read(reader io.Reader) ChangedObject { FfiConverterObjectInINSTANCE.Read(reader), FfiConverterObjectOutINSTANCE.Read(reader), FfiConverterIdOperationINSTANCE.Read(reader), + FfiConverterOptionalStringINSTANCE.Read(reader), } } @@ -25325,6 +25330,7 @@ func (c FfiConverterChangedObject) Write(writer io.Writer, value ChangedObject) FfiConverterObjectInINSTANCE.Write(writer, value.InputState); FfiConverterObjectOutINSTANCE.Write(writer, value.OutputState); FfiConverterIdOperationINSTANCE.Write(writer, value.IdOperation); + FfiConverterOptionalStringINSTANCE.Write(writer, value.ObjectType); } type FfiDestroyerChangedObject struct {} diff --git a/bindings/kotlin/lib/iota_sdk/iota_sdk_ffi.kt b/bindings/kotlin/lib/iota_sdk/iota_sdk_ffi.kt index f4f3c06f3..4fa218502 100644 --- a/bindings/kotlin/lib/iota_sdk/iota_sdk_ffi.kt +++ b/bindings/kotlin/lib/iota_sdk/iota_sdk_ffi.kt @@ -45255,7 +45255,12 @@ data class ChangedObject ( * This information isn't required by the protocol but is useful for * providing more detailed semantics on object changes. */ - var `idOperation`: IdOperation + var `idOperation`: IdOperation, + /** + * Optional object type information. This is not part of the BCS protocol + * data but can be populated from other sources when available. + */ + var `objectType`: kotlin.String? = null ) : Disposable { @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here @@ -45265,7 +45270,8 @@ data class ChangedObject ( this.`objectId`, this.`inputState`, this.`outputState`, - this.`idOperation` + this.`idOperation`, + this.`objectType` ) } @@ -45282,6 +45288,7 @@ public object FfiConverterTypeChangedObject: FfiConverterRustBuffer(&bcs)) .transpose()?; + // Populate object_type field from GraphQL object_changes + if let Some(ref mut effects) = effects { + if let Some(ref txn_block_ref) = txn_block { + if let Some(ref effects_gql) = txn_block_ref.effects { + populate_object_types(effects, &effects_gql.object_changes.nodes); + } + } + } + // Extract transaction let transaction = txn_block .and_then(|tx| tx.bcs) @@ -1475,8 +1484,9 @@ impl Client { if let Some(data) = response.data { let result = data.execute_transaction_block; let bcs = base64ct::Base64::decode_vec(result.effects.bcs.0.as_str())?; - let effects: TransactionEffects = bcs::from_bytes(&bcs)?; + let mut effects: TransactionEffects = bcs::from_bytes(&bcs)?; + populate_object_types(&mut effects, result.effects.object_changes.nodes.as_slice()); Ok(Some(effects)) } else { Ok(None) @@ -1829,6 +1839,53 @@ impl Client { } } +/// Helper function to populate object_type fields in TransactionEffects +/// from GraphQL ObjectChange data +fn populate_object_types( + effects: &mut TransactionEffects, + object_changes: &[query_types::TransactionObjectChange], +) { + use iota_types::TransactionEffects; + use query_types::TransactionObjectChange as ObjectChange; + + // Get the changed_objects from the effects based on version + match effects { + TransactionEffects::V1(ref mut effects_v1) => { + // Create a map of object_id -> object_type from GraphQL data + let type_map: std::collections::HashMap = object_changes + .iter() + .filter_map(|change: &ObjectChange| { + // Try to get type from output_state first, then input_state + let object_type = change + .output_state + .as_ref() + .and_then(|obj| obj.as_move_object.as_ref()) + .and_then(|move_obj| move_obj.contents.as_ref()) + .map(|contents| contents.type_.repr.clone()) + .or_else(|| { + change + .input_state + .as_ref() + .and_then(|obj| obj.as_move_object.as_ref()) + .and_then(|move_obj| move_obj.contents.as_ref()) + .map(|contents| contents.type_.repr.clone()) + }); + + // Convert Address to ObjectId + object_type.map(|typ| (ObjectId::from(change.address), typ)) + }) + .collect(); + + // Populate the object_type field for each changed object + for changed_obj in &mut effects_v1.changed_objects { + if let Some(object_type) = type_map.get(&changed_obj.object_id) { + changed_obj.object_type = Some(object_type.clone()); + } + } + } + } +} + // This function is used in tests to create a new client instance for the local // server. #[cfg(test)] diff --git a/crates/iota-graphql-client/src/query_types/execute_tx.rs b/crates/iota-graphql-client/src/query_types/execute_tx.rs index ab91d29be..a8894112b 100644 --- a/crates/iota-graphql-client/src/query_types/execute_tx.rs +++ b/crates/iota-graphql-client/src/query_types/execute_tx.rs @@ -2,7 +2,7 @@ // Modifications Copyright (c) 2025 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use crate::query_types::{Base64, schema}; +use crate::query_types::{Base64, PageInfo, schema, transaction}; #[derive(cynic::QueryFragment, Debug)] #[cynic( @@ -32,4 +32,12 @@ pub struct ExecutionResult { #[cynic(schema = "rpc", graphql_type = "TransactionBlockEffects")] pub struct TransactionBlockEffects { pub bcs: Base64, + pub object_changes: ObjectChangeConnection, +} + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema = "rpc", graphql_type = "ObjectChangeConnection")] +pub struct ObjectChangeConnection { + pub page_info: PageInfo, + pub nodes: Vec, } diff --git a/crates/iota-graphql-client/src/query_types/mod.rs b/crates/iota-graphql-client/src/query_types/mod.rs index 0c6df8e51..109588fa2 100644 --- a/crates/iota-graphql-client/src/query_types/mod.rs +++ b/crates/iota-graphql-client/src/query_types/mod.rs @@ -42,7 +42,9 @@ pub use dynamic_fields::{ }; pub use epoch::{Epoch, EpochArgs, EpochQuery, EpochSummaryQuery, ValidatorSet}; pub use events::{Event, EventConnection, EventFilter, EventsQuery, EventsQueryArgs}; -pub use execute_tx::{ExecuteTransactionArgs, ExecuteTransactionQuery, ExecutionResult}; +pub use execute_tx::{ + ExecuteTransactionArgs, ExecuteTransactionQuery, ExecutionResult, ObjectChangeConnection, +}; pub use iota_names::{ IotaNamesAddressDefaultNameQuery, IotaNamesAddressRegistrationsQuery, IotaNamesDefaultNameArgs, IotaNamesDefaultNameQuery, IotaNamesRegistrationsArgs, IotaNamesRegistrationsQuery, @@ -72,10 +74,11 @@ pub use protocol_config::{ use serde_json::Value as JsonValue; pub use service_config::{Feature, ServiceConfig, ServiceConfigQuery}; pub use transaction::{ - TransactionBlock, TransactionBlockArgs, TransactionBlockEffectsQuery, - TransactionBlockKindInput, TransactionBlockQuery, TransactionBlockWithEffects, - TransactionBlockWithEffectsQuery, TransactionBlocksEffectsQuery, TransactionBlocksQuery, - TransactionBlocksQueryArgs, TransactionBlocksWithEffectsQuery, TransactionsFilter, + ObjectChange as TransactionObjectChange, TransactionBlock, TransactionBlockArgs, + TransactionBlockEffectsQuery, TransactionBlockKindInput, TransactionBlockQuery, + TransactionBlockWithEffects, TransactionBlockWithEffectsQuery, TransactionBlocksEffectsQuery, + TransactionBlocksQuery, TransactionBlocksQueryArgs, TransactionBlocksWithEffectsQuery, + TransactionsFilter, }; use crate::error; diff --git a/crates/iota-graphql-client/src/query_types/transaction.rs b/crates/iota-graphql-client/src/query_types/transaction.rs index 10b8f0317..0144a459a 100644 --- a/crates/iota-graphql-client/src/query_types/transaction.rs +++ b/crates/iota-graphql-client/src/query_types/transaction.rs @@ -130,6 +130,50 @@ pub struct TxBlockEffects { #[cynic(schema = "rpc", graphql_type = "TransactionBlockEffects")] pub struct TransactionBlockEffects { pub bcs: Option, + pub object_changes: ObjectChangeConnection, +} + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema = "rpc", graphql_type = "ObjectChangeConnection")] +pub struct ObjectChangeConnection { + pub page_info: PageInfo, + pub nodes: Vec, +} + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema = "rpc", graphql_type = "ObjectChange")] +pub struct ObjectChange { + pub address: Address, + pub input_state: Option, + pub output_state: Option, + pub id_created: Option, + pub id_deleted: Option, +} + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema = "rpc", graphql_type = "Object")] +pub struct Object { + pub bcs: Option, + pub as_move_object: Option, +} + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema = "rpc", graphql_type = "MoveObject")] +pub struct MoveObject { + pub contents: Option, +} + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema = "rpc", graphql_type = "MoveValue")] +pub struct MoveValue { + #[cynic(rename = "type")] + pub type_: MoveType, +} + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema = "rpc", graphql_type = "MoveType")] +pub struct MoveType { + pub repr: String, } #[derive(cynic::Enum, Clone, Copy, Debug)] diff --git a/crates/iota-sdk-ffi/src/types/transaction/v1.rs b/crates/iota-sdk-ffi/src/types/transaction/v1.rs index 48c67894b..890122f7a 100644 --- a/crates/iota-sdk-ffi/src/types/transaction/v1.rs +++ b/crates/iota-sdk-ffi/src/types/transaction/v1.rs @@ -139,6 +139,10 @@ pub struct ChangedObject { /// This information isn't required by the protocol but is useful for /// providing more detailed semantics on object changes. pub id_operation: IdOperation, + /// Optional object type information. This is not part of the BCS protocol + /// data but can be populated from other sources when available. + #[uniffi(default = None)] + pub object_type: Option, } impl From for ChangedObject { @@ -148,6 +152,7 @@ impl From for ChangedObject { input_state: value.input_state.into(), output_state: value.output_state.into(), id_operation: value.id_operation, + object_type: value.object_type, } } } @@ -159,6 +164,7 @@ impl From for iota_types::ChangedObject { input_state: value.input_state.into(), output_state: value.output_state.into(), id_operation: value.id_operation, + object_type: value.object_type, } } } diff --git a/crates/iota-sdk-types/src/effects/v1.rs b/crates/iota-sdk-types/src/effects/v1.rs index 223c17a0d..b99dc841c 100644 --- a/crates/iota-sdk-types/src/effects/v1.rs +++ b/crates/iota-sdk-types/src/effects/v1.rs @@ -115,6 +115,12 @@ pub struct ChangedObject { /// This information isn't required by the protocol but is useful for /// providing more detailed semantics on object changes. pub id_operation: IdOperation, + /// Optional object type information. This is not part of the BCS protocol + /// data but can be populated from other sources when available. + #[cfg_attr(feature = "serde", serde(skip))] + #[cfg_attr(feature = "proptest", strategy(proptest::strategy::Just(None)))] + #[cfg_attr(feature = "schemars", schemars(skip))] + pub object_type: Option, } /// A shared object that wasn't changed during execution