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_sdk_types_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 indexes = state.inner().indexes().ok_or_else(RestError::not_found)?;
59
60    let core_coin_type = struct_tag_sdk_to_core(coin_type.clone())?;
61
62    let iota_types::storage::CoinInfo {
63        coin_metadata_object_id,
64        treasury_object_id,
65    } = indexes
66        .get_coin_info(&core_coin_type)?
67        .ok_or_else(|| CoinNotFoundError(coin_type.clone()))?;
68
69    let metadata = if let Some(coin_metadata_object_id) = coin_metadata_object_id {
70        state
71            .inner()
72            .try_get_object(&coin_metadata_object_id)?
73            .map(iota_types::coin::CoinMetadata::try_from)
74            .transpose()
75            .map_err(|_| {
76                RestError::new(
77                    axum::http::StatusCode::INTERNAL_SERVER_ERROR,
78                    format!("Unable to read object {coin_metadata_object_id} for coin type {core_coin_type} as CoinMetadata"),
79                )
80            })?
81            .map(CoinMetadata::from)
82    } else {
83        None
84    };
85
86    let treasury = if let Some(treasury_object_id) = treasury_object_id {
87        state
88            .inner()
89            .try_get_object(&treasury_object_id)?
90            .map(iota_types::coin::TreasuryCap::try_from)
91            .transpose()
92            .map_err(|_| {
93                RestError::new(
94                    axum::http::StatusCode::INTERNAL_SERVER_ERROR,
95                    format!("Unable to read object {treasury_object_id} for coin type {core_coin_type} as TreasuryCap"),
96                )
97            })?
98            .map(|treasury| CoinTreasury {
99                id: treasury.id.id.bytes.into(),
100                total_supply: treasury.total_supply.value,
101            })
102    } else if iota_types::gas_coin::GAS::is_gas(&core_coin_type) {
103        let system_state_summary = state.get_system_state_summary()?;
104
105        Some(CoinTreasury {
106            id: system_state_summary.iota_treasury_cap_id,
107            total_supply: system_state_summary.iota_total_supply,
108        })
109    } else {
110        None
111    };
112
113    Ok(Json(CoinInfo {
114        coin_type,
115        metadata,
116        treasury,
117    }))
118}
119
120#[derive(Debug)]
121pub struct CoinNotFoundError(StructTag);
122
123impl std::fmt::Display for CoinNotFoundError {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        write!(f, "Coin type {} not found", self.0)
126    }
127}
128
129impl std::error::Error for CoinNotFoundError {}
130
131impl From<CoinNotFoundError> for crate::RestError {
132    fn from(value: CoinNotFoundError) -> Self {
133        Self::new(axum::http::StatusCode::NOT_FOUND, value.to_string())
134    }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
138pub struct CoinInfo {
139    pub coin_type: StructTag,
140    pub metadata: Option<CoinMetadata>,
141    pub treasury: Option<CoinTreasury>,
142}
143
144#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
145pub struct CoinMetadata {
146    pub id: ObjectId,
147    /// Number of decimal places the coin uses.
148    pub decimals: u8,
149    /// Name for the token
150    pub name: String,
151    /// Symbol for the token
152    pub symbol: String,
153    /// Description of the token
154    pub description: String,
155    /// URL for the token logo
156    pub icon_url: Option<String>,
157}
158
159impl From<iota_types::coin::CoinMetadata> for CoinMetadata {
160    fn from(value: iota_types::coin::CoinMetadata) -> Self {
161        Self {
162            id: value.id.id.bytes.into(),
163            decimals: value.decimals,
164            name: value.name,
165            symbol: value.symbol,
166            description: value.description,
167            icon_url: value.icon_url,
168        }
169    }
170}
171
172#[serde_with::serde_as]
173#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
174pub struct CoinTreasury {
175    pub id: ObjectId,
176    #[serde_as(as = "iota_types::iota_serde::BigInt<u64>")]
177    #[schemars(with = "crate::_schemars::U64")]
178    pub total_supply: u64,
179}