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 = Some(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 =
159 SuiEvent::try_from(event, digest, ix as u64, Some(tx.timestamp_ms()), layout)
160 .with_context(|| format!("Failed to convert Event {ix} into response"))?;
161
162 sui_events.push(sui_event)
163 }
164
165 Ok(SuiTransactionBlockEvents { data: sui_events })
166}
167
168fn balance_changes(
170 balance_changes: StoredTxBalanceChange,
171) -> Result<Vec<SuiBalanceChange>, RpcError<Error>> {
172 let balance_changes: Vec<BalanceChange> = bcs::from_bytes(&balance_changes.balance_changes)
173 .context("Failed to deserialize BalanceChanges")?;
174 let mut response = Vec::with_capacity(balance_changes.len());
175
176 for BalanceChange::V1 {
177 owner,
178 coin_type,
179 amount,
180 } in balance_changes
181 {
182 let coin_type = TypeTag::from_str(&coin_type)
183 .with_context(|| format!("Invalid coin type: {coin_type:?}"))?;
184
185 response.push(SuiBalanceChange {
186 owner,
187 coin_type,
188 amount,
189 });
190 }
191
192 Ok(response)
193}
194
195async fn object_changes(
198 ctx: &Context,
199 digest: TransactionDigest,
200 tx: &TransactionContents,
201) -> Result<Vec<SuiObjectChange>, RpcError<Error>> {
202 let tx_data: TransactionData = tx.data()?;
203 let effects: TransactionEffects = tx.effects()?;
204
205 let mut keys = vec![];
206 let native_changes = effects.object_changes();
207 for change in &native_changes {
208 let id = change.id;
209 if let Some(version) = change.input_version {
210 keys.push(VersionedObjectKey(id, version.value()));
211 }
212 if let Some(version) = change.output_version {
213 keys.push(VersionedObjectKey(id, version.value()));
214 }
215 }
216
217 let objects = ctx
218 .kv_loader()
219 .load_many_objects(keys)
220 .await
221 .context("Failed to fetch object contents")?;
222
223 let fetch_object = |id: ObjectID,
228 v: Option<SequenceNumber>,
229 d: Option<ObjectDigest>|
230 -> Result<Option<(Object, ObjectDigest)>, RpcError<Error>> {
231 let Some(v) = v else { return Ok(None) };
232 let Some(d) = d else { return Ok(None) };
233
234 let v = v.value();
235
236 let o = objects
237 .get(&VersionedObjectKey(id, v))
238 .ok_or_else(|| invalid_params(Error::PrunedObject(digest, id, v)))?;
239
240 Ok(Some((o.clone(), d)))
241 };
242
243 let mut changes = Vec::with_capacity(native_changes.len());
244
245 for change in native_changes {
246 let &ObjectChange {
247 id: object_id,
248 id_operation,
249 input_version,
250 input_digest,
251 output_version,
252 output_digest,
253 ..
254 } = &change;
255
256 let input = fetch_object(object_id, input_version, input_digest)?;
257 let output = fetch_object(object_id, output_version, output_digest)?;
258
259 use IDOperation as ID;
260 changes.push(match (id_operation, input, output) {
261 (ID::Created, Some((i, _)), _) => rpc_bail!(
262 "Unexpected input version {} for object {object_id} created by transaction {digest}",
263 i.version().value(),
264 ),
265
266 (ID::Deleted, _, Some((o, _))) => rpc_bail!(
267 "Unexpected output version {} for object {object_id} deleted by transaction {digest}",
268 o.version().value(),
269 ),
270
271 (ID::Created, _, None) => continue,
275 (ID::None, None, _) => continue,
276 (ID::None, _, Some((o, _))) if o.is_package() => continue,
277 (ID::Deleted, None, _) => continue,
278
279 (ID::Created, _, Some((o, d))) if o.is_package() => SuiObjectChange::Published {
280 package_id: object_id,
281 version: o.version(),
282 digest: d,
283 modules: o
284 .data
285 .try_as_package()
286 .unwrap() .serialized_module_map()
288 .keys()
289 .cloned()
290 .collect(),
291 },
292
293 (ID::Created, _, Some((o, d))) => SuiObjectChange::Created {
294 sender: tx_data.sender(),
295 owner: o.owner().clone(),
296 object_type: o
297 .struct_tag()
298 .with_context(|| format!("No type for object {object_id}"))?,
299 object_id,
300 version: o.version(),
301 digest: d,
302 },
303
304 (ID::None, Some((i, _)), Some((o, od))) if i.owner() != o.owner() => {
305 SuiObjectChange::Transferred {
306 sender: tx_data.sender(),
307 recipient: o.owner().clone(),
308 object_type: o
309 .struct_tag()
310 .with_context(|| format!("No type for object {object_id}"))?,
311 object_id,
312 version: o.version(),
313 digest: od,
314 }
315 }
316
317 (ID::None, Some((i, _)), Some((o, od))) => SuiObjectChange::Mutated {
318 sender: tx_data.sender(),
319 owner: o.owner().clone(),
320 object_type: o
321 .struct_tag()
322 .with_context(|| format!("No type for object {object_id}"))?,
323 object_id,
324 version: o.version(),
325 previous_version: i.version(),
326 digest: od,
327 },
328
329 (ID::None, Some((i, _)), None) => SuiObjectChange::Wrapped {
330 sender: tx_data.sender(),
331 object_type: i
332 .struct_tag()
333 .with_context(|| format!("No type for object {object_id}"))?,
334 object_id,
335 version: effects.lamport_version(),
336 },
337
338 (ID::Deleted, Some((i, _)), None) => SuiObjectChange::Deleted {
339 sender: tx_data.sender(),
340 object_type: i
341 .struct_tag()
342 .with_context(|| format!("No type for object {object_id}"))?,
343 object_id,
344 version: effects.lamport_version(),
345 },
346 })
347 }
348
349 Ok(changes)
350}