sui_indexer_alt_jsonrpc/api/transactions/
response.rs1use std::str::FromStr;
5
6use anyhow::Context as _;
7use futures::future::OptionFuture;
8use move_core_types::annotated_value::MoveDatatypeLayout;
9use move_core_types::annotated_value::MoveTypeLayout;
10use sui_indexer_alt_reader::kv_loader::TransactionContents;
11use sui_indexer_alt_reader::objects::VersionedObjectKey;
12use sui_indexer_alt_reader::tx_balance_changes::TxBalanceChangeKey;
13use sui_indexer_alt_schema::transactions::BalanceChange;
14use sui_indexer_alt_schema::transactions::StoredTxBalanceChange;
15use sui_json_rpc_types::BalanceChange as SuiBalanceChange;
16use sui_json_rpc_types::ObjectChange as SuiObjectChange;
17use sui_json_rpc_types::SuiEvent;
18use sui_json_rpc_types::SuiTransactionBlock;
19use sui_json_rpc_types::SuiTransactionBlockData;
20use sui_json_rpc_types::SuiTransactionBlockEffects;
21use sui_json_rpc_types::SuiTransactionBlockEvents;
22use sui_json_rpc_types::SuiTransactionBlockResponse;
23use sui_json_rpc_types::SuiTransactionBlockResponseOptions;
24use sui_types::TypeTag;
25use sui_types::base_types::ObjectID;
26use sui_types::base_types::SequenceNumber;
27use sui_types::digests::ObjectDigest;
28use sui_types::digests::TransactionDigest;
29use sui_types::effects::IDOperation;
30use sui_types::effects::ObjectChange;
31use sui_types::effects::TransactionEffects;
32use sui_types::effects::TransactionEffectsAPI;
33use sui_types::event::Event;
34use sui_types::object::Object;
35use sui_types::signature::GenericSignature;
36use sui_types::transaction::TransactionData;
37use sui_types::transaction::TransactionDataAPI;
38use tokio::join;
39
40use crate::api::transactions::error::Error;
41use crate::context::Context;
42use crate::error::RpcError;
43use crate::error::invalid_params;
44use crate::error::rpc_bail;
45
46pub(super) async fn transaction(
49 ctx: &Context,
50 digest: TransactionDigest,
51 options: &SuiTransactionBlockResponseOptions,
52) -> Result<SuiTransactionBlockResponse, RpcError<Error>> {
53 let tx = ctx.kv_loader().load_one_transaction(digest);
54 let stored_bc: OptionFuture<_> = options
55 .show_balance_changes
56 .then(|| ctx.pg_loader().load_one(TxBalanceChangeKey(digest)))
57 .into();
58
59 let (tx, stored_bc) = join!(tx, stored_bc);
60
61 let tx = tx
62 .context("Failed to fetch transaction from store")?
63 .ok_or_else(|| invalid_params(Error::NotFound(digest)))?;
64
65 let stored_bc = match stored_bc
68 .transpose()
69 .context("Failed to fetch balance changes from store")?
70 {
71 Some(None) => return Err(invalid_params(Error::BalanceChangesNotFound(digest))),
72 Some(changes) => changes,
73 None => None,
74 };
75
76 let digest = tx.digest()?;
77
78 let mut response = SuiTransactionBlockResponse::new(digest);
79
80 response.timestamp_ms = tx.timestamp_ms();
81 response.checkpoint = tx.cp_sequence_number();
82
83 if options.show_input {
84 response.transaction = Some(input(ctx, &tx).await?);
85 }
86
87 if options.show_raw_input {
88 response.raw_transaction = tx.raw_transaction()?;
89 }
90
91 if options.show_effects {
92 response.effects = Some(effects(&tx)?);
93 }
94
95 if options.show_raw_effects {
96 response.raw_effects = tx.raw_effects()?;
97 }
98
99 if options.show_events {
100 response.events = Some(events(ctx, digest, &tx).await?);
101 }
102
103 if let Some(changes) = stored_bc {
104 response.balance_changes = Some(balance_changes(changes)?);
105 }
106
107 if options.show_object_changes {
108 response.object_changes = Some(object_changes(ctx, digest, &tx).await?);
109 }
110
111 Ok(response)
112}
113
114async fn input(
116 ctx: &Context,
117 tx: &TransactionContents,
118) -> Result<SuiTransactionBlock, RpcError<Error>> {
119 let data: TransactionData = tx.data()?;
120 let tx_signatures: Vec<GenericSignature> = tx.signatures()?;
121
122 Ok(SuiTransactionBlock {
123 data: SuiTransactionBlockData::try_from_with_package_resolver(data, ctx.package_resolver())
124 .await
125 .context("Failed to resolve types in transaction data")?,
126 tx_signatures,
127 })
128}
129
130fn effects(tx: &TransactionContents) -> Result<SuiTransactionBlockEffects, RpcError<Error>> {
132 let effects: TransactionEffects = tx.effects()?;
133 Ok(effects
134 .try_into()
135 .context("Failed to convert Effects into response")?)
136}
137
138async fn events(
140 ctx: &Context,
141 digest: TransactionDigest,
142 tx: &TransactionContents,
143) -> Result<SuiTransactionBlockEvents, RpcError<Error>> {
144 let events: Vec<Event> = tx.events()?;
145 let mut sui_events = Vec::with_capacity(events.len());
146
147 for (ix, event) in events.into_iter().enumerate() {
148 let layout = match ctx
149 .package_resolver()
150 .type_layout(event.type_.clone().into())
151 .await
152 .with_context(|| {
153 format!(
154 "Failed to resolve layout for {}",
155 event.type_.to_canonical_display(true)
156 )
157 })? {
158 MoveTypeLayout::Struct(s) => MoveDatatypeLayout::Struct(s),
159 MoveTypeLayout::Enum(e) => MoveDatatypeLayout::Enum(e),
160 _ => rpc_bail!(
161 "Event {ix} is not a struct or enum: {}",
162 event.type_.to_canonical_string(true)
163 ),
164 };
165
166 let sui_event = SuiEvent::try_from(event, digest, ix as u64, tx.timestamp_ms(), layout)
167 .with_context(|| format!("Failed to convert Event {ix} into response"))?;
168
169 sui_events.push(sui_event)
170 }
171
172 Ok(SuiTransactionBlockEvents { data: sui_events })
173}
174
175fn balance_changes(
177 balance_changes: StoredTxBalanceChange,
178) -> Result<Vec<SuiBalanceChange>, RpcError<Error>> {
179 let balance_changes: Vec<BalanceChange> = bcs::from_bytes(&balance_changes.balance_changes)
180 .context("Failed to deserialize BalanceChanges")?;
181 let mut response = Vec::with_capacity(balance_changes.len());
182
183 for BalanceChange::V1 {
184 owner,
185 coin_type,
186 amount,
187 } in balance_changes
188 {
189 let coin_type = TypeTag::from_str(&coin_type)
190 .with_context(|| format!("Invalid coin type: {coin_type:?}"))?;
191
192 response.push(SuiBalanceChange {
193 owner,
194 coin_type,
195 amount,
196 });
197 }
198
199 Ok(response)
200}
201
202async fn object_changes(
205 ctx: &Context,
206 digest: TransactionDigest,
207 tx: &TransactionContents,
208) -> Result<Vec<SuiObjectChange>, RpcError<Error>> {
209 let tx_data: TransactionData = tx.data()?;
210 let effects: TransactionEffects = tx.effects()?;
211
212 let mut keys = vec![];
213 let native_changes = effects.object_changes();
214 for change in &native_changes {
215 let id = change.id;
216 if let Some(version) = change.input_version {
217 keys.push(VersionedObjectKey(id, version.value()));
218 }
219 if let Some(version) = change.output_version {
220 keys.push(VersionedObjectKey(id, version.value()));
221 }
222 }
223
224 let objects = ctx
225 .kv_loader()
226 .load_many_objects(keys)
227 .await
228 .context("Failed to fetch object contents")?;
229
230 let fetch_object = |id: ObjectID,
235 v: Option<SequenceNumber>,
236 d: Option<ObjectDigest>|
237 -> Result<Option<(Object, ObjectDigest)>, RpcError<Error>> {
238 let Some(v) = v else { return Ok(None) };
239 let Some(d) = d else { return Ok(None) };
240
241 let v = v.value();
242
243 let o = objects
244 .get(&VersionedObjectKey(id, v))
245 .ok_or_else(|| invalid_params(Error::PrunedObject(digest, id, v)))?;
246
247 Ok(Some((o.clone(), d)))
248 };
249
250 let mut changes = Vec::with_capacity(native_changes.len());
251
252 for change in native_changes {
253 let &ObjectChange {
254 id: object_id,
255 id_operation,
256 input_version,
257 input_digest,
258 output_version,
259 output_digest,
260 ..
261 } = &change;
262
263 let input = fetch_object(object_id, input_version, input_digest)?;
264 let output = fetch_object(object_id, output_version, output_digest)?;
265
266 use IDOperation as ID;
267 changes.push(match (id_operation, input, output) {
268 (ID::Created, Some((i, _)), _) => rpc_bail!(
269 "Unexpected input version {} for object {object_id} created by transaction {digest}",
270 i.version().value(),
271 ),
272
273 (ID::Deleted, _, Some((o, _))) => rpc_bail!(
274 "Unexpected output version {} for object {object_id} deleted by transaction {digest}",
275 o.version().value(),
276 ),
277
278 (ID::Created, _, None) => continue,
282 (ID::None, None, _) => continue,
283 (ID::None, _, Some((o, _))) if o.is_package() => continue,
284 (ID::Deleted, None, _) => continue,
285
286 (ID::Created, _, Some((o, d))) if o.is_package() => SuiObjectChange::Published {
287 package_id: object_id,
288 version: o.version(),
289 digest: d,
290 modules: o
291 .data
292 .try_as_package()
293 .unwrap() .serialized_module_map()
295 .keys()
296 .cloned()
297 .collect(),
298 },
299
300 (ID::Created, _, Some((o, d))) => SuiObjectChange::Created {
301 sender: tx_data.sender(),
302 owner: o.owner().clone(),
303 object_type: o
304 .struct_tag()
305 .with_context(|| format!("No type for object {object_id}"))?,
306 object_id,
307 version: o.version(),
308 digest: d,
309 },
310
311 (ID::None, Some((i, _)), Some((o, od))) if i.owner() != o.owner() => {
312 SuiObjectChange::Transferred {
313 sender: tx_data.sender(),
314 recipient: o.owner().clone(),
315 object_type: o
316 .struct_tag()
317 .with_context(|| format!("No type for object {object_id}"))?,
318 object_id,
319 version: o.version(),
320 digest: od,
321 }
322 }
323
324 (ID::None, Some((i, _)), Some((o, od))) => SuiObjectChange::Mutated {
325 sender: tx_data.sender(),
326 owner: o.owner().clone(),
327 object_type: o
328 .struct_tag()
329 .with_context(|| format!("No type for object {object_id}"))?,
330 object_id,
331 version: o.version(),
332 previous_version: i.version(),
333 digest: od,
334 },
335
336 (ID::None, Some((i, _)), None) => SuiObjectChange::Wrapped {
337 sender: tx_data.sender(),
338 object_type: i
339 .struct_tag()
340 .with_context(|| format!("No type for object {object_id}"))?,
341 object_id,
342 version: effects.lamport_version(),
343 },
344
345 (ID::Deleted, Some((i, _)), None) => SuiObjectChange::Deleted {
346 sender: tx_data.sender(),
347 object_type: i
348 .struct_tag()
349 .with_context(|| format!("No type for object {object_id}"))?,
350 object_id,
351 version: effects.lamport_version(),
352 },
353 })
354 }
355
356 Ok(changes)
357}