Skip to content

Commit 9d79779

Browse files
committed
[graphql-alt] Load balanceChanges from simulate and execute transaction
1 parent 14245dd commit 9d79779

File tree

4 files changed

+161
-44
lines changed

4 files changed

+161
-44
lines changed

crates/sui-indexer-alt-e2e-tests/tests/graphql_simulate_transaction_tests.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,3 +995,84 @@ async fn test_package_resolver_finds_newly_published_package() {
995995

996996
graphql_cluster.stopped().await;
997997
}
998+
999+
#[tokio::test]
1000+
async fn test_simulate_transaction_balance_changes() {
1001+
let validator_cluster = TestClusterBuilder::new().build().await;
1002+
let graphql_cluster = GraphQlTestCluster::new(&validator_cluster).await;
1003+
1004+
// Create a transfer transaction that will cause balance changes
1005+
let recipient = SuiAddress::random_for_testing_only();
1006+
let transfer_amount = 1_000_000u64;
1007+
1008+
let signed_tx = make_transfer_sui_transaction(
1009+
&validator_cluster.wallet,
1010+
Some(recipient),
1011+
Some(transfer_amount),
1012+
)
1013+
.await;
1014+
let (tx_bytes, _signatures) = signed_tx.to_tx_bytes_and_signatures();
1015+
1016+
let result = graphql_cluster
1017+
.execute_graphql(
1018+
r#"
1019+
query($txData: JSON!) {
1020+
simulateTransaction(transaction: $txData) {
1021+
effects {
1022+
status
1023+
balanceChanges {
1024+
nodes {
1025+
coinType {
1026+
repr
1027+
}
1028+
amount
1029+
}
1030+
}
1031+
}
1032+
error
1033+
}
1034+
}
1035+
"#,
1036+
json!({
1037+
"txData": {
1038+
"bcs": {
1039+
"value": tx_bytes.encoded()
1040+
}
1041+
}
1042+
}),
1043+
)
1044+
.await
1045+
.expect("GraphQL request failed");
1046+
1047+
// Verify balance changes are populated from execution context
1048+
let balance_changes = result
1049+
.pointer("/data/simulateTransaction/effects/balanceChanges/nodes")
1050+
.expect("balanceChanges should be present")
1051+
.as_array()
1052+
.unwrap();
1053+
1054+
// Should have balance changes for both sender and recipient
1055+
assert_eq!(balance_changes.len(), 2, "Should have 2 balance changes");
1056+
1057+
// Verify structure matches expected format
1058+
insta::assert_json_snapshot!(result.pointer("/data/simulateTransaction/effects/balanceChanges"), @r#"
1059+
{
1060+
"nodes": [
1061+
{
1062+
"coinType": {
1063+
"repr": "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"
1064+
},
1065+
"amount": "-3976000"
1066+
},
1067+
{
1068+
"coinType": {
1069+
"repr": "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"
1070+
},
1071+
"amount": "1000000"
1072+
}
1073+
]
1074+
}
1075+
"#);
1076+
1077+
graphql_cluster.stopped().await;
1078+
}

crates/sui-indexer-alt-graphql/src/api/types/balance_change.rs

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,65 @@
44
use std::str::FromStr;
55

66
use anyhow::Context as _;
7-
use async_graphql::Object;
7+
use async_graphql::SimpleObject;
88
use sui_indexer_alt_schema::transactions::BalanceChange as StoredBalanceChange;
9-
use sui_types::{TypeTag, object::Owner as NativeOwner};
9+
use sui_rpc::proto::sui::rpc::v2::BalanceChange as GrpcBalanceChange;
10+
use sui_types::{TypeTag, object::Owner};
1011

1112
use crate::{api::scalars::big_int::BigInt, error::RpcError, scope::Scope};
1213

1314
use super::{address::Address, move_type::MoveType};
1415

15-
#[derive(Clone)]
16+
/// Effects to the balance (sum of coin values per coin type) of addresses and objects.
17+
#[derive(Clone, SimpleObject)]
1618
pub(crate) struct BalanceChange {
17-
pub(crate) scope: Scope,
18-
pub(crate) stored: StoredBalanceChange,
19+
/// The address or object whose balance has changed.
20+
pub(crate) owner: Option<Address>,
21+
/// The inner type of the coin whose balance has changed (e.g. `0x2::sui::SUI`).
22+
pub(crate) coin_type: Option<MoveType>,
23+
/// The signed balance change.
24+
pub(crate) amount: Option<BigInt>,
1925
}
2026

21-
/// Effects to the balance (sum of coin values per coin type) of addresses and objects.
22-
#[Object]
2327
impl BalanceChange {
24-
/// The address or object whose balance has changed.
25-
async fn owner(&self) -> Option<Address> {
26-
use NativeOwner as O;
27-
let StoredBalanceChange::V1 { owner, .. } = &self.stored;
28-
29-
match owner {
30-
O::AddressOwner(addr)
31-
| O::ObjectOwner(addr)
32-
| O::ConsensusAddressOwner { owner: addr, .. } => {
33-
Some(Address::with_address(self.scope.clone(), *addr))
34-
}
35-
O::Shared { .. } | O::Immutable => None,
36-
}
37-
}
28+
/// Create a BalanceChange from a gRPC BalanceChange.
29+
pub(crate) fn from_grpc(scope: Scope, grpc: &GrpcBalanceChange) -> Result<Self, RpcError> {
30+
let address = grpc.address().parse().context("Failed to parse address")?;
31+
let coin_type: TypeTag = grpc
32+
.coin_type()
33+
.parse()
34+
.context("Failed to parse coin type")?;
35+
let amount: i128 = grpc.amount().parse().context("Failed to parse amount")?;
3836

39-
/// The inner type of the coin whose balance has changed (e.g. `0x2::sui::SUI`).
40-
async fn coin_type(&self) -> Result<Option<MoveType>, RpcError> {
41-
let StoredBalanceChange::V1 { coin_type, .. } = &self.stored;
42-
let type_ = TypeTag::from_str(coin_type).context("Failed to parse coin type")?;
43-
Ok(Some(MoveType::from_native(type_, self.scope.clone())))
37+
Ok(Self {
38+
owner: Some(Address::with_address(scope.clone(), address)),
39+
coin_type: Some(MoveType::from_native(coin_type, scope)),
40+
amount: Some(BigInt::from(amount)),
41+
})
4442
}
4543

46-
/// The signed balance change.
47-
async fn amount(&self) -> Option<BigInt> {
48-
let StoredBalanceChange::V1 { amount, .. } = &self.stored;
49-
Some(BigInt::from(*amount))
44+
/// Create a BalanceChange from a stored BalanceChange (database).
45+
pub(crate) fn from_stored(scope: Scope, stored: StoredBalanceChange) -> Result<Self, RpcError> {
46+
let StoredBalanceChange::V1 {
47+
owner,
48+
coin_type,
49+
amount,
50+
} = stored;
51+
52+
// Extract address from owner
53+
let address = match owner {
54+
Owner::AddressOwner(addr)
55+
| Owner::ObjectOwner(addr)
56+
| Owner::ConsensusAddressOwner { owner: addr, .. } => Some(addr),
57+
Owner::Shared { .. } | Owner::Immutable => None,
58+
};
59+
60+
let coin_type = TypeTag::from_str(&coin_type).context("Failed to parse coin type")?;
61+
62+
Ok(Self {
63+
owner: address.map(|addr| Address::with_address(scope.clone(), addr)),
64+
coin_type: Some(MoveType::from_native(coin_type, scope)),
65+
amount: Some(BigInt::from(amount)),
66+
})
5067
}
5168
}

crates/sui-indexer-alt-graphql/src/api/types/transaction_effects.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use sui_indexer_alt_reader::{
1111
pg_reader::PgReader,
1212
tx_balance_changes::TxBalanceChangeKey,
1313
};
14-
use sui_indexer_alt_schema::transactions::BalanceChange as NativeBalanceChange;
14+
use sui_indexer_alt_schema::transactions::BalanceChange as StoredBalanceChange;
1515
use sui_rpc::proto::sui::rpc::v2::ExecutedTransaction;
1616
use sui_types::{
1717
digests::TransactionDigest,
@@ -217,9 +217,21 @@ impl EffectsContents {
217217
return Ok(Some(Connection::new(false, false)));
218218
};
219219

220-
let transaction_digest = content.digest()?;
220+
let pagination: &PaginationConfig = ctx.data()?;
221+
let limits = pagination.limits("TransactionEffects", "balanceChanges");
222+
let page = Page::from_params(limits, first, after, last, before)?;
221223

222-
// Load balance changes from database using DataLoader
224+
// First try to get balance changes from execution context
225+
if let Some(grpc_balance_changes) = content.balance_changes() {
226+
return page
227+
.paginate_indices(grpc_balance_changes.len(), |i| {
228+
BalanceChange::from_grpc(self.scope.clone(), &grpc_balance_changes[i])
229+
})
230+
.map(Some);
231+
}
232+
233+
// Fall back to loading from database
234+
let transaction_digest = content.digest()?;
223235
let pg_loader: &Arc<DataLoader<PgReader>> = ctx.data()?;
224236
let key = TxBalanceChangeKey(transaction_digest);
225237

@@ -232,19 +244,12 @@ impl EffectsContents {
232244
};
233245

234246
// Deserialize balance changes from BCS bytes
235-
let balance_changes: Vec<NativeBalanceChange> =
247+
let balance_changes: Vec<StoredBalanceChange> =
236248
bcs::from_bytes(&stored_balance_changes.balance_changes)
237249
.context("Failed to deserialize balance changes")?;
238250

239-
let pagination: &PaginationConfig = ctx.data()?;
240-
let limits = pagination.limits("TransactionEffects", "balanceChanges");
241-
let page = Page::from_params(limits, first, after, last, before)?;
242-
243251
page.paginate_indices(balance_changes.len(), |i| {
244-
Ok(BalanceChange {
245-
scope: self.scope.clone(),
246-
stored: balance_changes[i].clone(),
247-
})
252+
BalanceChange::from_stored(self.scope.clone(), balance_changes[i].clone())
248253
})
249254
.map(Some)
250255
}

crates/sui-indexer-alt-reader/src/kv_loader.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use sui_indexer_alt_schema::transactions::StoredTransaction;
99
use sui_kvstore::{
1010
TransactionData as KVTransactionData, TransactionEventsData as KVTransactionEventsData,
1111
};
12-
use sui_rpc::proto::sui::rpc::v2::ExecutedTransaction;
12+
use sui_rpc::proto::sui::rpc::v2 as grpc;
1313
use sui_types::{
1414
base_types::ObjectID,
1515
crypto::AuthorityQuorumSignInfo,
@@ -54,6 +54,7 @@ pub enum TransactionContents {
5454
events: Option<Vec<Event>>,
5555
transaction_data: Box<TransactionData>,
5656
signatures: Vec<GenericSignature>,
57+
balance_changes: Vec<grpc::BalanceChange>,
5758
},
5859
}
5960

@@ -218,7 +219,7 @@ impl KvLoader {
218219
impl TransactionContents {
219220
/// Create a TransactionContents from an ExecutedTransaction.
220221
pub fn from_executed_transaction(
221-
executed_transaction: &ExecutedTransaction,
222+
executed_transaction: &grpc::ExecutedTransaction,
222223
transaction_data: TransactionData,
223224
signatures: Vec<GenericSignature>,
224225
) -> anyhow::Result<Self> {
@@ -240,11 +241,15 @@ impl TransactionContents {
240241
.transpose()?
241242
.map(|events: TransactionEvents| events.data);
242243

244+
// Extract balance changes from the gRPC response
245+
let balance_changes = executed_transaction.balance_changes.clone();
246+
243247
Ok(Self::ExecutedTransaction {
244248
effects: Box::new(effects),
245249
events,
246250
transaction_data: Box::new(transaction_data),
247251
signatures,
252+
balance_changes,
248253
})
249254
}
250255

@@ -311,6 +316,15 @@ impl TransactionContents {
311316
}
312317
}
313318

319+
pub fn balance_changes(&self) -> Option<Vec<grpc::BalanceChange>> {
320+
match self {
321+
Self::ExecutedTransaction {
322+
balance_changes, ..
323+
} => Some(balance_changes.clone()),
324+
_ => None,
325+
}
326+
}
327+
314328
pub fn raw_transaction(&self) -> anyhow::Result<Vec<u8>> {
315329
match self {
316330
Self::Pg(stored) => Ok(stored.raw_transaction.clone()),

0 commit comments

Comments
 (0)