Skip to main content

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::SenderSignedData;
36use sui_types::transaction::TransactionData;
37use sui_types::transaction::TransactionDataAPI;
38use tokio::join;
39
40use crate::api::to_sui_object_change;
41use crate::api::transactions::error::Error;
42use crate::context::Context;
43use crate::error::RpcError;
44use crate::error::invalid_params;
45use crate::error::rpc_bail;
46
47/// Fetch the necessary data from the stores in `ctx` and transform it to build a response for the
48/// transaction identified by `digest`, according to the response `options`.
49pub(super) async fn transaction(
50    ctx: &Context,
51    digest: TransactionDigest,
52    options: &SuiTransactionBlockResponseOptions,
53) -> Result<SuiTransactionBlockResponse, RpcError<Error>> {
54    let tx = ctx.kv_loader().load_one_transaction(digest);
55    let stored_bc: OptionFuture<_> = options
56        .show_balance_changes
57        .then(|| ctx.pg_loader().load_one(TxBalanceChangeKey(digest)))
58        .into();
59
60    let (tx, stored_bc) = join!(tx, stored_bc);
61
62    let tx = tx
63        .context("Failed to fetch transaction from store")?
64        .ok_or_else(|| invalid_params(Error::NotFound(digest)))?;
65
66    // Balance changes might not be present because of pruning, in which case we return
67    // nothing, even if the changes were requested.
68    let stored_bc = match stored_bc
69        .transpose()
70        .context("Failed to fetch balance changes from store")?
71    {
72        Some(None) => return Err(invalid_params(Error::BalanceChangesNotFound(digest))),
73        Some(changes) => changes,
74        None => None,
75    };
76
77    let digest = tx.digest()?;
78
79    let mut response = SuiTransactionBlockResponse::new(digest);
80
81    response.timestamp_ms = tx.timestamp_ms();
82    response.checkpoint = tx.cp_sequence_number();
83
84    if options.show_input {
85        response.transaction = Some(input(ctx, &tx).await?);
86    }
87
88    if options.show_raw_input {
89        response.raw_transaction = raw_input(&tx)?;
90    }
91
92    if options.show_effects {
93        response.effects = Some(effects(&tx)?);
94    }
95
96    if options.show_raw_effects {
97        response.raw_effects = tx.raw_effects()?;
98    }
99
100    if options.show_events {
101        response.events = Some(events(ctx, digest, &tx).await?);
102    }
103
104    if let Some(changes) = stored_bc {
105        response.balance_changes = Some(balance_changes(changes)?);
106    }
107
108    if options.show_object_changes {
109        response.object_changes = Some(object_changes(ctx, digest, &tx).await?);
110    }
111
112    Ok(response)
113}
114
115/// Extract a representation of the transaction's input data from the stored form.
116async fn input(
117    ctx: &Context,
118    tx: &TransactionContents,
119) -> Result<SuiTransactionBlock, RpcError<Error>> {
120    let data: TransactionData = tx.data()?;
121    let tx_signatures: Vec<GenericSignature> = tx.signatures()?;
122
123    Ok(SuiTransactionBlock {
124        data: SuiTransactionBlockData::try_from_with_package_resolver(data, ctx.package_resolver())
125            .await
126            .context("Failed to resolve types in transaction data")?,
127        tx_signatures,
128    })
129}
130
131/// Extract the transaction's raw BCS input in the JSON-RPC representation.
132fn raw_input(tx: &TransactionContents) -> Result<Vec<u8>, RpcError<Error>> {
133    let data = SenderSignedData::new(tx.data()?, tx.signatures()?);
134    Ok(bcs::to_bytes(&data).context("Failed to serialize transaction")?)
135}
136
137/// Extract a representation of the transaction's effects from the stored form.
138fn effects(tx: &TransactionContents) -> Result<SuiTransactionBlockEffects, RpcError<Error>> {
139    let effects: TransactionEffects = tx.effects()?;
140    Ok(effects
141        .try_into()
142        .context("Failed to convert Effects into response")?)
143}
144
145/// Extract the transaction's events from its stored form.
146async fn events(
147    ctx: &Context,
148    digest: TransactionDigest,
149    tx: &TransactionContents,
150) -> Result<SuiTransactionBlockEvents, RpcError<Error>> {
151    let events = tx.events()?;
152    let mut sui_events = Vec::with_capacity(events.len());
153
154    for (ix, event) in events.into_iter().enumerate() {
155        let layout = match ctx
156            .package_resolver()
157            .type_layout(event.type_.clone().into())
158            .await
159            .with_context(|| {
160                format!(
161                    "Failed to resolve layout for {}",
162                    event.type_.to_canonical_display(/* with_prefix */ true)
163                )
164            })? {
165            MoveTypeLayout::Struct(s) => MoveDatatypeLayout::Struct(s),
166            MoveTypeLayout::Enum(e) => MoveDatatypeLayout::Enum(e),
167            _ => rpc_bail!(
168                "Event {ix} is not a struct or enum: {}",
169                event.type_.to_canonical_string(/* with_prefix */ true)
170            ),
171        };
172
173        let sui_event = SuiEvent::try_from(
174            Arc::unwrap_or_clone(event),
175            digest,
176            ix as u64,
177            tx.timestamp_ms(),
178            layout,
179        )
180        .with_context(|| format!("Failed to convert Event {ix} into response"))?;
181
182        sui_events.push(sui_event)
183    }
184
185    Ok(SuiTransactionBlockEvents { data: sui_events })
186}
187
188/// Extract the transaction's balance changes from their stored form.
189fn balance_changes(
190    balance_changes: StoredTxBalanceChange,
191) -> Result<Vec<SuiBalanceChange>, RpcError<Error>> {
192    let balance_changes: Vec<BalanceChange> = bcs::from_bytes(&balance_changes.balance_changes)
193        .context("Failed to deserialize BalanceChanges")?;
194    let mut response = Vec::with_capacity(balance_changes.len());
195
196    for BalanceChange::V1 {
197        owner,
198        coin_type,
199        amount,
200    } in balance_changes
201    {
202        let coin_type = TypeTag::from_str(&coin_type)
203            .with_context(|| format!("Invalid coin type: {coin_type:?}"))?;
204
205        response.push(SuiBalanceChange {
206            owner,
207            coin_type,
208            amount,
209        });
210    }
211
212    Ok(response)
213}
214
215/// Extract the transaction's object changes. Object IDs and versions are fetched from the stored
216/// transaction, and the object contents are fetched separately by a data loader.
217async fn object_changes(
218    ctx: &Context,
219    digest: TransactionDigest,
220    tx: &TransactionContents,
221) -> Result<Vec<SuiObjectChange>, RpcError<Error>> {
222    let tx_data: TransactionData = tx.data()?;
223    let effects: TransactionEffects = tx.effects()?;
224
225    let mut keys = vec![];
226    let native_changes = effects.object_changes();
227    for change in &native_changes {
228        let id = change.id;
229        if let Some(version) = change.input_version {
230            keys.push(VersionedObjectKey(id, version.value()));
231        }
232        if let Some(version) = change.output_version {
233            keys.push(VersionedObjectKey(id, version.value()));
234        }
235    }
236
237    let objects = ctx
238        .kv_loader()
239        .load_many_objects(keys)
240        .await
241        .context("Failed to fetch object contents")?;
242
243    // Fetch and deserialize the contents of an object, based on its object ref. Assumes that all
244    // object versions that will be fetched in this way have come from a valid transaction, and
245    // have been passed to the data loader in the call above. This means that if they cannot be
246    // found, they must have been pruned.
247    let fetch_object = |id: ObjectID,
248                        v: Option<SequenceNumber>,
249                        d: Option<ObjectDigest>|
250     -> Result<Option<(Object, ObjectDigest)>, RpcError<Error>> {
251        let Some(v) = v else { return Ok(None) };
252        let Some(d) = d else { return Ok(None) };
253
254        let v = v.value();
255
256        let o = objects
257            .get(&VersionedObjectKey(id, v))
258            .ok_or_else(|| invalid_params(Error::PrunedObject(digest, id, v)))?;
259
260        Ok(Some((o.clone(), d)))
261    };
262
263    let mut changes = Vec::with_capacity(native_changes.len());
264
265    for change in native_changes {
266        let &ObjectChange {
267            id: object_id,
268            id_operation,
269            input_version,
270            input_digest,
271            output_version,
272            output_digest,
273            ..
274        } = &change;
275
276        let input = fetch_object(object_id, input_version, input_digest)?;
277        let output = fetch_object(object_id, output_version, output_digest)?;
278
279        changes.extend(to_sui_object_change(
280            tx_data.sender(),
281            object_id,
282            id_operation,
283            input,
284            output,
285            effects.lamport_version(),
286        )?);
287    }
288
289    Ok(changes)
290}