Skip to content

Commit 9ba6283

Browse files
committed
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.
1 parent 8cef843 commit 9ba6283

File tree

10 files changed

+302
-12
lines changed

10 files changed

+302
-12
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
@@ -24304,13 +24304,17 @@ type ChangedObject struct {
2430424304
// This information isn't required by the protocol but is useful for
2430524305
// providing more detailed semantics on object changes.
2430624306
IdOperation IdOperation
24307+
// Optional object type information. This is not part of the BCS protocol
24308+
// data but can be populated from other sources when available.
24309+
ObjectType *string
2430724310
}
2430824311

2430924312
func (r *ChangedObject) Destroy() {
2431024313
FfiDestroyerObjectId{}.Destroy(r.ObjectId);
2431124314
FfiDestroyerObjectIn{}.Destroy(r.InputState);
2431224315
FfiDestroyerObjectOut{}.Destroy(r.OutputState);
2431324316
FfiDestroyerIdOperation{}.Destroy(r.IdOperation);
24317+
FfiDestroyerOptionalString{}.Destroy(r.ObjectType);
2431424318
}
2431524319

2431624320
type FfiConverterChangedObject struct {}
@@ -24327,6 +24331,7 @@ func (c FfiConverterChangedObject) Read(reader io.Reader) ChangedObject {
2432724331
FfiConverterObjectInINSTANCE.Read(reader),
2432824332
FfiConverterObjectOutINSTANCE.Read(reader),
2432924333
FfiConverterIdOperationINSTANCE.Read(reader),
24334+
FfiConverterOptionalStringINSTANCE.Read(reader),
2433024335
}
2433124336
}
2433224337

@@ -24339,6 +24344,7 @@ func (c FfiConverterChangedObject) Write(writer io.Writer, value ChangedObject)
2433924344
FfiConverterObjectInINSTANCE.Write(writer, value.InputState);
2434024345
FfiConverterObjectOutINSTANCE.Write(writer, value.OutputState);
2434124346
FfiConverterIdOperationINSTANCE.Write(writer, value.IdOperation);
24347+
FfiConverterOptionalStringINSTANCE.Write(writer, value.ObjectType);
2434224348
}
2434324349

2434424350
type FfiDestroyerChangedObject struct {}

bindings/kotlin/lib/iota_sdk/iota_sdk_ffi.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44001,7 +44001,12 @@ data class ChangedObject (
4400144001
* This information isn't required by the protocol but is useful for
4400244002
* providing more detailed semantics on object changes.
4400344003
*/
44004-
var `idOperation`: IdOperation
44004+
var `idOperation`: IdOperation,
44005+
/**
44006+
* Optional object type information. This is not part of the BCS protocol
44007+
* data but can be populated from other sources when available.
44008+
*/
44009+
var `objectType`: kotlin.String? = null
4400544010
) : Disposable {
4400644011

4400744012
@Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here
@@ -44011,7 +44016,8 @@ data class ChangedObject (
4401144016
this.`objectId`,
4401244017
this.`inputState`,
4401344018
this.`outputState`,
44014-
this.`idOperation`
44019+
this.`idOperation`,
44020+
this.`objectType`
4401544021
)
4401644022
}
4401744023

@@ -44028,21 +44034,24 @@ public object FfiConverterTypeChangedObject: FfiConverterRustBuffer<ChangedObjec
4402844034
FfiConverterTypeObjectIn.read(buf),
4402944035
FfiConverterTypeObjectOut.read(buf),
4403044036
FfiConverterTypeIdOperation.read(buf),
44037+
FfiConverterOptionalString.read(buf),
4403144038
)
4403244039
}
4403344040

4403444041
override fun allocationSize(value: ChangedObject) = (
4403544042
FfiConverterTypeObjectId.allocationSize(value.`objectId`) +
4403644043
FfiConverterTypeObjectIn.allocationSize(value.`inputState`) +
4403744044
FfiConverterTypeObjectOut.allocationSize(value.`outputState`) +
44038-
FfiConverterTypeIdOperation.allocationSize(value.`idOperation`)
44045+
FfiConverterTypeIdOperation.allocationSize(value.`idOperation`) +
44046+
FfiConverterOptionalString.allocationSize(value.`objectType`)
4403944047
)
4404044048

