Skip to content

Commit bab37df

Browse files
committed
feat: Return ObjectChange in ExecuteTx and DryRun
1 parent 8cfe701 commit bab37df

File tree

8 files changed

+273
-7
lines changed

8 files changed

+273
-7
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) 2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package main
5+
6+
import (
7+
"log"
8+
9+
sdk "bindings/iota_sdk_ffi"
10+
)
11+
12+
// === DryRun with ObjectChange Example ===
13+
// This example demonstrates the ObjectChange feature using DryRun,
14+
// which simulates transaction execution without actual on-chain changes.
15+
func main() {
16+
17+
// Initialize client
18+
client := sdk.GraphQlClientNewDevnet()
19+
20+
// Use actual addresses from devnet (these are examples)
21+
fromAddress, _ := sdk.AddressFromHex("0x611830d3641a68f94a690dcc25d1f4b0dac948325ac18f6dd32564371735f32c")
22+
23+
toAddress, _ := sdk.AddressFromHex("0x0000a4984bd495d4346fa208ddff4f5d5e5ad48c21dec631ddebc99809f16900")
24+
25+
coinObjId, _ := sdk.ObjectIdFromHex("0xd04077fe3b6fad13b3d4ed0d535b7ca92afcac8f0f2a0e0925fb9f4f0b30c699")
26+
27+
gasCoinObjId, _ := sdk.ObjectIdFromHex("0x0b0270ee9d27da0db09651e5f7338dfa32c7ee6441ccefa1f6e305735bcfc7ab")
28+
29+
builder := sdk.TransactionBuilderInit(fromAddress, client)
30+
builder.TransferObjects(toAddress, []*sdk.PtbArgument{sdk.PtbArgumentObjectId(coinObjId)})
31+
builder.Gas(gasCoinObjId).GasBudget(1000000000)
32+
33+
dryRunResult, err := builder.DryRun(false)
34+
if err.(*sdk.SdkFfiError) != nil {
35+
log.Fatalf("Dry run failed: %v", err)
36+
}
37+
38+
if dryRunResult.Error != nil {
39+
log.Fatalf("Dry run returned an error: %s\n", *dryRunResult.Error)
40+
}
41+
42+
log.Printf("Dry run succeeded!\n")
43+
44+
// Access transaction effects from dry run
45+
if dryRunResult.Effects != nil {
46+
PrintObjectChanges(*dryRunResult.Effects)
47+
} else {
48+
log.Println("No transaction effects available in dry run result")
49+
}
50+
}
51+
52+
func PrintObjectChanges(effects *sdk.TransactionEffects) {
53+
log.Println("=== Object Changes (from DryRun) ===")
54+
55+
if !effects.IsV1() {
56+
log.Println("Effects version is not V1")
57+
return
58+
}
59+
60+
effectsV1 := effects.AsV1()
61+
log.Printf("Total changed objects: %d\n", len(effectsV1.ChangedObjects))
62+
63+
for i, change := range effectsV1.ChangedObjects {
64+
log.Printf("Object #%d:\n", i+1)
65+
log.Printf(" Object ID: %s\n", change.ObjectId.ToHex())
66+
67+
// Check creation/deletion status using IdOperation
68+
switch change.IdOperation {
69+
case sdk.IdOperationCreated:
70+
log.Println(" Status: CREATED")
71+
case sdk.IdOperationDeleted:
72+
log.Println(" Status: DELETED")
73+
case sdk.IdOperationNone:
74+
log.Println(" Status: MODIFIED")
75+
}
76+
77+
// Object type (if available)
78+
if change.ObjectType != nil {
79+
log.Printf(" Type: %s\n", *change.ObjectType)
80+
} else {
81+
log.Printf(" Type: %v\n", change.ObjectType)
82+
}
83+
84+
// Input state (state before transaction)
85+
switch input := change.InputState.(type) {
86+
case sdk.ObjectInMissing:
87+
log.Println(" Input State: Missing (new object)")
88+
case sdk.ObjectInData:
89+
log.Printf(" Input State: Version=%d, Owner=%s\n", input.Version, input.Owner.String())
90+
}
91+
92+
// Output state (state after transaction)
93+
switch output := change.OutputState.(type) {
94+
case sdk.ObjectOutMissing:
95+
log.Println(" Output State: Missing (deleted)")
96+
case sdk.ObjectOutObjectWrite:
97+
log.Printf(" Output State: ObjectWrite, Owner=%s\n", output.Owner.String())
98+
case sdk.ObjectOutPackageWrite:
99+
log.Printf(" Output State: PackageWrite, Version=%d\n", output.Version)
100+
}
101+
}
102+
}

