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;
5use std::sync::Arc;
6
7use anyhow::Context as _;
8use futures::future::OptionFuture;
9use move_core_types::annotated_value::MoveDatatypeLayout;
10use move_core_types::annotated_value::MoveTypeLayout;
11use sui_indexer_alt_reader::kv_loader::TransactionContents;
12use sui_indexer_alt_reader::objects::VersionedObjectKey;
13use sui_indexer_alt_reader::tx_balance_changes::TxBalanceChangeKey;
14use sui_indexer_alt_schema::transactions::BalanceChange;
15use sui_indexer_alt_schema::transactions::StoredTxBalanceChange;
16use sui_json_rpc_types::BalanceChange as SuiBalanceChange;
17use sui_json_rpc_types::ObjectChange as SuiObjectChange;
18use sui_json_rpc_types::SuiEvent;
19use sui_json_rpc_types::SuiTransactionBlock;
20use sui_json_rpc_types::SuiTransactionBlockData;
21use sui_json_rpc_types::SuiTransactionBlockEffects;
22use sui_json_rpc_types::SuiTransactionBlockEvents;
23use sui_json_rpc_types::SuiTransactionBlockResponse;
24use sui_json_rpc_types::SuiTransactionBlockResponseOptions;
25use sui_types::TypeTag;
26use sui_types::base_types::ObjectID;
27use sui_types::base_types::SequenceNumber;
28use sui_types::digests::ObjectDigest;
29use sui_types::digests::TransactionDigest;
30use sui_types::effects::ObjectChange;
31use sui_types::effects::TransactionEffects;
32use sui_types::effects::TransactionEffectsAPI;
33use sui_types::object::Object;
34use sui_types::signature::GenericSignature;
35use sui_types::transaction::TransactionData;
36use sui_types::transaction::TransactionDataAPI;
37use tokio::join;
38
39use crate::api::to_sui_object_change;
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 = 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(
167            Arc::unwrap_or_clone(event),
168            digest,
169            ix as u64,
170            tx.timestamp_ms(),
171            layout,
172        )
173        .with_context(|| format!("Failed to convert Event {ix} into response"))?;
174
175        sui_events.push(sui_event)
176    }
177
178    Ok(SuiTransactionBlockEvents { data: sui_events })
179}
180
181/// Extract the transaction's balance changes from their stored form.
182fn balance_changes(
183    balance_changes: StoredTxBalanceChange,
184) -> Result<Vec<SuiBalanceChange>, RpcError<Error>> {
185    let balance_changes: Vec<BalanceChange> = bcs::from_bytes(&balance_changes.balance_changes)
186        .context("Failed to deserialize BalanceChanges")?;
187    let mut response = Vec::with_capacity(balance_changes.len());
188
189    for BalanceChange::V1 {
190        owner,
191        coin_type,
192        amount,
193    } in balance_changes
194    {
195        let coin_type = TypeTag::from_str(&coin_type)
196            .with_context(|| format!("Invalid coin type: {coin_type:?}"))?;
197
198        response.push(SuiBalanceChange {
199            owner,
200            coin_type,
201            amount,
202        });
203    }
204
205    Ok(response)
206}
207
208/// Extract the transaction's object changes. Object IDs and versions are fetched from the stored
209/// transaction, and the object contents are fetched separately by a data loader.
210async fn object_changes(
211    ctx: &Context,
212    digest: TransactionDigest,
213    tx: &TransactionContents,
214) -> Result<Vec<SuiObjectChange>, RpcError<Error>> {
215    let tx_data: TransactionData = tx.data()?;
216    let effects: TransactionEffects = tx.effects()?;
217
218    let mut keys = vec![];
219    let native_changes = effects.object_changes();
220    for change in &native_changes {
221        let id = change.id;
222        if let Some(version) = change.input_version {
223            keys.push(VersionedObjectKey(id, version.value()));
224        }
225        if let Some(version) = change.output_version {
226            keys.push(VersionedObjectKey(id, version.value()));
227        }
228    }
229
230    let objects = ctx
231        .kv_loader()
232        .load_many_objects(keys)
233        .await
234        .context("Failed to fetch object contents")?;
235
236    // Fetch and deserialize the contents of an object, based on its object ref. Assumes that all
237    // object versions that will be fetched in this way have come from a valid transaction, and
238    // have been passed to the data loader in the call above. This means that if they cannot be
239    // found, they must have been pruned.
240    let fetch_object = |id: ObjectID,
241                        v: Option<SequenceNumber>,
242                        d: Option<ObjectDigest>|
243     -> Result<Option<(Object, ObjectDigest)>, RpcError<Error>> {
244        let Some(v) = v else { return Ok(None) };
245        let Some(d) = d else { return Ok(None) };
246
247        let v = v.value();
248
249        let o = objects
250            .get(&VersionedObjectKey(id, v))
251            .ok_or_else(|| invalid_params(Error::PrunedObject(digest, id, v)))?;
252
253        Ok(Some((o.clone(), d)))
254    };
255
256    let mut changes = Vec::with_capacity(native_changes.len());
257
258    for change in native_changes {
259        let &ObjectChange {
260            id: object_id,
261            id_operation,
262            input_version,
263            input_digest,
264            output_version,
265            output_digest,
266            ..
267        } = &change;
268
269        let input = fetch_object(object_id, input_version, input_digest)?;
270        let output = fetch_object(object_id, output_version, output_digest)?;
271
272        changes.extend(to_sui_object_change(
273            tx_data.sender(),
274            object_id,
275            id_operation,
276            input,
277            output,
278            effects.lamport_version(),
279        )?);
280    }
281
282    Ok(changes)
283}