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, 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
38/// Fetch the necessary data from the stores in `ctx` and transform it to build a response for the
39/// transaction identified by `digest`, according to the response `options`.
40pub(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    // Balance changes might not be present because of pruning, in which case we return
58    // nothing, even if the changes were requested.
59    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
106/// Extract a representation of the transaction's input data from the stored form.
107async 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
122/// Extract a representation of the transaction's effects from the stored form.
123fn 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
130/// Extract the transaction's events from its stored form.
131async 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(/* with_prefix */ 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(/* with_prefix */ 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
168/// Extract the transaction's balance changes from their stored form.
169fn 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
195/// Extract the transaction's object changes. Object IDs and versions are fetched from the stored
196/// transaction, and the object contents are fetched separately by a data loader.
197async 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    // Fetch and deserialize the contents of an object, based on its object ref. Assumes that all
224    // object versions that will be fetched in this way have come from a valid transaction, and
225    // have been passed to the data loader in the call above. This means that if they cannot be
226    // found, they must have been pruned.
227    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            // The following cases don't end up in the output: created and wrapped objects,
272            // unwrapped objects (and by extension, unwrapped and deleted objects), system package
273            // upgrades (which happen in place).
274            (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() // SAFETY: Match guard checks that the object is a package.
287                    .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}