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};
6
7use anyhow::anyhow;
8use async_trait::async_trait;
9use futures::future::try_join_all;
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, OwnedObjectCursor, Page,
21    TransactionBlocksPage,
22};
23use iota_metrics::spawn_monitored_blocking;
24use iota_names::{
25    IotaNamesNft, NameRegistration, config::IotaNamesConfig, error::IotaNamesError, name::Name,
26    registry::NameRecord,
27};
28use iota_open_rpc::Module;
29use iota_sdk_move_types::iota_framework::dynamic_field::Field;
30use iota_sdk_types::{Address, ObjectId, TransactionDigest, TypeTag};
31use iota_storage::key_value_store::TransactionKeyValueStore;
32use iota_types::{
33    base_types::ObjectInfo, dynamic_field::DynamicFieldName, error::UserInputError, event::EventID,
34    iota_sdk_types_conversions::type_tag_sdk_to_core, object::ObjectRead,
35};
36use jsonrpsee::{RpcModule, core::RpcResult};
37use move_bytecode_utils::layout::TypeLayoutBuilder;
38use tracing::instrument;
39
40use crate::{
41    IotaRpcModule,
42    authority_state::{StateRead, StateReadResult},
43    coin_api::cursor_position,
44    error::{Error, IotaRpcInputError},
45    logger::FutureWithTracing as _,
46    read_api::object_response,
47};
48
49pub struct IndexerApi<R> {
50    state: Arc<dyn StateRead>,
51    read_api: R,
52    transaction_kv_store: Arc<TransactionKeyValueStore>,
53    iota_names_config: IotaNamesConfig,
54    pub metrics: Arc<JsonRpcMetrics>,
55}
56
57impl<R: ReadApiServer> IndexerApi<R> {
58    pub fn new(
59        state: Arc<AuthorityState>,
60        read_api: R,
61        transaction_kv_store: Arc<TransactionKeyValueStore>,
62        metrics: Arc<JsonRpcMetrics>,
63        iota_names_config: IotaNamesConfig,
64    ) -> Self {
65        Self {
66            state,
67            transaction_kv_store,
68            read_api,
69            metrics,
70            iota_names_config,
71        }
72    }
73
74    fn extract_values_from_dynamic_field_name(
75        &self,
76        name: DynamicFieldName,
77    ) -> Result<(TypeTag, Vec<u8>), IotaRpcInputError> {
78        let DynamicFieldName {
79            type_tag: name_type,
80            value,
81        } = name;
82        let epoch_store = self.state.load_epoch_store_one_call_per_task();
83        let layout = TypeLayoutBuilder::build_with_types(
84            &type_tag_sdk_to_core(&name_type),
85            epoch_store.module_cache(),
86        )?;
87        let iota_json_value = IotaJsonValue::new(value)?;
88        let name_bcs_value = iota_json_value.to_bcs_bytes(&layout)?;
89        Ok((name_type, name_bcs_value))
90    }
91
92    async fn get_dynamic_field_object(
93        &self,
94        parent_object_id: ObjectId,
95        name: DynamicFieldName,
96        options: Option<IotaObjectDataOptions>,
97    ) -> RpcResult<IotaObjectResponse> {
98        async move {
99            let (name_type, name_bcs_value) = self.extract_values_from_dynamic_field_name(name)?;
100
101            let object = self
102                .state
103                .get_dynamic_field_object(parent_object_id, name_type, &name_bcs_value)
104                .map_err(Error::from)?;
105
106            if let Some(object) = object {
107                let layout = self.state.get_object_layout(&object)?;
108                object_response(
109                    &self.state,
110                    &self.transaction_kv_store,
111                    ObjectRead::Exists(object.object_ref(), object, layout),
112                    &options.unwrap_or_default(),
113                )
114                .await
115            } else {
116                Ok(IotaObjectResponse::new_with_error(
117                    IotaObjectResponseError::DynamicFieldNotFound { parent_object_id },
118                ))
119            }
120        }
121        .trace()
122        .await
123    }
124
125    fn get_latest_checkpoint_timestamp_ms(&self) -> StateReadResult<u64> {
126        let latest_checkpoint = self.state.get_latest_checkpoint_sequence_number()?;
127
128        let checkpoint = self
129            .state
130            .get_verified_checkpoint_by_sequence_number(latest_checkpoint)?;
131
132        Ok(checkpoint.timestamp_ms)
133    }
134}
135
136#[async_trait]
137impl<R: ReadApiServer> IndexerApiServer for IndexerApi<R> {
138    #[instrument(skip(self, address), fields(address = %address))]
139    async fn get_owned_objects(
140        &self,
141        address: Address,
142        query: Option<IotaObjectResponseQuery>,
143        cursor: Option<OwnedObjectCursor>,
144        limit: Option<usize>,
145    ) -> RpcResult<ObjectsPage> {
146        async move {
147            let limit =
148                validate_limit(limit, *QUERY_MAX_RESULT_LIMIT).map_err(IotaRpcInputError::from)?;
149            self.metrics.get_owned_objects_limit.observe(limit as f64);
150            let IotaObjectResponseQuery { filter, options } = query.unwrap_or_default();
151            if let Some(filter) = &filter {
152                filter.check_size().map_err(|error| {
153                    IotaRpcInputError::GenericInvalid(format!("invalid object filter: {error}"))
154                })?;
155            }
156            let options = options.unwrap_or_default();
157            let cursor = cursor_position(cursor)?;
158            let state = self.state.clone();
159            let page = spawn_monitored_blocking!(move || {
160                state.get_owner_objects_page(address, cursor, limit, filter)
161            })
162            .await??;
163
164            // From the objects the page read, at the version their rows name,
165            // so each response matches the ownership the page reports.
166            let data = match options.is_not_in_object_info() {
167                true => {
168                    let reads = page
169                        .data
170                        .into_iter()
171                        .map(|object| {
172                            let layout = self.state.get_object_layout(&object)?;
173                            Ok(ObjectRead::Exists(object.object_ref(), object, layout))
174                        })
175                        .collect::<Result<Vec<_>, Error>>()?;
176                    try_join_all(reads.into_iter().map(|read| {
177                        object_response(&self.state, &self.transaction_kv_store, read, &options)
178                    }))
179                    .await?
180                }
181                false => page
182                    .data
183                    .into_iter()
184                    .map(|object| {
185                        IotaObjectResponse::try_from((
186                            ObjectInfo::new(&object.object_ref(), &object),
187                            options.clone(),
188                        ))
189                    })
190                    .collect::<Result<Vec<IotaObjectResponse>, _>>()?,
191            };
192
193            self.metrics
194                .get_owned_objects_result_size
195                .observe(data.len() as f64);
196            self.metrics
197                .get_owned_objects_result_size_total
198                .inc_by(data.len() as u64);
199            Ok(Page {
200                data,
201                next_cursor: page.next_cursor,
202                has_next_page: page.has_next_page,
203                oldest_available_checkpoint: None,
204            })
205        }
206        .trace()
207        .await
208    }
209
210    #[instrument(skip(self))]
211    async fn query_transaction_blocks(
212        &self,
213        query: IotaTransactionBlockResponseQuery,
214        // If `Some`, the query will start from the next item after the specified cursor
215        cursor: Option<TransactionDigest>,
216        limit: Option<usize>,
217        descending_order: Option<bool>,
218    ) -> RpcResult<TransactionBlocksPage> {
219        async move {
220            let limit = cap_page_limit(limit);
221            self.metrics.query_tx_blocks_limit.observe(limit as f64);
222            let descending = descending_order.unwrap_or_default();
223            let opts = query.options.unwrap_or_default();
224
225            // Retrieve 1 extra item for next cursor
226            let mut digests = self
227                .state
228                .get_transactions(
229                    &self.transaction_kv_store,
230                    query.filter,
231                    cursor,
232                    Some(limit + 1),
233                    descending,
234                )
235                .await
236                .map_err(Error::from)?;
237            // De-dup digests, duplicate digests are possible, for example,
238            // when get_transactions_by_move_function with module or function being None.
239            let mut seen = HashSet::new();
240            digests.retain(|digest| seen.insert(*digest));
241
242            // extract next cursor
243            let has_next_page = digests.len() > limit;
244            digests.truncate(limit);
245            let next_cursor = digests.last().cloned().map_or(cursor, Some);
246
247            let data: Vec<IotaTransactionBlockResponse> = if opts.only_digest() {
248                digests
249                    .into_iter()
250                    .map(IotaTransactionBlockResponse::new)
251                    .collect()
252            } else {
253                self.read_api
254                    .multi_get_transaction_blocks(digests, Some(opts))
255                    .await
256                    .map_err(|e| Error::Internal(anyhow!(e)))?
257            };
258
259            self.metrics
260                .query_tx_blocks_result_size
261                .observe(data.len() as f64);
262            self.metrics
263                .query_tx_blocks_result_size_total
264                .inc_by(data.len() as u64);
265            Ok(Page {
266                data,
267                next_cursor,
268                has_next_page,
269                oldest_available_checkpoint: None,
270            })
271        }
272        .trace()
273        .await
274    }
275
276    #[instrument(skip(self))]
277    async fn query_transaction_blocks_v2(
278        &self,
279        query: IotaTransactionBlockResponseQueryV2,
280        // If `Some`, the query will start from the next item after the specified cursor
281        cursor: Option<TransactionDigest>,
282        limit: Option<usize>,
283        descending_order: Option<bool>,
284    ) -> RpcResult<TransactionBlocksPage> {
285        let v1_filter = query
286            .filter
287            .map(|f| {
288                f.as_v1().ok_or_else(|| {
289                    Error::UserInput(UserInputError::Unsupported(
290                        "transaction filter is not supported".to_string(),
291                    ))
292                })
293            })
294            .transpose()?;
295
296        let v1_query = IotaTransactionBlockResponseQuery {
297            filter: v1_filter,
298            options: query.options,
299        };
300        self.query_transaction_blocks(v1_query, cursor, limit, descending_order)
301            .await
302    }
303
304    #[instrument(skip(self))]
305    async fn query_events(
306        &self,
307        query: EventFilter,
308        // exclusive cursor if `Some`, otherwise start from the beginning
309        cursor: Option<EventID>,
310        limit: Option<usize>,
311        descending_order: Option<bool>,
312    ) -> RpcResult<EventPage> {
313        async move {
314            let descending = descending_order.unwrap_or_default();
315            let limit = cap_page_limit(limit);
316            self.metrics.query_events_limit.observe(limit as f64);
317            // Retrieve 1 extra item for next cursor
318            let mut data = self
319                .state
320                .query_events(
321                    &self.transaction_kv_store,
322                    query,
323                    cursor,
324                    limit + 1,
325                    descending,
326                )
327                .await
328                .map_err(Error::from)?;
329            let has_next_page = data.len() > limit;
330            data.truncate(limit);
331            let next_cursor = data.last().map_or(cursor, |e| Some(e.id));
332            self.metrics
333                .query_events_result_size
334                .observe(data.len() as f64);
335            self.metrics
336                .query_events_result_size_total
337                .inc_by(data.len() as u64);
338            Ok(EventPage {
339                data,
340                next_cursor,
341                has_next_page,
342                oldest_available_checkpoint: None,
343            })
344        }
345        .trace()
346        .await
347    }
348
349    #[instrument(skip(self, parent_object_id), fields(parent_object_id = %parent_object_id))]
350    async fn get_dynamic_fields(
351        &self,
352        parent_object_id: ObjectId,
353        // If `Some`, the query will start from the next item after the specified cursor
354        cursor: Option<ObjectId>,
355        limit: Option<usize>,
356    ) -> RpcResult<DynamicFieldPage> {
357        async move {
358            let limit = cap_page_limit(limit);
359            self.metrics.get_dynamic_fields_limit.observe(limit as f64);
360            // An unresolvable field (`None` info) advances the cursor but is
361            // omitted from the page. A page with none resolved is retried a
362            // bounded number of times, since clients may treat an empty page
363            // as the end.
364            const MAX_EMPTY_PAGE_RETRIES: usize = 3;
365            let mut next_cursor = cursor;
366            let mut has_next_page;
367            let mut resolved;
368            let mut retries = 0;
369            loop {
370                let mut data = self
371                    .state
372                    .get_dynamic_fields(parent_object_id, next_cursor, limit + 1)
373                    .map_err(Error::from)?;
374                has_next_page = data.len() > limit;
375                data.truncate(limit);
376                next_cursor = data.last().map_or(next_cursor, |c| Some(c.0));
377                resolved = data
378                    .into_iter()
379                    .filter_map(|(_, info)| Some(info?.into()))
380                    .collect::<Vec<_>>();
381                if !resolved.is_empty() || !has_next_page || retries >= MAX_EMPTY_PAGE_RETRIES {
382                    break;
383                }
384                retries += 1;
385            }
386            self.metrics
387                .get_dynamic_fields_result_size
388                .observe(resolved.len() as f64);
389            self.metrics
390                .get_dynamic_fields_result_size_total
391                .inc_by(resolved.len() as u64);
392            Ok(DynamicFieldPage {
393                data: resolved,
394                next_cursor,
395                has_next_page,
396                oldest_available_checkpoint: None,
397            })
398        }
399        .trace()
400        .await
401    }
402
403    #[instrument(skip(self, parent_object_id), fields(parent_object_id = %parent_object_id))]
404    async fn get_dynamic_field_object(
405        &self,
406        parent_object_id: ObjectId,
407        name: DynamicFieldName,
408    ) -> RpcResult<IotaObjectResponse> {
409        self.get_dynamic_field_object(
410            parent_object_id,
411            name,
412            Some(IotaObjectDataOptions::full_content()),
413        )
414        .await
415    }
416
417    #[instrument(skip(self, parent_object_id), fields(parent_object_id = %parent_object_id))]
418    async fn get_dynamic_field_object_v2(
419        &self,
420        parent_object_id: ObjectId,
421        name: DynamicFieldName,
422        options: Option<IotaObjectDataOptions>,
423    ) -> RpcResult<IotaObjectResponse> {
424        self.get_dynamic_field_object(parent_object_id, name, options)
425            .await
426    }
427
428    async fn iota_names_lookup(&self, name: &str) -> RpcResult<Option<IotaNameRecord>> {
429        let name = name.parse::<Name>().map_err(Error::from)?;
430
431        // Construct the record id to lookup.
432        let record_id = self.iota_names_config.record_field_id(&name);
433
434        let parent_record_id = name
435            .parent()
436            .map(|parent_name| self.iota_names_config.record_field_id(&parent_name));
437
438        // Keep record IDs alive by declaring both before creating futures
439        let mut requests = vec![self.state.get_object(&record_id)];
440
441        // We only want to fetch both the child and the parent if the name is a
442        // subname.
443        if let Some(ref parent_record_id) = parent_record_id {
444            requests.push(self.state.get_object(parent_record_id));
445        }
446
447        // Couldn't find a `multi_get_object` for this crate (looks like it uses a k,v
448        // db) Always fetching both parent + child at the same time (even for
449        // node subnames), to avoid sequential db reads. We do this because we
450        // do not know if the requested name is a node subname or a leaf
451        // subname, and we can save a trip to the db.
452        let mut results = futures::future::try_join_all(requests)
453            .await
454            .map_err(Error::from)?;
455
456        // Removing without checking vector len, since it is known (== 1 or 2 depending
457        // on whether it is a subname or not).
458        let Some(object) = results.remove(0) else {
459            return Ok(None);
460        };
461
462        let name_record = NameRecord::try_from(object).map_err(Error::from)?;
463
464        let current_timestamp_ms = self
465            .get_latest_checkpoint_timestamp_ms()
466            .map_err(Error::from)?;
467
468        // Handling second-level names & node subnames is the same (we handle them as
469        // `node` records). We check their expiration, and if not expired,
470        // return the target address.
471        if !name_record.is_leaf_record() {
472            return if !name_record.is_node_expired(current_timestamp_ms) {
473                Ok(Some(name_record.into()))
474            } else {
475                Err(Error::from(IotaNamesError::NameExpired).into())
476            };
477        } else {
478            // Handle the `leaf` record case which requires to check the parent for
479            // expiration. We can remove since we know that if we're here, we have a parent
480            // result for the parent request. If the parent result is `None` for the
481            // existing leaf record, we consider it expired.
482            let Some(parent_object) = results.remove(0) else {
483                return Err(Error::from(IotaNamesError::NameExpired).into());
484            };
485
486            let parent_name_record = NameRecord::try_from(parent_object).map_err(Error::from)?;
487
488            // For a leaf record, we check that:
489            // 1. The parent is a valid parent for that leaf record
490            // 2. The parent is not expired
491            if parent_name_record.is_valid_leaf_parent(&name_record)
492                && !parent_name_record.is_node_expired(current_timestamp_ms)
493            {
494                Ok(Some(name_record.into()))
495            } else {
496                Err(Error::from(IotaNamesError::NameExpired).into())
497            }
498        }
499    }
500
501    #[instrument(skip(self, address), fields(address = %address))]
502    async fn iota_names_reverse_lookup(&self, address: Address) -> RpcResult<Option<String>> {
503        let reverse_record_id = self.iota_names_config.reverse_record_field_id(&address);
504
505        let Some(field_reverse_record_object) = self
506            .state
507            .get_object(&reverse_record_id)
508            .await
509            .map_err(Error::from)?
510        else {
511            return Ok(None);
512        };
513
514        let name = field_reverse_record_object
515            .to_rust::<Field<Address, Name>>()
516            .map_err(|e| Error::Unexpected(format!("malformed Object {reverse_record_id}: {e}")))?
517            .value;
518
519        let name = name.to_string();
520
521        let resolved_record = self.iota_names_lookup(&name).await?;
522
523        // If looking up the name returns an empty result, we return an empty result.
524        if resolved_record.is_none() {
525            return Ok(None);
526        }
527
528        Ok(Some(name))
529    }
530
531    #[instrument(skip(self, address), fields(address = %address))]
532    async fn iota_names_find_all_registration_nfts(
533        &self,
534        address: Address,
535        cursor: Option<OwnedObjectCursor>,
536        limit: Option<usize>,
537        options: Option<IotaObjectDataOptions>,
538    ) -> RpcResult<ObjectsPage> {
539        let query = IotaObjectResponseQuery {
540            filter: Some(IotaObjectDataFilter::StructType(
541                NameRegistration::struct_tag(self.iota_names_config.package_address),
542            )),
543            options,
544        };
545
546        let owned_objects = self
547            .get_owned_objects(address, Some(query), cursor, limit)
548            .await?;
549
550        Ok(owned_objects)
551    }
552}
553
554impl<R: ReadApiServer> IotaRpcModule for IndexerApi<R> {
555    fn rpc(self) -> RpcModule<Self> {
556        self.into_rpc()
557    }
558
559    fn rpc_doc_module() -> Module {
560        IndexerApiOpenRpc::module_doc()
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use iota_json_rpc_types::MAX_FILTER_NODES;
567    use iota_sdk_types::{CheckpointDigest, TransactionEvents, Version};
568    use iota_storage::{
569        key_value_store::{
570            KVStoreCheckpointData, KVStoreTransactionData, TransactionKeyValueStoreTrait,
571        },
572        key_value_store_metrics::KeyValueStoreMetrics,
573    };
574    use iota_types::{
575        error::IotaResult, messages_checkpoint::CheckpointSequenceNumber, object::Object,
576        storage::ObjectKey,
577    };
578    use jsonrpsee::{core::server::MethodsError, types::error::INVALID_PARAMS_CODE};
579    use mockall::mock;
580
581    use super::*;
582    use crate::{authority_state::MockStateRead, read_api::ReadApi};
583
584    mock! {
585        KeyValueStore {}
586        #[async_trait]
587        impl TransactionKeyValueStoreTrait for KeyValueStore {
588            async fn multi_get(
589                &self,
590                transaction_keys: &[TransactionDigest],
591                effects_keys: &[TransactionDigest],
592            ) -> IotaResult<KVStoreTransactionData>;
593            async fn multi_get_checkpoints(
594                &self,
595                checkpoint_summaries: &[CheckpointSequenceNumber],
596                checkpoint_contents: &[CheckpointSequenceNumber],
597                checkpoint_summaries_by_digest: &[CheckpointDigest],
598            ) -> IotaResult<KVStoreCheckpointData>;
599            async fn get_transaction_perpetual_checkpoint(
600                &self,
601                digest: TransactionDigest,
602            ) -> IotaResult<Option<CheckpointSequenceNumber>>;
603            async fn get_object(
604                &self,
605                object_id: ObjectId,
606                version: Version,
607            ) -> IotaResult<Option<Object>>;
608            async fn multi_get_objects(
609                &self,
610                object_keys: &[ObjectKey],
611            ) -> IotaResult<Vec<Option<Object>>>;
612            async fn multi_get_transactions_perpetual_checkpoints(
613                &self,
614                digests: &[TransactionDigest],
615            ) -> IotaResult<Vec<Option<CheckpointSequenceNumber>>>;
616            async fn multi_get_events_by_tx_digests(
617                &self,
618                digests: &[TransactionDigest],
619            ) -> IotaResult<Vec<Option<TransactionEvents>>>;
620        }
621    }
622
623    fn indexer_rpc_module() -> RpcModule<IndexerApi<ReadApi>> {
624        let state: Arc<dyn StateRead> = Arc::new(MockStateRead::new());
625        let transaction_kv_store = Arc::new(TransactionKeyValueStore::new(
626            "test",
627            KeyValueStoreMetrics::new_for_tests(),
628            Arc::new(MockKeyValueStore::new()),
629        ));
630        let metrics = Arc::new(JsonRpcMetrics::new_for_tests());
631        IndexerApi {
632            read_api: ReadApi {
633                state: state.clone(),
634                transaction_kv_store: transaction_kv_store.clone(),
635                metrics: metrics.clone(),
636            },
637            state,
638            transaction_kv_store,
639            metrics,
640            iota_names_config: IotaNamesConfig::default(),
641        }
642        .rpc()
643    }
644
645    fn assert_invalid_params(result: Result<impl std::fmt::Debug, MethodsError>, limit: &str) {
646        match result {
647            Err(MethodsError::JsonRpc(error)) => {
648                assert_eq!(error.code(), INVALID_PARAMS_CODE);
649                assert!(
650                    error.message().contains(limit),
651                    "message `{}` does not name the limit `{limit}`",
652                    error.message()
653                );
654            }
655            other => panic!("expected an invalid params error, got {other:?}"),
656        }
657    }
658
659    #[tokio::test]
660    async fn get_owned_objects_refuses_too_large_filter() {
661        let module = indexer_rpc_module();
662        let query = IotaObjectResponseQuery::new(
663            Some(IotaObjectDataFilter::ObjectIds(vec![
664                ObjectId::ZERO;
665                MAX_FILTER_NODES
666            ])),
667            None,
668        );
669        assert_invalid_params(
670            module
671                .call::<_, ObjectsPage>(
672                    "iotax_getOwnedObjects",
673                    (Address::ZERO, query, None::<ObjectId>, None::<usize>),
674                )
675                .await,
676            &format!("{MAX_FILTER_NODES} nodes"),
677        );
678    }
679}