1use 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 pub decimals: u8,
148 pub name: String,
150 pub symbol: String,
152 pub description: String,
154 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}