1use 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 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 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 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 let mut seen = HashSet::new();
240 digests.retain(|digest| seen.insert(*digest));
241
242 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 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 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 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 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 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 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 let mut requests = vec![self.state.get_object(&record_id)];
440
441 if let Some(ref parent_record_id) = parent_record_id {
444 requests.push(self.state.get_object(parent_record_id));
445 }
446
447 let mut results = futures::future::try_join_all(requests)
453 .await
454 .map_err(Error::from)?;
455
456 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 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 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 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 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}