1use 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 pub decimals: u8,
149 pub name: String,
151 pub symbol: String,
153 pub description: String,
155 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}