Skip to main content

iota_json_rpc/
indexer_api.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{collections::HashSet, sync::Arc, time::Duration};
6
7use anyhow::{anyhow, bail};
8use async_trait::async_trait;
9use futures::{Stream, StreamExt};
10use iota_core::authority::AuthorityState;
11use iota_json::IotaJsonValue;
12use iota_json_rpc_api::{
13    IndexerApiOpenRpc, IndexerApiServer, JsonRpcMetrics, QUERY_MAX_RESULT_LIMIT, ReadApiServer,
14    cap_page_limit, validate_limit,
15};
16use iota_json_rpc_types::{
17    DynamicFieldPage, EventFilter, EventPage, IotaNameRecord, IotaObjectDataFilter,
18    IotaObjectDataOptions, IotaObjectResponse, IotaObjectResponseError, IotaObjectResponseQuery,
19    IotaTransactionBlockResponse, IotaTransactionBlockResponseQuery,
20    IotaTransactionBlockResponseQueryV2, ObjectsPage, Page, TransactionBlocksPage,
21    TransactionFilter,
22};
23use iota_metrics::spawn_monitored_task;
24use iota_names::{
25    IotaNamesNft, NameRegistration, config::IotaNamesConfig, error::IotaNamesError, name::Name,
26    registry::NameRecord,
27};
28use iota_open_rpc::Module;
29use iota_sdk_types::{Address, ObjectId, TransactionDigest, TypeTag};
30use iota_storage::key_value_store::TransactionKeyValueStore;
31use iota_types::{
32    dynamic_field::{DynamicFieldName, Field},
33    error::UserInputError,
34    event::EventID,
35    iota_sdk_types_conversions::type_tag_sdk_to_core,
36};
37use jsonrpsee::{
38    PendingSubscriptionSink, RpcModule, SendTimeoutError, SubscriptionMessage,
39    core::{RpcResult, SubscriptionResult},
40};
41use move_bytecode_utils::layout::TypeLayoutBuilder;
42use serde::Serialize;
43use tokio::sync::{OwnedSemaphorePermit, Semaphore};
44use tracing::{debug, instrument};
45
46use crate::{
47    IotaRpcModule,
48    authority_state::{StateRead, StateReadResult},
49    error::{Error, IotaRpcInputError},
50    logger::FutureWithTracing as _,
51};
52
53async fn pipe_from_stream<T: Serialize>(
54    pending: PendingSubscriptionSink,
55    mut stream: impl Stream<Item = T> + Unpin,
56) -> Result<(), anyhow::Error> {
57    let sink = pending.accept().await?;
58
59    loop {
60        tokio::select! {
61            _ = sink.closed() => break Ok(()),
62            maybe_item = stream.next() => {
63                let Some(item) = maybe_item else {
64                    break Ok(());
65                };
66
67                let msg = SubscriptionMessage::from_json(&item)?;
68
69                if let Err(e) = sink.send_timeout(msg, Duration::from_secs(60)).await {
70                    match e {
71                        // The subscription or connection was closed.
72                        SendTimeoutError::Closed(_) => break Ok(()),
73                        // The subscription send timeout expired
74                        // the message is returned and you could save that message
75                        // and retry again later.
76                        SendTimeoutError::Timeout(_) => break Err(anyhow::anyhow!("Subscription timeout expired")),
77                    }
78                }
79            }
80        }
81    }
82}
83
84pub fn spawn_subscription<S, T>(
85    pending: PendingSubscriptionSink,
86    rx: S,
87    permit: Option<OwnedSemaphorePermit>,
88) where
89    S: Stream<Item = T> + Unpin + Send + 'static,
90    T: Serialize + Send,
91{
92    spawn_monitored_task!(async move {
93        let _permit = permit;
94        match pipe_from_stream(pending, rx).await {
95            Ok(_) => {
96                debug!("Subscription completed.");
97            }
98            Err(err) => {
99                debug!("Subscription failed: {err:?}");
100            }
101        }
102    });
103}
104const DEFAULT_MAX_SUBSCRIPTIONS: usize = 100;
105
106pub struct IndexerApi<R> {
107    state: Arc<dyn StateRead>,
108    read_api: R,
109    transaction_kv_store: Arc<TransactionKeyValueStore>,
110    iota_names_config: IotaNamesConfig,
111    pub metrics: Arc<JsonRpcMetrics>,
112    subscription_semaphore: Arc<Semaphore>,
113}
114
115impl<R: ReadApiServer> IndexerApi<R> {
116    pub fn new(
117        state: Arc<AuthorityState>,
118        read_api: R,
119        transaction_kv_store: Arc<TransactionKeyValueStore>,
120        metrics: Arc<JsonRpcMetrics>,
121        iota_names_config: IotaNamesConfig,
122        max_subscriptions: Option<usize>,
123    ) -> Self {
124        let max_subscriptions = max_subscriptions.unwrap_or(DEFAULT_MAX_SUBSCRIPTIONS);
125        Self {
126            state,
127            transaction_kv_store,
128            read_api,
129            metrics,
130            iota_names_config,
131            subscription_semaphore: Arc::new(Semaphore::new(max_subscriptions)),
132        }
133    }
134
135    fn extract_values_from_dynamic_field_name(
136        &self,
137        name: DynamicFieldName,
138    ) -> Result<(TypeTag, Vec<u8>), IotaRpcInputError> {
139        let DynamicFieldName {
140            type_tag: name_type,
141            value,
142        } = name;
143        let epoch_store = self.state.load_epoch_store_one_call_per_task();
144        let layout = TypeLayoutBuilder::build_with_types(
145            &type_tag_sdk_to_core(&name_type),
146            epoch_store.module_cache(),
147        )?;
148        let iota_json_value = IotaJsonValue::new(value)?;
149        let name_bcs_value = iota_json_value.to_bcs_bytes(&layout)?;
150        Ok((name_type, name_bcs_value))
151    }
152
153    fn acquire_subscribe_permit(&self) -> anyhow::Result<OwnedSemaphorePermit> {
154        match self.subscription_semaphore.clone().try_acquire_owned() {
155            Ok(p) => Ok(p),
156            Err(_) => bail!("Resources exhausted"),
157        }
158    }
159
160    async fn get_dynamic_field_object(
161        &self,
162        parent_object_id: ObjectId,
163        name: DynamicFieldName,
164        options: Option<IotaObjectDataOptions>,
165    ) -> RpcResult<IotaObjectResponse> {
166        async move {
167            let (name_type, name_bcs_value) = self.extract_values_from_dynamic_field_name(name)?;
168
169            let id = self
170                .state
171                .get_dynamic_field_object_id(parent_object_id, name_type, &name_bcs_value)
172                .map_err(Error::from)?;
173
174            if let Some(id) = id {
175                self.read_api
176                    .get_object(id, options)
177                    .await
178                    .map_err(|e| Error::Internal(anyhow!(e)))
179            } else {
180                Ok(IotaObjectResponse::new_with_error(
181                    IotaObjectResponseError::DynamicFieldNotFound { parent_object_id },
182                ))
183            }
184        }
185        .trace()
186        .await
187    }
188
189    fn get_latest_checkpoint_timestamp_ms(&self) -> StateReadResult<u64> {
190        let latest_checkpoint = self.state.get_latest_checkpoint_sequence_number()?;
191
192        let checkpoint = self
193            .state
194            .get_verified_checkpoint_by_sequence_number(latest_checkpoint)?;
195
196        Ok(checkpoint.timestamp_ms)
197    }
198}
199
200#[async_trait]
201impl<R: ReadApiServer> IndexerApiServer for IndexerApi<R> {
202    #[instrument(skip(self, address), fields(address = %address))]
203    async fn get_owned_objects(
204        &self,
205        address: Address,
206        query: Option<IotaObjectResponseQuery>,
207        cursor: Option<ObjectId>,
208        limit: Option<usize>,
209    ) -> RpcResult<ObjectsPage> {
210        async move {
211            let limit =
212                validate_limit(limit, *QUERY_MAX_RESULT_LIMIT).map_err(IotaRpcInputError::from)?;
213            self.metrics.get_owned_objects_limit.observe(limit as f64);
214            let IotaObjectResponseQuery { filter, options } = query.unwrap_or_default();
215            let options = options.unwrap_or_default();
216            let mut objects =
217                self.state
218                    .get_owner_objects_with_limit(address, cursor, limit + 1, filter)?;
219
220            // objects here are of size (limit + 1), where the last one is the cursor for
221            // the next page
222            let has_next_page = objects.len() > limit && limit > 0;
223            objects.truncate(limit);
224            let next_cursor = (has_next_page).then_some(
225                objects
226                    .last()
227                    .map(|obj| obj.object_id)
228                    .unwrap_or(ObjectId::ZERO),
229            );
230
231            let data = match options.is_not_in_object_info() {
232                true => {
233                    let object_ids = objects.iter().map(|obj| obj.object_id).collect();
234                    self.read_api
235                        .multi_get_objects(object_ids, Some(options))
236                        .await
237                        .map_err(|e| Error::Internal(anyhow!(e)))?
238                }
239                false => objects
240                    .into_iter()
241                    .map(|o_info| IotaObjectResponse::try_from((o_info, options.clone())))
242                    .collect::<Result<Vec<IotaObjectResponse>, _>>()?,
243            };
244
245            self.metrics
246                .get_owned_objects_result_size
247                .observe(data.len() as f64);
248            self.metrics
249                .get_owned_objects_result_size_total
250                .inc_by(data.len() as u64);
251            Ok(Page {
252                data,
253                next_cursor,
254                has_next_page,
255            })
256        }
257        .trace()
258        .await
259    }
260
261    #[instrument(skip(self))]
262    async fn query_transaction_blocks(
263        &self,
264        query: IotaTransactionBlockResponseQuery,
265        // If `Some`, the query will start from the next item after the specified cursor
266        cursor: Option<TransactionDigest>,
267        limit: Option<usize>,
268        descending_order: Option<bool>,
269    ) -> RpcResult<TransactionBlocksPage> {
270        async move {
271            let limit = cap_page_limit(limit);
272            self.metrics.query_tx_blocks_limit.observe(limit as f64);
273            let descending = descending_order.unwrap_or_default();
274            let opts = query.options.unwrap_or_default();
275
276            // Retrieve 1 extra item for next cursor
277            let mut digests = self
278                .state
279                .get_transactions(
280                    &self.transaction_kv_store,
281                    query.filter,
282                    cursor,
283                    Some(limit + 1),
284                    descending,
285                )
286                .await
287                .map_err(Error::from)?;
288            // De-dup digests, duplicate digests are possible, for example,
289            // when get_transactions_by_move_function with module or function being None.
290            let mut seen = HashSet::new();
291            digests.retain(|digest| seen.insert(*digest));
292
293            // extract next cursor
294            let has_next_page = digests.len() > limit;
295            digests.truncate(limit);
296            let next_cursor = digests.last().cloned().map_or(cursor, Some);
297
298            let data: Vec<IotaTransactionBlockResponse> = if opts.only_digest() {
299                digests
300                    .into_iter()
301                    .map(IotaTransactionBlockResponse::new)
302                    .collect()
303            } else {
304                self.read_api
305                    .multi_get_transaction_blocks(digests, Some(opts))
306                    .await
307                    .map_err(|e| Error::Internal(anyhow!(e)))?
308            };
309
310            self.metrics
311                .query_tx_blocks_result_size
312                .observe(data.len() as f64);
313            self.metrics
314                .query_tx_blocks_result_size_total
315                .inc_by(data.len() as u64);
316            Ok(Page {
317                data,
318                next_cursor,
319                has_next_page,
320            })
321        }
322        .trace()
323        .await
324    }
325
326    #[instrument(skip(self))]
327    async fn query_transaction_blocks_v2(
328        &self,
329        query: IotaTransactionBlockResponseQueryV2,
330        // If `Some`, the query will start from the next item after the specified cursor
331        cursor: Option<TransactionDigest>,
332        limit: Option<usize>,
333        descending_order: Option<bool>,
334    ) -> RpcResult<TransactionBlocksPage> {
335        let v1_filter = query
336            .filter
337            .map(|f| {
338                f.as_v1().ok_or_else(|| {
339                    Error::UserInput(UserInputError::Unsupported(
340                        "transaction filter is not supported".to_string(),
341                    ))
342                })
343            })
344            .transpose()?;
345
346        let v1_query = IotaTransactionBlockResponseQuery {
347            filter: v1_filter,
348            options: query.options,
349        };
350        self.query_transaction_blocks(v1_query, cursor, limit, descending_order)
351            .await
352    }
353
354    #[instrument(skip(self))]
355    async fn query_events(
356        &self,
357        query: EventFilter,
358        // exclusive cursor if `Some`, otherwise start from the beginning
359        cursor: Option<EventID>,
360        limit: Option<usize>,
361        descending_order: Option<bool>,
362    ) -> RpcResult<EventPage> {
363        async move {
364            let descending = descending_order.unwrap_or_default();
365            let limit = cap_page_limit(limit);
366            self.metrics.query_events_limit.observe(limit as f64);
367            // Retrieve 1 extra item for next cursor
368            let mut data = self
369                .state
370                .query_events(
371                    &self.transaction_kv_store,
372                    query,
373                    cursor,
374                    limit + 1,
375                    descending,
376                )
377                .await
378                .map_err(Error::from)?;
379            let has_next_page = data.len() > limit;
380            data.truncate(limit);
381            let next_cursor = data.last().map_or(cursor, |e| Some(e.id));
382            self.metrics
383                .query_events_result_size
384                .observe(data.len() as f64);
385            self.metrics
386                .query_events_result_size_total
387                .inc_by(data.len() as u64);
388            Ok(EventPage {
389                data,
390                next_cursor,
391                has_next_page,
392            })
393        }
394        .trace()
395        .await
396    }
397
398    #[instrument(skip(self))]
399    fn subscribe_event(
400        &self,
401        sink: PendingSubscriptionSink,
402        filter: EventFilter,
403    ) -> SubscriptionResult {
404        let permit = self.acquire_subscribe_permit()?;
405        spawn_subscription(
406            sink,
407            self.state
408                .get_subscription_handler()
409                .subscribe_events(filter),
410            Some(permit),
411        );
412        Ok(())
413    }
414
415    fn subscribe_transaction(
416        &self,
417        sink: PendingSubscriptionSink,
418        filter: TransactionFilter,
419    ) -> SubscriptionResult {
420        // Validate unsupported filters
421        if matches!(filter, TransactionFilter::Checkpoint(_)) {
422            return Err("checkpoint filter is not supported".into());
423        }
424
425        let permit = self.acquire_subscribe_permit()?;
426        spawn_subscription(
427            sink,
428            self.state
429                .get_subscription_handler()
430                .subscribe_transactions(filter),
431            Some(permit),
432        );
433        Ok(())
434    }
435
436    #[instrument(skip(self, parent_object_id), fields(parent_object_id = %parent_object_id))]
437    async fn get_dynamic_fields(
438        &self,
439        parent_object_id: ObjectId,
440        // If `Some`, the query will start from the next item after the specified cursor
441        cursor: Option<ObjectId>,
442        limit: Option<usize>,
443    ) -> RpcResult<DynamicFieldPage> {
444        async move {
445            let limit = cap_page_limit(limit);
446            self.metrics.get_dynamic_fields_limit.observe(limit as f64);
447            let mut data = self
448                .state
449                .get_dynamic_fields(parent_object_id, cursor, limit + 1)
450                .map_err(Error::from)?;
451            let has_next_page = data.len() > limit;
452            data.truncate(limit);
453            let next_cursor = data.last().cloned().map_or(cursor, |c| Some(c.0));
454            self.metrics
455                .get_dynamic_fields_result_size
456                .observe(data.len() as f64);
457            self.metrics
458                .get_dynamic_fields_result_size_total
459                .inc_by(data.len() as u64);
460            Ok(DynamicFieldPage {
461                data: data.into_iter().map(|(_, w)| w.into()).collect(),
462                next_cursor,
463                has_next_page,
464            })
465        }
466        .trace()
467        .await
468    }
469
470    #[instrument(skip(self, parent_object_id), fields(parent_object_id = %parent_object_id))]
471    async fn get_dynamic_field_object(
472        &self,
473        parent_object_id: ObjectId,
474        name: DynamicFieldName,
475    ) -> RpcResult<IotaObjectResponse> {
476        self.get_dynamic_field_object(
477            parent_object_id,
478            name,
479            Some(IotaObjectDataOptions::full_content()),
480        )
481        .await
482    }
483
484    #[instrument(skip(self, parent_object_id), fields(parent_object_id = %parent_object_id))]
485    async fn get_dynamic_field_object_v2(
486        &self,
487        parent_object_id: ObjectId,
488        name: DynamicFieldName,
489        options: Option<IotaObjectDataOptions>,
490    ) -> RpcResult<IotaObjectResponse> {
491        self.get_dynamic_field_object(parent_object_id, name, options)
492            .await
493    }
494
495    async fn iota_names_lookup(&self, name: &str) -> RpcResult<Option<IotaNameRecord>> {
496        let name = name.parse::<Name>().map_err(Error::from)?;
497
498        // Construct the record id to lookup.
499        let record_id = self.iota_names_config.record_field_id(&name);
500
501        let parent_record_id = name
502            .parent()
503            .map(|parent_name| self.iota_names_config.record_field_id(&parent_name));
504
505        // Keep record IDs alive by declaring both before creating futures
506        let mut requests = vec![self.state.get_object(&record_id)];
507
508        // We only want to fetch both the child and the parent if the name is a
509        // subname.
510        if let Some(ref parent_record_id) = parent_record_id {
511            requests.push(self.state.get_object(parent_record_id));
512        }
513
514        // Couldn't find a `multi_get_object` for this crate (looks like it uses a k,v
515        // db) Always fetching both parent + child at the same time (even for
516        // node subnames), to avoid sequential db reads. We do this because we
517        // do not know if the requested name is a node subname or a leaf
518        // subname, and we can save a trip to the db.
519        let mut results = futures::future::try_join_all(requests)
520            .await
521            .map_err(Error::from)?;
522
523        // Removing without checking vector len, since it is known (== 1 or 2 depending
524        // on whether it is a subname or not).
525        let Some(object) = results.remove(0) else {
526            return Ok(None);
527        };
528
529        let name_record = NameRecord::try_from(object).map_err(Error::from)?;
530
531        let current_timestamp_ms = self
532            .get_latest_checkpoint_timestamp_ms()
533            .map_err(Error::from)?;
534
535        // Handling second-level names & node subnames is the same (we handle them as
536        // `node` records). We check their expiration, and if not expired,
537        // return the target address.
538        if !name_record.is_leaf_record() {
539            return if !name_record.is_node_expired(current_timestamp_ms) {
540                Ok(Some(name_record.into()))
541            } else {
542                Err(Error::from(IotaNamesError::NameExpired).into())
543            };
544        } else {
545            // Handle the `leaf` record case which requires to check the parent for
546            // expiration. We can remove since we know that if we're here, we have a parent
547            // result for the parent request. If the parent result is `None` for the
548            // existing leaf record, we consider it expired.
549            let Some(parent_object) = results.remove(0) else {
550                return Err(Error::from(IotaNamesError::NameExpired).into());
551            };
552
553            let parent_name_record = NameRecord::try_from(parent_object).map_err(Error::from)?;
554
555            // For a leaf record, we check that:
556            // 1. The parent is a valid parent for that leaf record
557            // 2. The parent is not expired
558            if parent_name_record.is_valid_leaf_parent(&name_record)
559                && !parent_name_record.is_node_expired(current_timestamp_ms)
560            {
561                Ok(Some(name_record.into()))
562            } else {
563                Err(Error::from(IotaNamesError::NameExpired).into())
564            }
565        }
566    }
567
568    #[instrument(skip(self, address), fields(address = %address))]
569    async fn iota_names_reverse_lookup(&self, address: Address) -> RpcResult<Option<String>> {
570        let reverse_record_id = self.iota_names_config.reverse_record_field_id(&address);
571
572        let Some(field_reverse_record_object) = self
573            .state
574            .get_object(&reverse_record_id)
575            .await
576            .map_err(Error::from)?
577        else {
578            return Ok(None);
579        };
580
581        let name = field_reverse_record_object
582            .to_rust::<Field<Address, Name>>()
583            .map_err(|e| Error::Unexpected(format!("malformed Object {reverse_record_id}: {e}")))?
584            .value;
585
586        let name = name.to_string();
587
588        let resolved_record = self.iota_names_lookup(&name).await?;
589
590        // If looking up the name returns an empty result, we return an empty result.
591        if resolved_record.is_none() {
592            return Ok(None);
593        }
594
595        Ok(Some(name))
596    }
597
598    #[instrument(skip(self, address), fields(address = %address))]
599    async fn iota_names_find_all_registration_nfts(
600        &self,
601        address: Address,
602        cursor: Option<ObjectId>,
603        limit: Option<usize>,
604        options: Option<IotaObjectDataOptions>,
605    ) -> RpcResult<ObjectsPage> {
606        let query = IotaObjectResponseQuery {
607            filter: Some(IotaObjectDataFilter::StructType(
608                NameRegistration::struct_tag(self.iota_names_config.package_address),
609            )),
610            options,
611        };
612
613        let owned_objects = self
614            .get_owned_objects(address, Some(query), cursor, limit)
615            .await?;
616
617        Ok(owned_objects)
618    }
619}
620
621impl<R: ReadApiServer> IotaRpcModule for IndexerApi<R> {
622    fn rpc(self) -> RpcModule<Self> {
623        self.into_rpc()
624    }
625
626    fn rpc_doc_module() -> Module {
627        IndexerApiOpenRpc::module_doc()
628    }
629}