1use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5use reqwest::header::RETRY_AFTER;
6use reqwest::{Method, RequestBuilder, Response, header};
7use serde::de::DeserializeOwned;
8use serde::{Deserialize, Serialize};
9use worgl::issuer::LayerSummary;
10use worgl::{
11 BurnRequest, KeySet, MintRequest, Note, NoteStatus, Policy, PublicKey, RotationReport, Serial,
12 Supply, TransferRequest,
13};
14use worgl_sdk::{MintInfo, MintView, Schedule};
15
16use crate::error::{Error, Refusal};
17
18const TIMEOUT: Duration = Duration::from_secs(30);
20
21const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
23
24const MESSAGE_LIMIT: usize = 512;
26
27#[derive(Debug, Clone)]
34pub struct Client {
35 http: reqwest::Client,
36 base: String,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct Info {
42 pub issuer: PublicKey,
44 pub sequence: u64,
46 pub mint_layer: u32,
48 pub layers: Vec<LayerSummary>,
50 pub supply: Supply,
52 pub policy: Policy,
54 #[serde(default)]
56 pub rotation: Option<Schedule>,
57 #[serde(default)]
60 pub paused: bool,
61}
62
63impl Info {
64 pub fn mint_info(&self) -> MintInfo {
66 MintInfo {
67 issuer: self.issuer,
68 layers: self.layers.clone(),
69 policy: self.policy.clone(),
70 rotation: self.rotation,
71 paused: self.paused,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub struct FaucetInfo {
79 pub mint: PublicKey,
81 pub amount: u64,
83 pub per_address_per_hour: u32,
85 pub per_day: u32,
87 pub left_today: u32,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93pub struct Verified {
94 pub serial: Serial,
96 pub amount: u64,
98 #[serde(flatten)]
100 pub status: NoteStatus,
101}
102
103#[derive(Serialize)]
104struct NoteRef<'a> {
105 note: &'a Note,
106}
107
108#[derive(Deserialize)]
109struct NoteBody {
110 note: Note,
111}
112
113#[derive(Serialize)]
114struct HolderRef<'a> {
115 holder: &'a PublicKey,
116}
117
118#[derive(Deserialize)]
119struct NotesBody {
120 notes: Vec<Note>,
121}
122
123#[derive(Deserialize)]
124struct BurnedBody {
125 value: u128,
126}
127
128#[derive(Deserialize)]
129struct PausedBody {
130 #[expect(dead_code, reason = "the status code already says it worked")]
131 paused: bool,
132}
133
134impl Client {
135 pub fn new(base_url: &str) -> Result<Self, Error> {
138 let http = reqwest::Client::builder()
139 .user_agent(concat!("worgl-client/", env!("CARGO_PKG_VERSION")))
140 .timeout(TIMEOUT)
141 .connect_timeout(CONNECT_TIMEOUT)
142 .build()?;
143 Self::with_http(http, base_url)
144 }
145
146 pub fn with_http(http: reqwest::Client, base_url: &str) -> Result<Self, Error> {
149 let url = reqwest::Url::parse(base_url).map_err(|_| Error::Url(base_url.into()))?;
150 if !matches!(url.scheme(), "http" | "https") || url.cannot_be_a_base() {
151 return Err(Error::Url(base_url.into()));
152 }
153 Ok(Self {
154 http,
155 base: url.as_str().trim_end_matches('/').to_owned(),
156 })
157 }
158
159 pub fn base_url(&self) -> &str {
161 &self.base
162 }
163
164 pub async fn info(&self, mint: &PublicKey) -> Result<Info, Error> {
166 self.send(self.mint_route(Method::GET, mint, "info")).await
167 }
168
169 pub async fn view(&self, mint: &PublicKey) -> Result<MintView, Error> {
173 let info = self.info(mint).await?;
174 Ok(MintView::new(&info.mint_info())?)
175 }
176
177 pub async fn info_and_time(&self, mint: &PublicKey) -> Result<(Info, u64), Error> {
181 let answer = self
182 .answer(self.mint_route(Method::GET, mint, "info"))
183 .await?;
184 let time = answered_at(&answer).unwrap_or_else(unix_now);
185 Ok((answer.json().await?, time))
186 }
187
188 pub async fn view_and_time(&self, mint: &PublicKey) -> Result<(MintView, u64), Error> {
196 let (info, time) = self.info_and_time(mint).await?;
197 Ok((MintView::new(&info.mint_info())?, time))
198 }
199
200 pub async fn keys(&self, mint: &PublicKey) -> Result<KeySet, Error> {
204 let keys: KeySet = self
205 .send(self.mint_route(Method::GET, mint, "keys"))
206 .await?;
207 if keys.issuer() == mint {
208 Ok(keys)
209 } else {
210 Err(Error::Keys)
211 }
212 }
213
214 pub async fn verify(&self, note: &Note) -> Result<Verified, Error> {
216 let request = self
217 .route(Method::POST, "/v1/verify")
218 .json(&NoteRef { note });
219 self.send(request).await
220 }
221
222 pub async fn transfer(&self, request: &TransferRequest) -> Result<Vec<Note>, Error> {
224 let request = self.route(Method::POST, "/v1/transfer").json(request);
225 Ok(self.send::<NotesBody>(request).await?.notes)
226 }
227
228 pub async fn burn(&self, request: &BurnRequest) -> Result<u128, Error> {
230 let request = self.route(Method::POST, "/v1/burn").json(request);
231 Ok(self.send::<BurnedBody>(request).await?.value)
232 }
233
234 pub async fn mint(&self, mint: &PublicKey, request: &MintRequest) -> Result<Note, Error> {
237 let request = self.mint_route(Method::POST, mint, "mint").json(request);
238 Ok(self.send::<NoteBody>(request).await?.note)
239 }
240
241 pub async fn faucet(&self) -> Result<FaucetInfo, Error> {
245 self.send(self.route(Method::GET, "/v1/faucet")).await
246 }
247
248 pub async fn ask_faucet(&self, holder: &PublicKey) -> Result<Note, Error> {
252 let request = self
253 .route(Method::POST, "/v1/faucet")
254 .json(&HolderRef { holder });
255 Ok(self.send::<NoteBody>(request).await?.note)
256 }
257
258 pub async fn rotate(&self, mint: &PublicKey, token: &str) -> Result<RotationReport, Error> {
260 let request = self.mint_route(Method::POST, mint, "rotate");
261 self.send(request.bearer_auth(token)).await
262 }
263
264 pub async fn pause(&self, mint: &PublicKey, token: &str) -> Result<(), Error> {
266 let request = self.mint_route(Method::POST, mint, "pause");
267 self.send::<PausedBody>(request.bearer_auth(token)).await?;
268 Ok(())
269 }
270
271 pub async fn resume(&self, mint: &PublicKey, token: &str) -> Result<(), Error> {
273 let request = self.mint_route(Method::POST, mint, "resume");
274 self.send::<PausedBody>(request.bearer_auth(token)).await?;
275 Ok(())
276 }
277
278 fn route(&self, method: Method, path: &str) -> RequestBuilder {
279 self.http.request(method, format!("{}{path}", self.base))
280 }
281
282 fn mint_route(&self, method: Method, mint: &PublicKey, route: &str) -> RequestBuilder {
283 self.route(method, &format!("/v1/mints/{mint}/{route}"))
284 }
285
286 async fn send<T: DeserializeOwned>(&self, request: RequestBuilder) -> Result<T, Error> {
287 Ok(self.answer(request).await?.json().await?)
288 }
289
290 async fn answer(&self, request: RequestBuilder) -> Result<Response, Error> {
292 let response = request.send().await?;
293 if response.status().is_success() {
294 Ok(response)
295 } else {
296 Err(Error::Refused(refusal(response).await))
297 }
298 }
299}
300
301fn answered_at(response: &Response) -> Option<u64> {
304 let date = response.headers().get(header::DATE)?.to_str().ok()?;
305 let time = httpdate::parse_http_date(date).ok()?;
306 Some(time.duration_since(UNIX_EPOCH).ok()?.as_secs())
307}
308
309async fn refusal(response: Response) -> Refusal {
311 let status = response.status().as_u16();
312 let retry_after = response
314 .headers()
315 .get(RETRY_AFTER)
316 .and_then(|value| value.to_str().ok())
317 .and_then(|value| value.trim().parse().ok())
318 .map(Duration::from_secs);
319 let text = response.text().await.unwrap_or_default();
320 match serde_json::from_str::<Refusal>(&text) {
321 Ok(refusal) => Refusal {
322 status,
323 retry_after,
324 ..refusal
325 },
326 Err(_) => {
327 let mut message = text.trim().to_owned();
328 if message.len() > MESSAGE_LIMIT {
329 let mut end = MESSAGE_LIMIT;
330 while !message.is_char_boundary(end) {
331 end -= 1;
332 }
333 message.truncate(end);
334 }
335 Refusal {
336 status,
337 code: "http".into(),
338 message,
339 retry_after,
340 }
341 }
342 }
343}
344
345fn unix_now() -> u64 {
347 SystemTime::now()
348 .duration_since(UNIX_EPOCH)
349 .map_or(0, |elapsed| elapsed.as_secs())
350}