sui_indexer_alt_jsonrpc/api/transactions/
response.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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
46/// Fetch the necessary data from the stores in `ctx` and transform it to build a response for the
47/// transaction identified by `digest`, according to the response `options`.
48pub(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    // Balance changes might not be present because of pruning, in which case we return
66    // nothing, even if the changes were requested.
67    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
114/// Extract a representation of the transaction's input data from the stored form.
115async 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
130/// Extract a representation of the transaction's effects from the stored form.
131fn 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
138/// Extract the transaction's events from its stored form.
139async 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(/* with_prefix */ 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(/* with_prefix */ 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
175/// Extract the transaction's balance changes from their stored form.
176fn 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
202/// Extract the transaction's object changes. Object IDs and versions are fetched from the stored
203/// transaction, and the object contents are fetched separately by a data loader.
204async 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    // Fetch and deserialize the contents of an object, based on its object ref. Assumes that all
231    // object versions that will be fetched in this way have come from a valid transaction, and
232    // have been passed to the data loader in the call above. This means that if they cannot be
233    // found, they must have been pruned.
234    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            // The following cases don't end up in the output: created and wrapped objects,
279            // unwrapped objects (and by extension, unwrapped and deleted objects), system package
280            // upgrades (which happen in place).
281            (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() // SAFETY: Match guard checks that the object is a package.
294                    .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}