Skip to content

Commit 1da8677

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 e81f8a4 commit 1da8677

File tree

12 files changed

+433
-19
lines changed

12 files changed

+433
-19
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright (c) 2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package main
5+
6+
import (
7+
"log"
8+
"os"
9+
10+
sdk "bindings/iota_sdk_ffi"
11+
)
12+
13+
// === ExecuteTx with ObjectChange Example ===
14+
// This example demonstrates the ObjectChange feature using ExecuteTx,
15+
// which executes a transaction on-chain and returns the actual transaction effects.
16+
//
17+
// SETUP INSTRUCTIONS:
18+
// 1. Set environment variable PRIVATE_KEY_HEX with your private key (64 hex chars)
19+
// 2. Set SENDER_ADDRESS with the address that owns the coins
20+
// 3. Set COIN_OBJECT_ID and GAS_COIN_OBJECT_ID with valid object IDs owned by the sender
21+
//
22+
// Example:
23+
//
24+
// export PRIVATE_KEY_HEX="your_private_key_in_hex"
25+
// export SENDER_ADDRESS="0x611830d3641a68f94a690dcc25d1f4b0dac948325ac18f6dd32564371735f32c"
26+
// export COIN_OBJECT_ID="0xd04077fe3b6fad13b3d4ed0d535b7ca92afcac8f0f2a0e0925fb9f4f0b30c699"
27+
// export GAS_COIN_OBJECT_ID="0x0b0270ee9d27da0db09651e5f7338dfa32c7ee6441ccefa1f6e305735bcfc7ab"
28+
func main() {
29+
30+
// Initialize client
31+
client := sdk.GraphQlClientNewTestnet()
32+
33+
// Check for required environment variables
34+
privateKeyHex := "0032e67709569b6a1ef551ea7a745aac943b2158fc862ae7c6d08dd1744e15d818"
35+
senderAddressHex := "0x786dff8a4ee13d45b502c8f22f398e3517e6ec78aa4ae564c348acb07fad7f50"
36+
coinObjectIdHex := "0x7714345721694a20c6e2e04a6a289b1c08710cda863f7dd49b4661cde524103d"
37+
gasObjectIdHex := "0xd7913b6c9c1e67c281ad13bffcb4d172ffb8f5a85581e5d5984eacb310365be8"
38+
39+
if privateKeyHex == "" || senderAddressHex == "" || coinObjectIdHex == "" || gasObjectIdHex == "" {
40+
log.Println("ERROR: Missing required environment variables.")
41+
log.Println("This example requires actual funded accounts to execute transactions.")
42+
log.Println("\nPlease set the following environment variables:")
43+
log.Println(" PRIVATE_KEY_HEX - Your Ed25519 private key in hex format")
44+
log.Println(" SENDER_ADDRESS - The address that owns the coins")
45+
log.Println(" COIN_OBJECT_ID - Object ID of a coin to transfer")
46+
log.Println(" GAS_COIN_OBJECT_ID - Object ID of a coin to use for gas")
47+
log.Println("\nAlternatively, you can hardcode these values in the source for testing.")
48+
os.Exit(1)
49+
}
50+
51+
// Load keypair from hex using SimpleKeypair
52+
privateKeyBytes, err := sdk.HexDecode(privateKeyHex)
53+
if err != nil {
54+
log.Fatalf("Failed to decode private key: %v", err)
55+
}
56+
57+
simpleKeypair, err := sdk.SimpleKeypairFromBytes(privateKeyBytes)
58+
if err != nil {
59+
log.Fatalf("Failed to load private key: %v", err)
60+
}
61+
62+
fromAddress, _ := sdk.AddressFromHex(senderAddressHex)
63+
toAddress, _ := sdk.AddressFromHex(senderAddressHex)
64+
coinObjId, _ := sdk.ObjectIdFromHex(coinObjectIdHex)
65+
gasCoinObjId, _ := sdk.ObjectIdFromHex(gasObjectIdHex)
66+
67+
log.Printf("Building transaction from %s to %s\n", fromAddress.ToHex(), toAddress.ToHex())
68+
69+
// Build the transaction
70+
builder := sdk.TransactionBuilderInit(fromAddress, client)
71+
builder.TransferObjects(toAddress, []*sdk.PtbArgument{sdk.PtbArgumentObjectId(coinObjId)})
72+
builder.Gas(gasCoinObjId).GasBudget(1000000000)
73+
74+
// Finish building to get the transaction
75+
txn, err := builder.Finish()
76+
if err.(*sdk.SdkFfiError) != nil {
77+
log.Fatalf("Failed to build transaction: %v", err)
78+
}
79+
80+
log.Printf("Transaction built, signing with keypair...\n")
81+
82+
// Sign the transaction using SimpleKeypair
83+
simpleSignature, err := simpleKeypair.TrySign(txn.SigningDigest())
84+
if err != nil {
85+
log.Fatalf("Failed to sign transaction: %v", err)
86+
}
87+
88+
// Convert SimpleSignature to UserSignature
89+
userSignature := sdk.UserSignatureNewSimple(simpleSignature)
90+
91+
log.Printf("Transaction signed, executing on-chain...\n")
92+
93+
// Execute the transaction on-chain
94+
effects, err := client.ExecuteTx([]*sdk.UserSignature{userSignature}, txn)
95+
if err.(*sdk.SdkFfiError) != nil {
96+
log.Fatalf("ExecuteTx failed: %v", err)
97+
}
98+
99+
if effects == nil {
100+
log.Println("No transaction effects returned")
101+
return
102+
}
103+
104+
log.Printf("Transaction executed successfully!\n")
105+
106+
// Access and print transaction effects
107+
PrintObjectChanges(*effects)
108+
}
109+
110+
func PrintObjectChanges(effects *sdk.TransactionEffects) {
111+
log.Println("=== Object Changes (from ExecuteTx) ===")
112+
113+
if !effects.IsV1() {
114+
log.Println("Effects version is not V1")
115+
return
116+
}
117+
118+
effectsV1 := effects.AsV1()
119+
log.Printf("Total changed objects: %d\n", len(effectsV1.ChangedObjects))
120+
121+
for i, change := range effectsV1.ChangedObjects {
122+
log.Printf("Object #%d:\n", i+1)
123+
log.Printf(" Object ID: %s\n", change.ObjectId.ToHex())
124+
125+
// Check creation/deletion status using IdOperation
126+
switch change.IdOperation {
127+
case sdk.IdOperationCreated:
128+
log.Println(" Status: CREATED")
129+
case sdk.IdOperationDeleted:
130+
log.Println(" Status: DELETED")
131+
case sdk.IdOperationNone:
132+
log.Println(" Status: MODIFIED")
133+
}
134+
135+
// Object type (if available)
136+
if change.ObjectType != nil {
137+
log.Printf(" Type: %s\n", *change.ObjectType)
138+
} else {
139+
log.Printf(" Type: %v\n", change.ObjectType)
140+
}
141+
142+
// Input state (state before transaction)
143+
switch input := change.InputState.(type) {
144+
case sdk.ObjectInMissing:
145+
log.Println(" Input State: Missing (new object)")
146+
case sdk.ObjectInData:
147+
log.Printf(" Input State: Version=%d, Owner=%s\n", input.Version, input.Owner.String())
148+
}
149+
150+
// Output state (state after transaction)
151+
switch output := change.OutputState.(type) {
152+
case sdk.ObjectOutMissing:
153+
log.Println(" Output State: Missing (deleted)")
154+
case sdk.ObjectOutObjectWrite:
155+
log.Printf(" Output State: ObjectWrite, Owner=%s\n", output.Owner.String())
156+
case sdk.ObjectOutPackageWrite:
157+
log.Printf(" Output State: PackageWrite, Version=%d\n", output.Version)
158+
}
159+
}
160+
}
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/examples/prepare_split_coins/main.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ func main() {
3030
sender,
3131
[]*sdk.PtbArgument{sdk.PtbArgumentRes("coin1"), sdk.PtbArgumentRes("coin2"), sdk.PtbArgumentRes("coin3")},
3232
)
33-
builder.Gas(coinObjId).GasBudget(1000000000)
3433

