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