bindings/go/iota_sdk_ffi/iota_sdk_ffi.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24195,13 +24195,17 @@ type ChangedObject struct {
2419524195
// This information isn't required by the protocol but is useful for
2419624196
// providing more detailed semantics on object changes.
2419724197
IdOperation IdOperation
24198+
// Optional object type information. This is not part of the BCS protocol
24199+
// data but can be populated from other sources when available.
24200+
ObjectType *string
2419824201
}
2419924202

2420024203
func (r *ChangedObject) Destroy() {
2420124204
FfiDestroyerObjectId{}.Destroy(r.ObjectId);
2420224205
FfiDestroyerObjectIn{}.Destroy(r.InputState);
2420324206
FfiDestroyerObjectOut{}.Destroy(r.OutputState);
2420424207
FfiDestroyerIdOperation{}.Destroy(r.IdOperation);
24208+
FfiDestroyerOptionalString{}.Destroy(r.ObjectType);
2420524209
}
2420624210

2420724211
type FfiConverterChangedObject struct {}
@@ -24218,6 +24222,7 @@ func (c FfiConverterChangedObject) Read(reader io.Reader) ChangedObject {
2421824222
FfiConverterObjectInINSTANCE.Read(reader),
2421924223
FfiConverterObjectOutINSTANCE.Read(reader),
2422024224
FfiConverterIdOperationINSTANCE.Read(reader),
24225+
FfiConverterOptionalStringINSTANCE.Read(reader),
2422124226
}
2422224227
}
2422324228

@@ -24230,6 +24235,7 @@ func (c FfiConverterChangedObject) Write(writer io.Writer, value ChangedObject)
2423024235
FfiConverterObjectInINSTANCE.Write(writer, value.InputState);
2423124236
FfiConverterObjectOutINSTANCE.Write(writer, value.OutputState);
2423224237
FfiConverterIdOperationINSTANCE.Write(writer, value.IdOperation);
24238+
FfiConverterOptionalStringINSTANCE.Write(writer, value.ObjectType);
2423324239
}
2423424240

2423524241
type FfiDestroyerChangedObject struct {}