3534
txn, err := builder.Finish()
3635
if err.(*sdk.SdkFfiError) != nil {
@@ -44,14 +43,13 @@ func main() {
4443
log.Printf("Signing Digest: %v", sdk.HexEncode(txn.SigningDigest()))
4544
log.Printf("Txn Bytes: %v", sdk.Base64Encode(txnBytes))
4645

47-
res, err := builder.DryRun(false)
46+
skipChecks := bool(false)
47+
res, err := client.DryRunTx(txn, &skipChecks)
4848
if err.(*sdk.SdkFfiError) != nil {
49-
log.Fatalf("Failed to split coins: %v", err)
49+
log.Fatalf("Failed to dry run split coins: %v", err)
5050
}
51-
5251
if res.Error != nil {
5352
log.Fatalf("Failed to split coins: %v", *res.Error)
5453
}
55-
5654
log.Print("Split coins dry run was successful!")
5755
}

bindings/go/iota_sdk_ffi/iota_sdk_ffi.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25290,13 +25290,17 @@ type ChangedObject struct {
2529025290
// This information isn't required by the protocol but is useful for
2529125291
// providing more detailed semantics on object changes.
2529225292
IdOperation IdOperation
25293+
// Optional object type information. This is not part of the BCS protocol
25294+
// data but can be populated from other sources when available.
25295+
ObjectType *string
2529325296
}
2529425297

2529525298
func (r *ChangedObject) Destroy() {
2529625299
FfiDestroyerObjectId{}.Destroy(r.ObjectId);
2529725300
FfiDestroyerObjectIn{}.Destroy(r.InputState);
2529825301
FfiDestroyerObjectOut{}.Destroy(r.OutputState);
2529925302
FfiDestroyerIdOperation{}.Destroy(r.IdOperation);
25303+
FfiDestroyerOptionalString{}.Destroy(r.ObjectType);
2530025304
}
2530125305

2530225306
type FfiConverterChangedObject struct {}
@@ -25313,6 +25317,7 @@ func (c FfiConverterChangedObject) Read(reader io.Reader) ChangedObject {
2531325317
FfiConverterObjectInINSTANCE.Read(reader),
2531425318
FfiConverterObjectOutINSTANCE.Read(reader),
2531525319
FfiConverterIdOperationINSTANCE.Read(reader),
25320+
FfiConverterOptionalStringINSTANCE.Read(reader),
2531625321
}
2531725322
}
2531825323

