iota_rest_api/
coins.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use axum::{
6    Json,
7    extract::{Path, State},
8};
9use iota_sdk2::types::{ObjectId, StructTag};
10use iota_types::iota_sdk2_conversions::struct_tag_sdk_to_core;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use crate::{
15    RestError, RestService, Result,
16    openapi::{ApiEndpoint, OperationBuilder, ResponseBuilder, RouteHandler},
17    reader::StateReader,
18};
19
20pub struct GetCoinInfo;
21
22impl ApiEndpoint<RestService> for GetCoinInfo {
23    fn method(&self) -> axum::http::Method {
24        axum::http::Method::GET
25    }
26
27    fn path(&self) -> &'static str {
28        "/coins/{coin_type}"
29    }
30
31    fn operation(
32        &self,
33        generator: &mut schemars::gen::SchemaGenerator,
34    ) -> openapiv3::v3_1::Operation {
35        OperationBuilder::new()
36            .tag("Coins")
37            .operation_id("GetCoinInfo")
38            .path_parameter::<StructTag>("coin_type", generator)
39            .response(
40                200,
41                ResponseBuilder::new()
42                    .json_content::<CoinInfo>(generator)
43                    .build(),
44            )
45            .response(404, ResponseBuilder::new().build())
46            .build()
47    }
48
49    fn handler(&self) -> crate::openapi::RouteHandler<RestService> {
50        RouteHandler::new(self.method(), get_coin_info)
51    }
52}
53
54async fn get_coin_info(
55    Path(coin_type): Path<StructTag>,
56    State(state): State<StateReader>,
57) -> Result<Json<CoinInfo>> {
58    let core_coin_type = struct_tag_sdk_to_core(coin_type.clone())?;
59
60    let iota_types::storage::CoinInfo {
61        coin_metadata_object_id,
62        treasury_object_id,
63    } = state
64        .inner()
65        .get_coin_info(&core_coin_type)?
66        .ok_or_else(|| CoinNotFoundError(coin_type.clone()))?;
67
68    let metadata = if let Some(coin_metadata_object_id) = coin_metadata_object_id {
69        state
70            .inner()
71            .get_object(&coin_metadata_object_id)?
72            .map(iota_types::coin::CoinMetadata::try_from)
73            .transpose()
74            .map_err(|_| {
75                RestError::new(
76                    axum::http::StatusCode::INTERNAL_SERVER_ERROR,
77                    format!("Unable to read object {coin_metadata_object_id} for coin type {core_coin_type} as CoinMetadata"),
78                )
79            })?
80            .map(CoinMetadata::from)
81    } else {
82        None
83    };
84
85    let treasury = if let Some(treasury_object_id) = treasury_object_id {
86        state
87            .inner()
88            .get_object(&treasury_object_id)?
89            .map(iota_types::coin::TreasuryCap::try_from)
90            .transpose()
91            .map_err(|_| {
92                RestError::new(
93                    axum::http::StatusCode::INTERNAL_SERVER_ERROR,
94                    format!("Unable to read object {treasury_object_id} for coin type {core_coin_type} as TreasuryCap"),
95                )
96            })?
97            .map(|treasury| CoinTreasury {
98                id: treasury.id.id.bytes.into(),
99                total_supply: treasury.total_supply.value,
100            })
101    } else if iota_types::gas_coin::GAS::is_gas(&core_coin_type) {
102        let system_state_summary = state.get_system_state_summary()?;
103
104        Some(CoinTreasury {
105            id: system_state_summary.iota_treasury_cap_id,
106            total_supply: system_state_summary.iota_total_supply,
107        })
108    } else {
109        None
110    };
111
112    Ok(Json(CoinInfo {
113        coin_type,
114        metadata,
115        treasury,
116    }))
117}
118
119#[derive(Debug)]
120pub struct CoinNotFoundError(StructTag);
121
122impl std::fmt::Display for CoinNotFoundError {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        write!(f, "Coin type {} not found", self.0)
125    }
126}
127
128impl std::error::Error for CoinNotFoundError {}
129
130impl From<CoinNotFoundError> for crate::RestError {
131    fn from(value: CoinNotFoundError) -> Self {
132        Self::new(axum::http::StatusCode::NOT_FOUND, value.to_string())
133    }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
137pub struct CoinInfo {
138    pub coin_type: StructTag,
139    pub metadata: Option<CoinMetadata>,
140    pub treasury: Option<CoinTreasury>,
141}
142
143#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
144pub struct CoinMetadata {
145    pub id: ObjectId,
146    /// Number of decimal places the coin uses.
147    pub decimals: u8,
148    /// Name for the token
149    pub name: String,
150    /// Symbol for the token
151    pub symbol: String,
152    /// Description of the token
153    pub description: String,
154    /// URL for the token logo
155    pub icon_url: Option<String>,
156}
157
158impl From<iota_types::coin::CoinMetadata> for CoinMetadata {
159    fn from(value: iota_types::coin::CoinMetadata) -> Self {
160        Self {
161            id: value.id.id.bytes.into(),
162            decimals: value.decimals,
163            name: value.name,
164            symbol: value.symbol,
165            description: value.description,
166            icon_url: value.icon_url,
167        }
168    }
169}
170
171#[serde_with::serde_as]
172#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
173pub struct CoinTreasury {
174    pub id: ObjectId,
175    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
176    #[schemars(with = "crate::_schemars::U64")]
177    pub total_supply: u64,
178}