crates/iota-graphql-client/src/lib.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ impl Client {
266266
.data
267267
.and_then(|tx| tx.dry_run_transaction_block.transaction);
268268

269-
let effects = txn_block
269+
let mut effects = txn_block
270270
.as_ref()
271271
.and_then(|tx| tx.effects.as_ref())
272272
.and_then(|tx| tx.bcs.as_ref())
@@ -275,6 +275,15 @@ impl Client {
275275
.map(|bcs| bcs::from_bytes::<TransactionEffects>(&bcs))
276276
.transpose()?;
277277

278+
// Populate object_type field from GraphQL object_changes
279+
if let Some(ref mut effects) = effects {
280+
if let Some(ref txn_block_ref) = txn_block {
281+
if let Some(ref effects_gql) = txn_block_ref.effects {
282+
populate_object_types(effects, &effects_gql.object_changes.nodes);
283+
}
284+
}
285+
}
286+
278287
// Extract transaction
279288
let transaction = txn_block
280289
.and_then(|tx| tx.bcs)
@@ -1801,6 +1810,53 @@ impl Client {
18011810
}
18021811
}
18031812

1813+
/// Helper function to populate object_type fields in TransactionEffects
1814+
/// from GraphQL ObjectChange data
1815+
fn populate_object_types(
1816+
effects: &mut TransactionEffects,
1817+
object_changes: &[query_types::TransactionObjectChange],
1818+
) {
1819+
use iota_types::TransactionEffects;
1820+
use query_types::TransactionObjectChange as ObjectChange;
1821+
1822+
// Get the changed_objects from the effects based on version
1823+
match effects {
1824+
TransactionEffects::V1(ref mut effects_v1) => {
1825+
// Create a map of object_id -> object_type from GraphQL data
1826+
let type_map: std::collections::HashMap<ObjectId, String> = object_changes
1827+
.iter()
1828+
.filter_map(|change: &ObjectChange| {
1829+
// Try to get type from output_state first, then input_state
1830+
let object_type = change
1831+
.output_state
1832+
.as_ref()
1833+
.and_then(|obj| obj.as_move_object.as_ref())
1834+
.and_then(|move_obj| move_obj.contents.as_ref())
1835+
.map(|contents| contents.type_.repr.clone())
1836+
.or_else(|| {
1837+
change
1838+
.input_state
1839+
.as_ref()
1840+
.and_then(|obj| obj.as_move_object.as_ref())
1841+
.and_then(|move_obj| move_obj.contents.as_ref())
1842+
.map(|contents| contents.type_.repr.clone())
1843+
});
1844+
1845+
// Convert Address to ObjectId
1846+
object_type.map(|typ| (ObjectId::from(change.address), typ))
1847+
})
1848+
.collect();
1849+
1850+
// Populate the object_type field for each changed object
1851+
for changed_obj in &mut effects_v1.changed_objects {
1852+
if let Some(object_type) = type_map.get(&changed_obj.object_id) {
1853+
changed_obj.object_type = Some(object_type.clone());
1854+
}
1855+
}
1856+
}
1857+
}
1858+
}
1859+
18041860
// This function is used in tests to create a new client instance for the local
18051861
// server.
18061862
#[cfg(test)]

crates/iota-graphql-client/src/query_types/execute_tx.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Modifications Copyright (c) 2025 IOTA Stiftung
33
// SPDX-License-Identifier: Apache-2.0
44

5-
use crate::query_types::{Base64, schema};
5+
use crate::query_types::{Base64, PageInfo, schema};
66

77
#[derive(cynic::QueryFragment, Debug)]
88
#[cynic(
@@ -32,4 +32,48 @@ pub struct ExecutionResult {
3232
#[cynic(schema = "rpc", graphql_type = "TransactionBlockEffects")]
3333
pub struct TransactionBlockEffects {
3434
pub bcs: Base64,
35+
pub object_changes: ObjectChangeConnection,
36+
}
37+
38+
#[derive(cynic::QueryFragment, Debug)]
39+
#[cynic(schema = "rpc", graphql_type = "ObjectChangeConnection")]
40+
pub struct ObjectChangeConnection {
41+
pub page_info: PageInfo,
42+
pub nodes: Vec<ObjectChange>,
43+
}
44+
45+
#[derive(cynic::QueryFragment, Debug)]
46+
#[cynic(schema = "rpc", graphql_type = "ObjectChange")]
47+
pub struct ObjectChange {
48+
pub address: crate::query_types::Address,
49+
pub input_state: Option<Object>,
50+
pub output_state: Option<Object>,
51+
pub id_created: Option<bool>,
52+
pub id_deleted: Option<bool>,
53+
}
54+
55+
#[derive(cynic::QueryFragment, Debug)]
56+
#[cynic(schema = "rpc", graphql_type = "Object")]
57+
pub struct Object {
58+
pub bcs: Option<Base64>,
59+
pub as_move_object: Option<MoveObject>,
60+
}
61+
62+
#[derive(cynic::QueryFragment, Debug)]
63+
#[cynic(schema = "rpc", graphql_type = "MoveObject")]
64+
pub struct MoveObject {
65+
pub contents: Option<MoveValue>,
66+
}
67+
68+
#[derive(cynic::QueryFragment, Debug)]
69+
#[cynic(schema = "rpc", graphql_type = "MoveValue")]
70+
pub struct MoveValue {
71+
#[cynic(rename = "type")]
72+
pub type_: MoveType,
73+
}
74+
75+
#[derive(cynic::QueryFragment, Debug)]
76+
#[cynic(schema = "rpc", graphql_type = "MoveType")]
77+
pub struct MoveType {
78+
pub repr: String,
3579
}

crates/iota-graphql-client/src/query_types/mod.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ pub use dynamic_fields::{
4242
};
4343
pub use epoch::{Epoch, EpochArgs, EpochQuery, EpochSummaryQuery, ValidatorSet};
4444
pub use events::{Event, EventConnection, EventFilter, EventsQuery, EventsQueryArgs};
45-
pub use execute_tx::{ExecuteTransactionArgs, ExecuteTransactionQuery, ExecutionResult};
45+
pub use execute_tx::{
46+
ExecuteTransactionArgs, ExecuteTransactionQuery, ExecutionResult, ObjectChange,
47+
ObjectChangeConnection,
48+
};
4649
pub use iota_names::{
4750
IotaNamesAddressDefaultNameQuery, IotaNamesAddressRegistrationsQuery, IotaNamesDefaultNameArgs,
4851
IotaNamesDefaultNameQuery, IotaNamesRegistrationsArgs, IotaNamesRegistrationsQuery,
@@ -72,10 +75,11 @@ pub use protocol_config::{
7275
use serde_json::Value as JsonValue;
7376
pub use service_config::{Feature, ServiceConfig, ServiceConfigQuery};
7477
pub use transaction::{
75-
TransactionBlock, TransactionBlockArgs, TransactionBlockEffectsQuery,
76-
TransactionBlockKindInput, TransactionBlockQuery, TransactionBlockWithEffects,
77-
TransactionBlockWithEffectsQuery, TransactionBlocksEffectsQuery, TransactionBlocksQuery,
78-
TransactionBlocksQueryArgs, TransactionBlocksWithEffectsQuery, TransactionsFilter,
78+
ObjectChange as TransactionObjectChange, TransactionBlock, TransactionBlockArgs,
79+
TransactionBlockEffectsQuery, TransactionBlockKindInput, TransactionBlockQuery,
80+
TransactionBlockWithEffects, TransactionBlockWithEffectsQuery, TransactionBlocksEffectsQuery,
81+
TransactionBlocksQuery, TransactionBlocksQueryArgs, TransactionBlocksWithEffectsQuery,
82+
TransactionsFilter,
7983
};
8084

8185
use crate::error;

crates/iota-graphql-client/src/query_types/transaction.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,50 @@ pub struct TxBlockEffects {
130130
#[cynic(schema = "rpc", graphql_type = "TransactionBlockEffects")]
131131
pub struct TransactionBlockEffects {
132132
pub bcs: Option<Base64>,
133+
pub object_changes: ObjectChangeConnection,
134+
}
135+
136+
#[derive(cynic::QueryFragment, Debug)]
137+
#[cynic(schema = "rpc", graphql_type = "ObjectChangeConnection")]
138+
pub struct ObjectChangeConnection {
139+
pub page_info: PageInfo,
140+
pub nodes: Vec<ObjectChange>,
141+
}
142+
143+
#[derive(cynic::QueryFragment, Debug)]
144+
#[cynic(schema = "rpc", graphql_type = "ObjectChange")]
145+
pub struct ObjectChange {
146+
pub address: Address,
147+
pub input_state: Option<Object>,
148+
pub output_state: Option<Object>,
149+
pub id_created: Option<bool>,
150+
pub id_deleted: Option<bool>,
151+
}
152+
153+
#[derive(cynic::QueryFragment, Debug)]
154+
#[cynic(schema = "rpc", graphql_type = "Object")]
155+
pub struct Object {
156+
pub bcs: Option<Base64>,
157+
pub as_move_object: Option<MoveObject>,
158+
}
159+
160+
#[derive(cynic::QueryFragment, Debug)]
161+
#[cynic(schema = "rpc", graphql_type = "MoveObject")]
162+
pub struct MoveObject {
163+
pub contents: Option<MoveValue>,
164+
}
165+
166+
#[derive(cynic::QueryFragment, Debug)]
167+
#[cynic(schema = "rpc", graphql_type = "MoveValue")]
168+
pub struct MoveValue {
169+
#[cynic(rename = "type")]
170+
pub type_: MoveType,
171+
}
172+
173+
#[derive(cynic::QueryFragment, Debug)]
174+
#[cynic(schema = "rpc", graphql_type = "MoveType")]
175+
pub struct MoveType {
176+
pub repr: String,
133177
}
134178

135179
#[derive(cynic::Enum, Clone, Copy, Debug)]

crates/iota-sdk-ffi/src/types/transaction/v1.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ pub struct ChangedObject {
139139
/// This information isn't required by the protocol but is useful for
140140
/// providing more detailed semantics on object changes.
141141
pub id_operation: IdOperation,
142+
/// Optional object type information. This is not part of the BCS protocol
143+
/// data but can be populated from other sources when available.
144+
#[uniffi(default = None)]
145+
pub object_type: Option<String>,
142146
}
143147

144148
impl From<iota_types::ChangedObject> for ChangedObject {
@@ -148,6 +152,7 @@ impl From<iota_types::ChangedObject> for ChangedObject {
148152
input_state: value.input_state.into(),
149153
output_state: value.output_state.into(),
150154
id_operation: value.id_operation,
155+
object_type: value.object_type,
151156
}
152157
}
153158
}
@@ -159,6 +164,7 @@ impl From<ChangedObject> for iota_types::ChangedObject {
159164
input_state: value.input_state.into(),
160165
output_state: value.output_state.into(),
161166
id_operation: value.id_operation,
167+
object_type: value.object_type,
162168
}
163169
}
164170
}

crates/iota-sdk-types/src/effects/v1.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ pub struct ChangedObject {
115115
/// This information isn't required by the protocol but is useful for
116116
/// providing more detailed semantics on object changes.
117117
pub id_operation: IdOperation,
118+
/// Optional object type information. This is not part of the BCS protocol
119+
/// data but can be populated from other sources when available.
120+
#[cfg_attr(feature = "serde", serde(skip))]
121+
pub object_type: Option<String>,
118122
}
119123

120124
/// A shared object that wasn't changed during execution

0 commit comments

Comments
 (0)