@@ -25325,6 +25330,7 @@ func (c FfiConverterChangedObject) Write(writer io.Writer, value ChangedObject)
2532525330
FfiConverterObjectInINSTANCE.Write(writer, value.InputState);
2532625331
FfiConverterObjectOutINSTANCE.Write(writer, value.OutputState);
2532725332
FfiConverterIdOperationINSTANCE.Write(writer, value.IdOperation);
25333+
FfiConverterOptionalStringINSTANCE.Write(writer, value.ObjectType);
2532825334
}
2532925335

2533025336
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
@@ -45255,7 +45255,12 @@ data class ChangedObject (
4525545255
* This information isn't required by the protocol but is useful for
4525645256
* providing more detailed semantics on object changes.
4525745257
*/
45258-
var `idOperation`: IdOperation
45258+
var `idOperation`: IdOperation,
45259+
/**
45260+
* Optional object type information. This is not part of the BCS protocol
45261+
* data but can be populated from other sources when available.
45262+
*/
45263+
var `objectType`: kotlin.String? = null
4525945264
) : Disposable {
4526045265

4526145266
@Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here
@@ -45265,7 +45270,8 @@ data class ChangedObject (
4526545270
this.`objectId`,
4526645271
this.`inputState`,
4526745272
this.`outputState`,
45268-
this.`idOperation`
45273+
this.`idOperation`,
45274+
this.`objectType`
4526945275
)
4527045276
}
4527145277

@@ -45282,21 +45288,24 @@ public object FfiConverterTypeChangedObject: FfiConverterRustBuffer<ChangedObjec
4528245288
FfiConverterTypeObjectIn.read(buf),
4528345289
FfiConverterTypeObjectOut.read(buf),
4528445290
FfiConverterTypeIdOperation.read(buf),
45291+
FfiConverterOptionalString.read(buf),
4528545292
)
4528645293
}
4528745294

4528845295
override fun allocationSize(value: ChangedObject) = (
4528945296
FfiConverterTypeObjectId.allocationSize(value.`objectId`) +
4529045297
FfiConverterTypeObjectIn.allocationSize(value.`inputState`) +
4529145298
FfiConverterTypeObjectOut.allocationSize(value.`outputState`) +
45292-
FfiConverterTypeIdOperation.allocationSize(value.`idOperation`)
45299+
FfiConverterTypeIdOperation.allocationSize(value.`idOperation`) +
45300+
FfiConverterOptionalString.allocationSize(value.`objectType`)
4529345301
)
4529445302

4529545303
override fun write(value: ChangedObject, buf: ByteBuffer) {
4529645304
FfiConverterTypeObjectId.write(value.`objectId`, buf)
4529745305
FfiConverterTypeObjectIn.write(value.`inputState`, buf)
4529845306
FfiConverterTypeObjectOut.write(value.`outputState`, buf)
4529945307
FfiConverterTypeIdOperation.write(value.`idOperation`, buf)
45308+
FfiConverterOptionalString.write(value.`objectType`, buf)
4530045309
}
4530145310
}
4530245311

0 commit comments

Comments
 (0)