4404144049
override fun write(value: ChangedObject, buf: ByteBuffer) {
4404244050
FfiConverterTypeObjectId.write(value.`objectId`, buf)
4404344051
FfiConverterTypeObjectIn.write(value.`inputState`, buf)
4404444052
FfiConverterTypeObjectOut.write(value.`outputState`, buf)
4404544053
FfiConverterTypeIdOperation.write(value.`idOperation`, buf)
44054+
FfiConverterOptionalString.write(value.`objectType`, buf)
4404644055
}
4404744056
}
4404844057

bindings/python/lib/iota_sdk_ffi.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9524,14 +9524,24 @@ class ChangedObject:
95249524
providing more detailed semantics on object changes.
95259525
"""
95269526

9527-
def __init__(self, *, object_id: "ObjectId", input_state: "ObjectIn", output_state: "ObjectOut", id_operation: "IdOperation"):
9527+
object_type: "typing.Optional[str]"
9528+
"""
9529+
Optional object type information. This is not part of the BCS protocol
9530+
data but can be populated from other sources when available.
9531+
"""
9532+
9533+
def __init__(self, *, object_id: "ObjectId", input_state: "ObjectIn", output_state: "ObjectOut", id_operation: "IdOperation", object_type: "typing.Optional[str]" = _DEFAULT):
95289534
self.object_id = object_id
95299535
self.input_state = input_state
95309536
self.output_state = output_state
95319537
self.id_operation = id_operation
9538+
if object_type is _DEFAULT:
9539+
self.object_type = None
9540+
else:
9541+
self.object_type = object_type
95329542

95339543
def __str__(self):
9534-
return "ChangedObject(object_id={}, input_state={}, output_state={}, id_operation={})".format(self.object_id, self.input_state, self.output_state, self.id_operation)
9544+
return "ChangedObject(object_id={}, input_state={}, output_state={}, id_operation={}, object_type={})".format(self.object_id, self.input_state, self.output_state, self.id_operation, self.object_type)
95359545

95369546
def __eq__(self, other):
95379547
if self.object_id != other.object_id:
@@ -9542,6 +9552,8 @@ def __eq__(self, other):
95429552
return False
95439553
if self.id_operation != other.id_operation:
95449554
return False
9555+
if self.object_type != other.object_type:
9556+
return False
95459557
return True
95469558

95479559
class _UniffiConverterTypeChangedObject(_UniffiConverterRustBuffer):
@@ -9552,6 +9564,7 @@ def read(buf):
95529564
input_state=_UniffiConverterTypeObjectIn.read(buf),
95539565
output_state=_UniffiConverterTypeObjectOut.read(buf),
95549566
id_operation=_UniffiConverterTypeIdOperation.read(buf),
9567+
object_type=_UniffiConverterOptionalString.read(buf),
95559568
)
95569569

95579570
@staticmethod
@@ -9560,13 +9573,15 @@ def check_lower(value):
95609573
_UniffiConverterTypeObjectIn.check_lower(value.input_state)
95619574
_UniffiConverterTypeObjectOut.check_lower(value.output_state)
95629575
_UniffiConverterTypeIdOperation.check_lower(value.id_operation)
9576+
_UniffiConverterOptionalString.check_lower(value.object_type)
95639577

95649578
@staticmethod
95659579
def write(value, buf):
95669580
_UniffiConverterTypeObjectId.write(value.object_id, buf)
95679581
_UniffiConverterTypeObjectIn.write(value.input_state, buf)
95689582
_UniffiConverterTypeObjectOut.write(value.output_state, buf)
95699583
_UniffiConverterTypeIdOperation.write(value.id_operation, buf)
9584+
_UniffiConverterOptionalString.write(value.object_type, buf)
95709585

95719586

95729587
class CheckpointSummaryPage:

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
}

0 commit comments

Comments
 (0)