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_sdk_types::{Address, ObjectId, PublicKeyExt};
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_sdk_types::Bls12381PublicKey,
250    pub network_public_key: iota_sdk_types::Ed25519PublicKey,
251    pub protocol_public_key: iota_sdk_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_sdk_types::Bls12381PublicKey>,
263    pub next_epoch_network_public_key: Option<iota_sdk_types::Ed25519PublicKey>,
264    pub next_epoch_protocol_public_key: Option<iota_sdk_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_sdk_types::Bls12381PublicKey::from_bytes(
387                authority_pubkey_bytes,
388            )
389            .unwrap(),
390            network_public_key: iota_sdk_types::Ed25519PublicKey::from_bytes(network_pubkey_bytes)
391                .unwrap(),
392            protocol_public_key: iota_sdk_types::Ed25519PublicKey::from_bytes(
393                protocol_pubkey_bytes,
394            )
395            .unwrap(),
396            proof_of_possession_bytes,
397            name,
398            description,
399            image_url,
400            project_url,
401            net_address,
402            p2p_address,
403            primary_address,
404            next_epoch_authority_public_key: next_epoch_authority_pubkey_bytes
405                .map(|bytes| iota_sdk_types::Bls12381PublicKey::from_bytes(bytes).unwrap()),
406            next_epoch_network_public_key: next_epoch_network_pubkey_bytes
407                .map(|bytes| iota_sdk_types::Ed25519PublicKey::from_bytes(bytes).unwrap()),
408            next_epoch_protocol_public_key: next_epoch_protocol_pubkey_bytes
409                .map(|bytes| iota_sdk_types::Ed25519PublicKey::from_bytes(bytes).unwrap()),
410            next_epoch_proof_of_possession,
411            next_epoch_net_address,
412            next_epoch_p2p_address,
413            next_epoch_primary_address,
414            voting_power,
415            operation_cap_id: operation_cap_id.into(),
416            gas_price,
417            commission_rate,
418            next_epoch_stake,
419            next_epoch_gas_price,
420            next_epoch_commission_rate,
421            staking_pool_id: staking_pool_id.into(),
422            staking_pool_activation_epoch,
423            staking_pool_deactivation_epoch,
424            staking_pool_iota_balance,
425            rewards_pool,
426            pool_token_balance,
427            pending_stake,
428            pending_total_iota_withdraw,
429            pending_pool_token_withdraw,
430            exchange_rates_id: exchange_rates_id.into(),
431            exchange_rates_size,
432        }
433    }
434}
435
436impl From<iota_types::iota_system_state::iota_system_state_summary::IotaSystemStateSummaryV2>
437    for SystemStateSummary
438{
439    fn from(
440        value: iota_types::iota_system_state::iota_system_state_summary::IotaSystemStateSummaryV2,
441    ) -> Self {
442        let iota_types::iota_system_state::iota_system_state_summary::IotaSystemStateSummaryV2 {
443            epoch,
444            protocol_version,
445            system_state_version,
446            iota_total_supply,
447            iota_treasury_cap_id,
448            storage_fund_total_object_storage_rebates,
449            storage_fund_non_refundable_balance,
450            reference_gas_price,
451            safe_mode,
452            safe_mode_storage_charges,
453            safe_mode_computation_charges,
454            safe_mode_computation_charges_burned,
455            safe_mode_storage_rebates,
456            safe_mode_non_refundable_storage_fee,
457            epoch_start_timestamp_ms,
458            epoch_duration_ms,
459            min_validator_count,
460            max_validator_count,
461            min_validator_joining_stake,
462            validator_low_stake_threshold,
463            validator_very_low_stake_threshold,
464            validator_low_stake_grace_period,
465            total_stake,
466            committee_members,
467            active_validators,
468            pending_active_validators_id,
469            pending_active_validators_size,
470            pending_removals,
471            staking_pool_mappings_id,
472            staking_pool_mappings_size,
473            inactive_pools_id,
474            inactive_pools_size,
475            validator_candidates_id,
476            validator_candidates_size,
477            at_risk_validators,
478            validator_report_records,
479        } = value;
480
481        Self {
482            epoch,
483            protocol_version,
484            system_state_version,
485            iota_total_supply,
486            iota_treasury_cap_id: iota_treasury_cap_id.into(),
487            storage_fund_total_object_storage_rebates,
488            storage_fund_non_refundable_balance,
489            reference_gas_price,
490            safe_mode,
491            safe_mode_storage_charges,
492            safe_mode_computation_charges,
493            safe_mode_computation_charges_burned,
494            safe_mode_storage_rebates,
495            safe_mode_non_refundable_storage_fee,
496            epoch_start_timestamp_ms,
497            epoch_duration_ms,
498            min_validator_count,
499            max_validator_count,
500            min_validator_joining_stake,
501            validator_low_stake_threshold,
502            validator_very_low_stake_threshold,
503            validator_low_stake_grace_period,
504            total_stake,
505            committee_members,
506            active_validators: active_validators.into_iter().map(Into::into).collect(),
507            pending_active_validators_id: pending_active_validators_id.into(),
508            pending_active_validators_size,
509            pending_removals,
510            staking_pool_mappings_id: staking_pool_mappings_id.into(),
511            staking_pool_mappings_size,
512            inactive_pools_id: inactive_pools_id.into(),
513            inactive_pools_size,
514            validator_candidates_id: validator_candidates_id.into(),
515            validator_candidates_size,
516            at_risk_validators: at_risk_validators
517                .into_iter()
518                .map(|(address, idx)| (address.into(), idx))
519                .collect(),
520            validator_report_records: validator_report_records
521                .into_iter()
522                .map(|(address, reports)| {
523                    (
524                        address.into(),
525                        reports.into_iter().map(Into::into).collect(),
526                    )
527                })
528                .collect(),
529        }
530    }
531}
532
533pub struct GetCurrentProtocolConfig;
534
535impl ApiEndpoint<RestService> for GetCurrentProtocolConfig {
536    fn method(&self) -> axum::http::Method {
537        axum::http::Method::GET
538    }
539
540    fn path(&self) -> &'static str {
541        "/system/protocol"
542    }
543
544    fn operation(
545        &self,
546        generator: &mut schemars::gen::SchemaGenerator,
547    ) -> openapiv3::v3_1::Operation {
548        OperationBuilder::new()
549            .tag("System")
550            .operation_id("GetCurrentProtocolConfig")
551            .response(
552                200,
553                ResponseBuilder::new()
554                    .json_content::<ProtocolConfigResponse>(generator)
555                    .header::<String>(X_IOTA_MIN_SUPPORTED_PROTOCOL_VERSION, generator)
556                    .header::<String>(X_IOTA_MAX_SUPPORTED_PROTOCOL_VERSION, generator)
557                    .build(),
558            )
559            .build()
560    }
561
562    fn handler(&self) -> RouteHandler<RestService> {
563        RouteHandler::new(self.method(), get_current_protocol_config)
564    }
565}
566
567async fn get_current_protocol_config(
568    accept: AcceptFormat,
569    State(state): State<StateReader>,
570) -> Result<(SupportedProtocolHeaders, Json<ProtocolConfigResponse>)> {
571    match accept {
572        AcceptFormat::Json => {}
573        _ => {
574            return Err(RestError::new(
575                axum::http::StatusCode::BAD_REQUEST,
576                "invalid accept type",
577            ));
578        }
579    }
580
581    let current_protocol_version = state.get_system_state_summary()?.protocol_version;
582
583    let config = ProtocolConfig::get_for_version_if_supported(
584        current_protocol_version.into(),
585        state.inner().get_chain_identifier()?.chain(),
586    )
587    .ok_or_else(|| ProtocolNotFoundError::new(current_protocol_version))?;
588
589    Ok((supported_protocol_headers(), Json(config.into())))
590}
591
592pub struct GetProtocolConfig;
593
594impl ApiEndpoint<RestService> for GetProtocolConfig {
595    fn method(&self) -> axum::http::Method {
596        axum::http::Method::GET
597    }
598
599    fn path(&self) -> &'static str {
600        "/system/protocol/{version}"
601    }
602
603    fn operation(
604        &self,
605        generator: &mut schemars::gen::SchemaGenerator,
606    ) -> openapiv3::v3_1::Operation {
607        OperationBuilder::new()
608            .tag("System")
609            .operation_id("GetProtocolConfig")
610            .path_parameter::<u64>("version", generator)
611            .response(
612                200,
613                ResponseBuilder::new()
614                    .json_content::<ProtocolConfigResponse>(generator)
615                    .header::<String>(X_IOTA_MIN_SUPPORTED_PROTOCOL_VERSION, generator)
616                    .header::<String>(X_IOTA_MAX_SUPPORTED_PROTOCOL_VERSION, generator)
617                    .build(),
618            )
619            .response(404, ResponseBuilder::new().build())
620            .build()
621    }
622
623    fn handler(&self) -> RouteHandler<RestService> {
624        RouteHandler::new(self.method(), get_protocol_config)
625    }
626}
627
628async fn get_protocol_config(
629    Path(version): Path<u64>,
630    accept: AcceptFormat,
631    State(state): State<StateReader>,
632) -> Result<(SupportedProtocolHeaders, Json<ProtocolConfigResponse>)> {
633    match accept {
634        AcceptFormat::Json => {}
635        _ => {
636            return Err(RestError::new(
637                axum::http::StatusCode::BAD_REQUEST,
638                "invalid accept type",
639            ));
640        }
641    }
642
643    let config = ProtocolConfig::get_for_version_if_supported(
644        version.into(),
645        state.inner().get_chain_identifier()?.chain(),
646    )
647    .ok_or_else(|| ProtocolNotFoundError::new(version))?;
648
649    Ok((supported_protocol_headers(), Json(config.into())))
650}
651
652#[derive(Debug)]
653pub struct ProtocolNotFoundError {
654    version: u64,
655}
656
657impl ProtocolNotFoundError {
658    pub fn new(version: u64) -> Self {
659        Self { version }
660    }
661}
662
663impl std::fmt::Display for ProtocolNotFoundError {
664    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
665        write!(f, "Protocol version {} not found", self.version)
666    }
667}
668
669impl std::error::Error for ProtocolNotFoundError {}
670
671impl From<ProtocolNotFoundError> for crate::RestError {
672    fn from(value: ProtocolNotFoundError) -> Self {
673        Self::new(axum::http::StatusCode::NOT_FOUND, value.to_string())
674    }
675}
676
677/// Minimum supported protocol version by this node
678pub const X_IOTA_MIN_SUPPORTED_PROTOCOL_VERSION: &str = "x-iota-min-supported-protocol-version";
679/// Maximum supported protocol version by this node
680pub const X_IOTA_MAX_SUPPORTED_PROTOCOL_VERSION: &str = "x-iota-max-supported-protocol-version";
681
682type SupportedProtocolHeaders = [(&'static str, String); 2];
683
684fn supported_protocol_headers() -> SupportedProtocolHeaders {
685    [
686        (
687            X_IOTA_MIN_SUPPORTED_PROTOCOL_VERSION,
688            ProtocolVersion::MIN.as_u64().to_string(),
689        ),
690        (
691            X_IOTA_MAX_SUPPORTED_PROTOCOL_VERSION,
692            ProtocolVersion::MAX.as_u64().to_string(),
693        ),
694    ]
695}
696
697#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
698#[serde(rename = "ProtocolConfig")]
699pub struct ProtocolConfigResponse {
700    #[serde(with = "serde_with::As::<serde_with::DisplayFromStr>")]
701    #[schemars(with = "crate::_schemars::U64")]
702    pub protocol_version: u64,
703    pub feature_flags: BTreeMap<String, bool>,
704    pub attributes: BTreeMap<String, String>,
705}
706
707impl From<ProtocolConfig> for ProtocolConfigResponse {
708    fn from(config: ProtocolConfig) -> Self {
709        let attributes = config
710            .attr_map()
711            .into_iter()
712            .filter_map(|(k, maybe_v)| {
713                maybe_v.map(move |v| {
714                    let v = match v {
715                        ProtocolConfigValue::u16(x) => x.to_string(),
716                        ProtocolConfigValue::u32(y) => y.to_string(),
717                        ProtocolConfigValue::u64(z) => z.to_string(),
718                        ProtocolConfigValue::bool(b) => b.to_string(),
719                    };
720                    (k, v)
721                })
722            })
723            .collect();
724        ProtocolConfigResponse {
725            protocol_version: config.version.as_u64(),
726            attributes,
727            feature_flags: config.feature_map(),
728        }
729    }
730}
731
732pub struct GetGasInfo;
733
734impl ApiEndpoint<RestService> for GetGasInfo {
735    fn method(&self) -> axum::http::Method {
736        axum::http::Method::GET
737    }
738
739    fn path(&self) -> &'static str {
740        "/system/gas"
741    }
742
743    fn operation(
744        &self,
745        generator: &mut schemars::gen::SchemaGenerator,
746    ) -> openapiv3::v3_1::Operation {
747        OperationBuilder::new()
748            .tag("System")
749            .operation_id("GetGasInfo")
750            .response(
751                200,
752                ResponseBuilder::new()
753                    .json_content::<GasInfo>(generator)
754                    .build(),
755            )
756            .build()
757    }
758
759    fn handler(&self) -> RouteHandler<RestService> {
760        RouteHandler::new(self.method(), get_gas_info)
761    }
762}
763
764async fn get_gas_info(
765    accept: AcceptFormat,
766    State(state): State<StateReader>,
767) -> Result<Json<GasInfo>> {
768    match accept {
769        AcceptFormat::Json => {}
770        _ => {
771            return Err(RestError::new(
772                axum::http::StatusCode::BAD_REQUEST,
773                "invalid accept type",
774            ));
775        }
776    }
777
778    let reference_gas_price = state.get_system_state_summary()?.reference_gas_price;
779
780    Ok(Json(GasInfo {
781        reference_gas_price,
782    }))
783}
784
785#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
786pub struct GasInfo {
787    #[serde(with = "serde_with::As::<serde_with::DisplayFromStr>")]
788    #[schemars(with = "crate::_schemars::U64")]
789    pub reference_gas_price: u64,
790}