iota_rest_api/
system.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::BTreeMap;
6
7use axum::{
8    Json,
9    extract::{Path, State},
10};
11use iota_protocol_config::{ProtocolConfig, ProtocolConfigValue, ProtocolVersion};
12use iota_sdk2::types::{Address, ObjectId};
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15
16use crate::{
17    RestError, RestService, Result,
18    accept::AcceptFormat,
19    openapi::{ApiEndpoint, OperationBuilder, ResponseBuilder, RouteHandler},
20    reader::StateReader,
21};
22
23pub struct GetSystemStateSummary;
24
25impl ApiEndpoint<RestService> for GetSystemStateSummary {
26    fn method(&self) -> axum::http::Method {
27        axum::http::Method::GET
28    }
29
30    fn path(&self) -> &'static str {
31        "/system"
32    }
33
34    fn operation(
35        &self,
36        generator: &mut schemars::gen::SchemaGenerator,
37    ) -> openapiv3::v3_1::Operation {
38        OperationBuilder::new()
39            .tag("System")
40            .operation_id("GetSystemStateSummary")
41            .response(
42                200,
43                ResponseBuilder::new()
44                    .json_content::<SystemStateSummary>(generator)
45                    .build(),
46            )
47            .build()
48    }
49
50    fn handler(&self) -> RouteHandler<RestService> {
51        RouteHandler::new(self.method(), get_system_state_summary)
52    }
53}
54
55async fn get_system_state_summary(
56    accept: AcceptFormat,
57    State(state): State<StateReader>,
58) -> Result<Json<SystemStateSummary>> {
59    match accept {
60        AcceptFormat::Json => {}
61        _ => {
62            return Err(RestError::new(
63                axum::http::StatusCode::BAD_REQUEST,
64                "invalid accept type",
65            ));
66        }
67    }
68
69    let summary = state.get_system_state_summary()?;
70
71    Ok(Json(summary))
72}
73
74#[serde_with::serde_as]
75#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
76pub struct SystemStateSummary {
77    /// The current epoch ID, starting from 0.
78    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
79    #[schemars(with = "crate::_schemars::U64")]
80    pub epoch: u64,
81    /// The current protocol version, starting from 1.
82    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
83    #[schemars(with = "crate::_schemars::U64")]
84    pub protocol_version: u64,
85    /// The current version of the system state data structure type.
86    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
87    #[schemars(with = "crate::_schemars::U64")]
88    pub system_state_version: u64,
89    /// The current IOTA supply.
90    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
91    #[schemars(with = "crate::_schemars::U64")]
92    pub iota_total_supply: u64,
93    /// The `TreasuryCap<IOTA>` object ID.
94    pub iota_treasury_cap_id: ObjectId,
95    /// The storage rebates of all the objects on-chain stored in the storage
96    /// fund.
97    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
98    #[schemars(with = "crate::_schemars::U64")]
99    pub storage_fund_total_object_storage_rebates: u64,
100    /// The non-refundable portion of the storage fund coming from storage
101    /// reinvestment, non-refundable storage rebates and any leftover
102    /// staking rewards.
103    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
104    #[schemars(with = "crate::_schemars::U64")]
105    pub storage_fund_non_refundable_balance: u64,
106    /// The reference gas price for the current epoch.
107    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
108    #[schemars(with = "crate::_schemars::U64")]
109    pub reference_gas_price: u64,
110    /// Whether the system is running in a downgraded safe mode due to a
111    /// non-recoverable bug. This is set whenever we failed to execute
112    /// advance_epoch, and ended up executing advance_epoch_safe_mode.
113    /// It can be reset once we are able to successfully execute advance_epoch.
114    pub safe_mode: bool,
115    /// Amount of storage charges accumulated (and not yet distributed) during
116    /// safe mode.
117    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
118    #[schemars(with = "crate::_schemars::U64")]
119    pub safe_mode_storage_charges: u64,
120    /// Amount of computation charges accumulated (and not yet distributed)
121    /// during safe mode.
122    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
123    #[schemars(with = "crate::_schemars::U64")]
124    pub safe_mode_computation_charges: u64,
125    /// Amount of burned computation charges accumulated during safe mode.
126    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
127    #[schemars(with = "crate::_schemars::U64")]
128    pub safe_mode_computation_charges_burned: u64,
129    /// Amount of storage rebates accumulated (and not yet burned) during safe
130    /// mode.
131    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
132    #[schemars(with = "crate::_schemars::U64")]
133    pub safe_mode_storage_rebates: u64,
134    /// Amount of non-refundable storage fee accumulated during safe mode.
135    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
136    #[schemars(with = "crate::_schemars::U64")]
137    pub safe_mode_non_refundable_storage_fee: u64,
138    /// Unix timestamp of the current epoch start
139    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
140    #[schemars(with = "crate::_schemars::U64")]
141    pub epoch_start_timestamp_ms: u64,
142
143    // System parameters
144    /// The duration of an epoch, in milliseconds.
145    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
146    #[schemars(with = "crate::_schemars::U64")]
147    pub epoch_duration_ms: u64,
148
149    /// Minimum number of active validators at any moment.
150    /// We do not allow the number of validators in any epoch to go under this.
151    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
152    #[schemars(with = "crate::_schemars::U64")]
153    pub min_validator_count: u64,
154
155    /// Maximum number of active validators at any moment.
156    /// We do not allow the number of validators in any epoch to go above this.
157    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
158    #[schemars(with = "crate::_schemars::U64")]
159    pub max_validator_count: u64,
160
161    /// Lower-bound on the amount of stake required to become a validator.
162    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
163    #[schemars(with = "crate::_schemars::U64")]
164    pub min_validator_joining_stake: u64,
165
166    /// Validators with stake amount below `validator_low_stake_threshold` are
167    /// considered to have low stake and will be escorted out of the
168    /// validator set after being below this threshold for more than
169    /// `validator_low_stake_grace_period` number of epochs.
170    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
171    #[schemars(with = "crate::_schemars::U64")]
172    pub validator_low_stake_threshold: u64,
173
174    /// Validators with stake below `validator_very_low_stake_threshold` will be
175    /// removed immediately at epoch change, no grace period.
176    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
177    #[schemars(with = "crate::_schemars::U64")]
178    pub validator_very_low_stake_threshold: u64,
179
180    /// A validator can have stake below `validator_low_stake_threshold`
181    /// for this many epochs before being kicked out.
182    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
183    #[schemars(with = "crate::_schemars::U64")]
184    pub validator_low_stake_grace_period: u64,
185
186    // Validator set
187    /// Total amount of stake from all committee validators at the beginning of
188    /// the epoch.
189    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
190    #[schemars(with = "crate::_schemars::U64")]
191    pub total_stake: u64,
192    /// List of committee validators in the current epoch. Each element is an
193    /// index pointing to `active_validators`.
194    #[serde_as(as = "Vec<iota_types::iota_serde::BigInt<u64>>")]
195    #[schemars(with = "Vec<crate::_schemars::U64>")]
196    pub committee_members: Vec<u64>,
197    /// The list of active validators in the current epoch.
198    pub active_validators: Vec<ValidatorSummary>,
199    /// ID of the object that contains the list of new validators that will join
200    /// at the end of the epoch.
201    pub pending_active_validators_id: ObjectId,
202    /// Number of new validators that will join at the end of the epoch.
203    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
204    #[schemars(with = "crate::_schemars::U64")]
205    pub pending_active_validators_size: u64,
206    /// Removal requests from the validators. Each element is an index
207    /// pointing to `active_validators`.
208    #[serde_as(as = "Vec<iota_types::iota_serde::BigInt<u64>>")]
209    #[schemars(with = "Vec<crate::_schemars::U64>")]
210    pub pending_removals: Vec<u64>,
211    /// ID of the object that maps from staking pool's ID to the iota address of
212    /// a validator.
213    pub staking_pool_mappings_id: ObjectId,
214    /// Number of staking pool mappings.
215    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
216    #[schemars(with = "crate::_schemars::U64")]
217    pub staking_pool_mappings_size: u64,
218    /// ID of the object that maps from a staking pool ID to the inactive
219    /// validator that has that pool as its staking pool.
220    pub inactive_pools_id: ObjectId,
221    /// Number of inactive staking pools.
222    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
223    #[schemars(with = "crate::_schemars::U64")]
224    pub inactive_pools_size: u64,
225    /// ID of the object that stores preactive validators, mapping their
226    /// addresses to their `Validator` structs.
227    pub validator_candidates_id: ObjectId,
228    /// Number of preactive validators.
229    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
230    #[schemars(with = "crate::_schemars::U64")]
231    pub validator_candidates_size: u64,
232    /// Map storing the number of epochs for which each validator has been below
233    /// the low stake threshold.
234    #[serde_as(as = "Vec<(_, iota_types::iota_serde::BigInt<u64>)>")]
235    #[schemars(with = "Vec<(Address, crate::_schemars::U64)>")]
236    pub at_risk_validators: Vec<(Address, u64)>,
237    /// A map storing the records of validator reporting each other.
238    pub validator_report_records: Vec<(Address, Vec<Address>)>,
239}
240
241/// This is the REST type for the iota validator. It flattens all inner
242/// structures to top-level fields so that they are decoupled from the internal
243/// definitions.
244#[serde_with::serde_as]
245#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
246pub struct ValidatorSummary {
247    // Metadata
248    pub address: Address,
249    pub authority_public_key: iota_sdk2::types::Bls12381PublicKey,
250    pub network_public_key: iota_sdk2::types::Ed25519PublicKey,
251    pub protocol_public_key: iota_sdk2::types::Ed25519PublicKey,
252    #[serde_as(as = "fastcrypto::encoding::Base64")]
253    #[schemars(with = "String")]
254    pub proof_of_possession_bytes: Vec<u8>,
255    pub name: String,
256    pub description: String,
257    pub image_url: String,
258    pub project_url: String,
259    pub net_address: String,
260    pub p2p_address: String,
261    pub primary_address: String,
262    pub next_epoch_authority_public_key: Option<iota_sdk2::types::Bls12381PublicKey>,
263    pub next_epoch_network_public_key: Option<iota_sdk2::types::Ed25519PublicKey>,
264    pub next_epoch_protocol_public_key: Option<iota_sdk2::types::Ed25519PublicKey>,
265    #[serde_as(as = "Option<fastcrypto::encoding::Base64>")]
266    #[schemars(with = "Option<String>")]
267    pub next_epoch_proof_of_possession: Option<Vec<u8>>,
268    pub next_epoch_net_address: Option<String>,
269    pub next_epoch_p2p_address: Option<String>,
270    pub next_epoch_primary_address: Option<String>,
271
272    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
273    #[schemars(with = "crate::_schemars::U64")]
274    pub voting_power: u64,
275    pub operation_cap_id: ObjectId,
276    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
277    #[schemars(with = "crate::_schemars::U64")]
278    pub gas_price: u64,
279    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
280    #[schemars(with = "crate::_schemars::U64")]
281    pub commission_rate: u64,
282    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
283    #[schemars(with = "crate::_schemars::U64")]
284    pub next_epoch_stake: u64,
285    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
286    #[schemars(with = "crate::_schemars::U64")]
287    pub next_epoch_gas_price: u64,
288    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
289    #[schemars(with = "crate::_schemars::U64")]
290    pub next_epoch_commission_rate: u64,
291
292    // Staking pool information
293    /// ID of the staking pool object.
294    pub staking_pool_id: ObjectId,
295    /// The epoch at which this pool became active.
296    #[serde_as(as = "Option<iota_types::iota_serde::BigInt<u64>>")]
297    #[schemars(with = "Option<crate::_schemars::U64>")]
298    pub staking_pool_activation_epoch: Option<u64>,
299    /// The epoch at which this staking pool ceased to be active. `None` =
300    /// {pre-active, active},
301    #[serde_as(as = "Option<iota_types::iota_serde::BigInt<u64>>")]
302    #[schemars(with = "Option<crate::_schemars::U64>")]
303    pub staking_pool_deactivation_epoch: Option<u64>,
304    /// The total number of IOTA tokens in this pool.
305    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
306    #[schemars(with = "crate::_schemars::U64")]
307    pub staking_pool_iota_balance: u64,
308    /// The epoch stake rewards will be added here at the end of each epoch.
309    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
310    #[schemars(with = "crate::_schemars::U64")]
311    pub rewards_pool: u64,
312    /// Total number of pool tokens issued by the pool.
313    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
314    #[schemars(with = "crate::_schemars::U64")]
315    pub pool_token_balance: u64,
316    /// Pending stake amount for this epoch.
317    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
318    #[schemars(with = "crate::_schemars::U64")]
319    pub pending_stake: u64,
320    /// Pending stake withdrawn during the current epoch, emptied at epoch
321    /// boundaries.
322    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
323    #[schemars(with = "crate::_schemars::U64")]
324    pub pending_total_iota_withdraw: u64,
325    /// Pending pool token withdrawn during the current epoch, emptied at epoch
326    /// boundaries.
327    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
328    #[schemars(with = "crate::_schemars::U64")]
329    pub pending_pool_token_withdraw: u64,
330    /// ID of the exchange rate table object.
331    pub exchange_rates_id: ObjectId,
332    /// Number of exchange rates in the table.
333    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
334    #[schemars(with = "crate::_schemars::U64")]
335    pub exchange_rates_size: u64,
336}
337
338impl From<iota_types::iota_system_state::iota_system_state_summary::IotaValidatorSummary>
339    for ValidatorSummary
340{
341    fn from(
342        value: iota_types::iota_system_state::iota_system_state_summary::IotaValidatorSummary,
343    ) -> Self {
344        let iota_types::iota_system_state::iota_system_state_summary::IotaValidatorSummary {
345            iota_address,
346            authority_pubkey_bytes,
347            network_pubkey_bytes,
348            protocol_pubkey_bytes,
349            proof_of_possession_bytes,
350            name,
351            description,
352            image_url,
353            project_url,
354            net_address,
355            p2p_address,
356            primary_address,
357            next_epoch_authority_pubkey_bytes,
358            next_epoch_proof_of_possession,
359            next_epoch_network_pubkey_bytes,
360            next_epoch_protocol_pubkey_bytes,
361            next_epoch_net_address,
362            next_epoch_p2p_address,
363            next_epoch_primary_address,
364            voting_power,
365            operation_cap_id,
366            gas_price,
367            commission_rate,
368            next_epoch_stake,
369            next_epoch_gas_price,
370            next_epoch_commission_rate,
371            staking_pool_id,
372            staking_pool_activation_epoch,
373            staking_pool_deactivation_epoch,
374            staking_pool_iota_balance,
375            rewards_pool,
376            pool_token_balance,
377            pending_stake,
378            pending_total_iota_withdraw,
379            pending_pool_token_withdraw,
380            exchange_rates_id,
381            exchange_rates_size,
382        } = value;
383
384        Self {
385            address: iota_address.into(),
386            authority_public_key: iota_sdk2::types::Bls12381PublicKey::from_bytes(
387                authority_pubkey_bytes,
388            )
389            .unwrap(),
390            network_public_key: iota_sdk2::types::Ed25519PublicKey::from_bytes(
391                network_pubkey_bytes,
392            )
393            .unwrap(),
394            protocol_public_key: iota_sdk2::types::Ed25519PublicKey::from_bytes(
395                protocol_pubkey_bytes,
396            )
397            .unwrap(),
398            proof_of_possession_bytes,
399            name,
400            description,
401            image_url,
402            project_url,
403            net_address,
404            p2p_address,
405            primary_address,
406            next_epoch_authority_public_key: next_epoch_authority_pubkey_bytes
407                .map(|bytes| iota_sdk2::types::Bls12381PublicKey::from_bytes(bytes).unwrap()),
408            next_epoch_network_public_key: next_epoch_network_pubkey_bytes
409                .map(|bytes| iota_sdk2::types::Ed25519PublicKey::from_bytes(bytes).unwrap()),
410            next_epoch_protocol_public_key: next_epoch_protocol_pubkey_bytes
411                .map(|bytes| iota_sdk2::types::Ed25519PublicKey::from_bytes(bytes).unwrap()),
412            next_epoch_proof_of_possession,
413            next_epoch_net_address,
414            next_epoch_p2p_address,
415            next_epoch_primary_address,
416            voting_power,
417            operation_cap_id: operation_cap_id.into(),
418            gas_price,
419            commission_rate,
420            next_epoch_stake,
421            next_epoch_gas_price,
422            next_epoch_commission_rate,
423            staking_pool_id: staking_pool_id.into(),
424            staking_pool_activation_epoch,
425            staking_pool_deactivation_epoch,
426            staking_pool_iota_balance,
427            rewards_pool,
428            pool_token_balance,
429            pending_stake,
430            pending_total_iota_withdraw,
431            pending_pool_token_withdraw,
432            exchange_rates_id: exchange_rates_id.into(),
433            exchange_rates_size,
434        }
435    }
436}
437
438impl From<iota_types::iota_system_state::iota_system_state_summary::IotaSystemStateSummaryV2>
439    for SystemStateSummary
440{
441    fn from(
442        value: iota_types::iota_system_state::iota_system_state_summary::IotaSystemStateSummaryV2,
443    ) -> Self {
444        let iota_types::iota_system_state::iota_system_state_summary::IotaSystemStateSummaryV2 {
445            epoch,
446            protocol_version,
447            system_state_version,
448            iota_total_supply,
449            iota_treasury_cap_id,
450            storage_fund_total_object_storage_rebates,
451            storage_fund_non_refundable_balance,
452            reference_gas_price,
453            safe_mode,
454            safe_mode_storage_charges,
455            safe_mode_computation_charges,
456            safe_mode_computation_charges_burned,
457            safe_mode_storage_rebates,
458            safe_mode_non_refundable_storage_fee,
459            epoch_start_timestamp_ms,
460            epoch_duration_ms,
461            min_validator_count,
462            max_validator_count,
463            min_validator_joining_stake,
464            validator_low_stake_threshold,
465            validator_very_low_stake_threshold,
466            validator_low_stake_grace_period,
467            total_stake,
468            committee_members,
469            active_validators,
470            pending_active_validators_id,
471            pending_active_validators_size,
472            pending_removals,
473            staking_pool_mappings_id,
474            staking_pool_mappings_size,
475            inactive_pools_id,
476            inactive_pools_size,
477            validator_candidates_id,
478            validator_candidates_size,
479            at_risk_validators,
480            validator_report_records,
481        } = value;
482
483        Self {
484            epoch,
485            protocol_version,
486            system_state_version,
487            iota_total_supply,
488            iota_treasury_cap_id: iota_treasury_cap_id.into(),
489            storage_fund_total_object_storage_rebates,
490            storage_fund_non_refundable_balance,
491            reference_gas_price,
492            safe_mode,
493            safe_mode_storage_charges,
494            safe_mode_computation_charges,
495            safe_mode_computation_charges_burned,
496            safe_mode_storage_rebates,
497            safe_mode_non_refundable_storage_fee,
498            epoch_start_timestamp_ms,
499            epoch_duration_ms,
500            min_validator_count,
501            max_validator_count,
502            min_validator_joining_stake,
503            validator_low_stake_threshold,
504            validator_very_low_stake_threshold,
505            validator_low_stake_grace_period,
506            total_stake,
507            committee_members,
508            active_validators: active_validators.into_iter().map(Into::into).collect(),
509            pending_active_validators_id: pending_active_validators_id.into(),
510            pending_active_validators_size,
511            pending_removals,
512            staking_pool_mappings_id: staking_pool_mappings_id.into(),
513            staking_pool_mappings_size,
514            inactive_pools_id: inactive_pools_id.into(),
515            inactive_pools_size,
516            validator_candidates_id: validator_candidates_id.into(),
517            validator_candidates_size,
518            at_risk_validators: at_risk_validators
519                .into_iter()
520                .map(|(address, idx)| (address.into(), idx))
521                .collect(),
522            validator_report_records: validator_report_records
523                .into_iter()
524                .map(|(address, reports)| {
525                    (
526                        address.into(),
527                        reports.into_iter().map(Into::into).collect(),
528                    )
529                })
530                .collect(),
531        }
532    }
533}
534
535pub struct GetCurrentProtocolConfig;
536
537impl ApiEndpoint<RestService> for GetCurrentProtocolConfig {
538    fn method(&self) -> axum::http::Method {
539        axum::http::Method::GET
540    }
541
542    fn path(&self) -> &'static str {
543        "/system/protocol"
544    }
545
546    fn operation(
547        &self,
548        generator: &mut schemars::gen::SchemaGenerator,
549    ) -> openapiv3::v3_1::Operation {
550        OperationBuilder::new()
551            .tag("System")
552            .operation_id("GetCurrentProtocolConfig")
553            .response(
554                200,
555                ResponseBuilder::new()
556                    .json_content::<ProtocolConfigResponse>(generator)
557                    .header::<String>(X_IOTA_MIN_SUPPORTED_PROTOCOL_VERSION, generator)
558                    .header::<String>(X_IOTA_MAX_SUPPORTED_PROTOCOL_VERSION, generator)
559                    .build(),
560            )
561            .build()
562    }
563
564    fn handler(&self) -> RouteHandler<RestService> {
565        RouteHandler::new(self.method(), get_current_protocol_config)
566    }
567}
568
569async fn get_current_protocol_config(
570    accept: AcceptFormat,
571    State(state): State<StateReader>,
572) -> Result<(SupportedProtocolHeaders, Json<ProtocolConfigResponse>)> {
573    match accept {
574        AcceptFormat::Json => {}
575        _ => {
576            return Err(RestError::new(
577                axum::http::StatusCode::BAD_REQUEST,
578                "invalid accept type",
579            ));
580        }
581    }
582
583    let current_protocol_version = state.get_system_state_summary()?.protocol_version;
584
585    let config = ProtocolConfig::get_for_version_if_supported(
586        current_protocol_version.into(),
587        state.inner().get_chain_identifier()?.chain(),
588    )
589    .ok_or_else(|| ProtocolNotFoundError::new(current_protocol_version))?;
590
591    Ok((supported_protocol_headers(), Json(config.into())))
592}
593
594pub struct GetProtocolConfig;
595
596impl ApiEndpoint<RestService> for GetProtocolConfig {
597    fn method(&self) -> axum::http::Method {
598        axum::http::Method::GET
599    }
600
601    fn path(&self) -> &'static str {
602        "/system/protocol/{version}"
603    }
604
605    fn operation(
606        &self,
607        generator: &mut schemars::gen::SchemaGenerator,
608    ) -> openapiv3::v3_1::Operation {
609        OperationBuilder::new()
610            .tag("System")
611            .operation_id("GetProtocolConfig")
612            .path_parameter::<u64>("version", generator)
613            .response(
614                200,
615                ResponseBuilder::new()
616                    .json_content::<ProtocolConfigResponse>(generator)
617                    .header::<String>(X_IOTA_MIN_SUPPORTED_PROTOCOL_VERSION, generator)
618                    .header::<String>(X_IOTA_MAX_SUPPORTED_PROTOCOL_VERSION, generator)
619                    .build(),
620            )
621            .response(404, ResponseBuilder::new().build())
622            .build()
623    }
624
625    fn handler(&self) -> RouteHandler<RestService> {
626        RouteHandler::new(self.method(), get_protocol_config)
627    }
628}
629
630async fn get_protocol_config(
631    Path(version): Path<u64>,
632    accept: AcceptFormat,
633    State(state): State<StateReader>,
634) -> Result<(SupportedProtocolHeaders, Json<ProtocolConfigResponse>)> {
635    match accept {
636        AcceptFormat::Json => {}
637        _ => {
638            return Err(RestError::new(
639                axum::http::StatusCode::BAD_REQUEST,
640                "invalid accept type",
641            ));
642        }
643    }
644
645    let config = ProtocolConfig::get_for_version_if_supported(
646        version.into(),
647        state.inner().get_chain_identifier()?.chain(),
648    )
649    .ok_or_else(|| ProtocolNotFoundError::new(version))?;
650
651    Ok((supported_protocol_headers(), Json(config.into())))
652}
653
654#[derive(Debug)]
655pub struct ProtocolNotFoundError {
656    version: u64,
657}
658
659impl ProtocolNotFoundError {
660    pub fn new(version: u64) -> Self {
661        Self { version }
662    }
663}
664
665impl std::fmt::Display for ProtocolNotFoundError {
666    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
667        write!(f, "Protocol version {} not found", self.version)
668    }
669}
670
671impl std::error::Error for ProtocolNotFoundError {}
672
673impl From<ProtocolNotFoundError> for crate::RestError {
674    fn from(value: ProtocolNotFoundError) -> Self {
675        Self::new(axum::http::StatusCode::NOT_FOUND, value.to_string())
676    }
677}
678
679/// Minimum supported protocol version by this node
680pub const X_IOTA_MIN_SUPPORTED_PROTOCOL_VERSION: &str = "x-iota-min-supported-protocol-version";
681/// Maximum supported protocol version by this node
682pub const X_IOTA_MAX_SUPPORTED_PROTOCOL_VERSION: &str = "x-iota-max-supported-protocol-version";
683
684type SupportedProtocolHeaders = [(&'static str, String); 2];
685
686fn supported_protocol_headers() -> SupportedProtocolHeaders {
687    [
688        (
689            X_IOTA_MIN_SUPPORTED_PROTOCOL_VERSION,
690            ProtocolVersion::MIN.as_u64().to_string(),
691        ),
692        (
693            X_IOTA_MAX_SUPPORTED_PROTOCOL_VERSION,
694            ProtocolVersion::MAX.as_u64().to_string(),
695        ),
696    ]
697}
698
699#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
700#[serde(rename = "ProtocolConfig")]
701pub struct ProtocolConfigResponse {
702    #[serde(with = "serde_with::As::<serde_with::DisplayFromStr>")]
703    #[schemars(with = "crate::_schemars::U64")]
704    pub protocol_version: u64,
705    pub feature_flags: BTreeMap<String, bool>,
706    pub attributes: BTreeMap<String, String>,
707}
708
709impl From<ProtocolConfig> for ProtocolConfigResponse {
710    fn from(config: ProtocolConfig) -> Self {
711        let attributes = config
712            .attr_map()
713            .into_iter()
714            .filter_map(|(k, maybe_v)| {
715                maybe_v.map(move |v| {
716                    let v = match v {
717                        ProtocolConfigValue::u16(x) => x.to_string(),
718                        ProtocolConfigValue::u32(y) => y.to_string(),
719                        ProtocolConfigValue::u64(z) => z.to_string(),
720                        ProtocolConfigValue::bool(b) => b.to_string(),
721                    };
722                    (k, v)
723                })
724            })
725            .collect();
726        ProtocolConfigResponse {
727            protocol_version: config.version.as_u64(),
728            attributes,
729            feature_flags: config.feature_map(),
730        }
731    }
732}
733
734pub struct GetGasInfo;
735
736impl ApiEndpoint<RestService> for GetGasInfo {
737    fn method(&self) -> axum::http::Method {
738        axum::http::Method::GET
739    }
740
741    fn path(&self) -> &'static str {
742        "/system/gas"
743    }
744
745    fn operation(
746        &self,
747        generator: &mut schemars::gen::SchemaGenerator,
748    ) -> openapiv3::v3_1::Operation {
749        OperationBuilder::new()
750            .tag("System")
751            .operation_id("GetGasInfo")
752            .response(
753                200,
754                ResponseBuilder::new()
755                    .json_content::<GasInfo>(generator)
756                    .build(),
757            )
758            .build()
759    }
760
761    fn handler(&self) -> RouteHandler<RestService> {
762        RouteHandler::new(self.method(), get_gas_info)
763    }
764}
765
766async fn get_gas_info(
767    accept: AcceptFormat,
768    State(state): State<StateReader>,
769) -> Result<Json<GasInfo>> {
770    match accept {
771        AcceptFormat::Json => {}
772        _ => {
773            return Err(RestError::new(
774                axum::http::StatusCode::BAD_REQUEST,
775                "invalid accept type",
776            ));
777        }
778    }
779
780    let reference_gas_price = state.get_system_state_summary()?.reference_gas_price;
781
782    Ok(Json(GasInfo {
783        reference_gas_price,
784    }))
785}
786
787#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
788pub struct GasInfo {
789    #[serde(with = "serde_with::As::<serde_with::DisplayFromStr>")]
790    #[schemars(with = "crate::_schemars::U64")]
791    pub reference_gas_price: u64,
792}