1use 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 SendTimeoutError::Closed(_) => break Ok(()),
73 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 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 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 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 let mut seen = HashSet::new();
291 digests.retain(|digest| seen.insert(*digest));
292
293 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 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 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 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 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 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 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 let mut requests = vec![self.state.get_object(&record_id)];
507
508 if let Some(ref parent_record_id) = parent_record_id {
511 requests.push(self.state.get_object(parent_record_id));
512 }
513
514 let mut results = futures::future::try_join_all(requests)
520 .await
521 .map_err(Error::from)?;
522
523 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 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 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 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 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}