Skip to main content

worgl_client/
client.rs

1//! Typed calls to a Worgl server, one per route.
2
3use 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
18/// How long a request may take, connection included.
19const TIMEOUT: Duration = Duration::from_secs(30);
20
21/// How long connecting may take.
22const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
23
24/// Most bytes of an error answer without a code kept in [`Refusal::message`].
25const MESSAGE_LIMIT: usize = 512;
26
27/// A connection to one Worgl server, which may host many mints.
28///
29/// Every call is a single request with no retry. Transfers and burns are
30/// idempotent, so resending one after an [`Error::is_transient`] failure is
31/// safe; a [`Purse`](crate::Purse) does that itself. Cloning is cheap and
32/// shares the connection pool.
33#[derive(Debug, Clone)]
34pub struct Client {
35    http: reqwest::Client,
36    base: String,
37}
38
39/// A mint's `info`.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct Info {
42    /// The mint's issuer key.
43    pub issuer: PublicKey,
44    /// Events the mint has applied since it was created.
45    pub sequence: u64,
46    /// Position new money is minted into.
47    pub mint_layer: u32,
48    /// Every layer, first to last.
49    pub layers: Vec<LayerSummary>,
50    /// Value minted, destroyed, burned and outstanding.
51    pub supply: Supply,
52    /// The rules in force.
53    pub policy: Policy,
54    /// When the mint rotates on its own; `None` when only on request.
55    #[serde(default)]
56    pub rotation: Option<Schedule>,
57    /// Whether the mint is paused: it then refuses transfers, burns, mints
58    /// and rotations.
59    #[serde(default)]
60    pub paused: bool,
61}
62
63impl Info {
64    /// What a [`Holder`](worgl_sdk::Holder) needs of it.
65    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/// What a playground faucet hands out, from `GET /v1/faucet`.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub struct FaucetInfo {
79    /// The mint its notes come from.
80    pub mint: PublicKey,
81    /// The value of each note.
82    pub amount: u64,
83    /// Notes one client address may get in any hour.
84    pub per_address_per_hour: u32,
85    /// Notes the faucet hands out in a day, UTC.
86    pub per_day: u32,
87    /// Notes left to hand out today.
88    pub left_today: u32,
89}
90
91/// The mint's answer about a genuine note.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93pub struct Verified {
94    /// The note's serial.
95    pub serial: Serial,
96    /// Its amount.
97    pub amount: u64,
98    /// Whether it is live, spent, expired or unknown to the ledger.
99    #[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    /// A client for the server at `base_url`, such as
136    /// `https://mint.example.com`. Requests time out after 30 seconds.
137    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    /// A client that sends its requests through `http`, for custom timeouts,
147    /// proxies or certificates.
148    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    /// The server URL requests go to.
160    pub fn base_url(&self) -> &str {
161        &self.base
162    }
163
164    /// `GET /v1/mints/{mint}/info`.
165    pub async fn info(&self, mint: &PublicKey) -> Result<Info, Error> {
166        self.send(self.mint_route(Method::GET, mint, "info")).await
167    }
168
169    /// The mint's info as a [`MintView`], ready for a
170    /// [`Holder`](worgl_sdk::Holder): its layers are the ones the server
171    /// answered with, whatever this machine's clock says.
172    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    /// [`info`](Self::info), with the time the server answered: Unix seconds
178    /// by its clock, from the answer's `Date` header, or by this machine's
179    /// if the answer has no `Date` it can read.
180    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    /// [`view`](Self::view), with the time the server answered, as
189    /// [`info_and_time`](Self::info_and_time) reads it. Plan the transfers
190    /// due before the mint's next rotation from it, as in
191    /// [`Holder::refresh`](worgl_sdk::Holder::refresh): the mint rotates by
192    /// its server's clock, and planning by this machine's, if it runs slow
193    /// by more than the [margin](worgl_sdk::REFRESH_MARGIN_SECS), can put
194    /// them past the rotation.
195    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    /// `GET /v1/mints/{mint}/keys`: the keys that sign the mint's live
201    /// generations, each certified by the issuer key. Fails with
202    /// [`Error::Keys`] if the set is another issuer's.
203    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    /// `POST /v1/verify`: what the note's mint says about it.
215    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    /// `POST /v1/transfer`: the new notes, in the order of the outputs.
223    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    /// `POST /v1/burn`: the value destroyed.
229    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    /// `POST /v1/mints/{mint}/mint`: a new note, for a request signed by one
235    /// of the mint's authorities.
236    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    /// `GET /v1/faucet`, from a playground faucet: its mint and limits. The
242    /// faucet is `worgl-faucet`, at this client's URL, which may be its
243    /// mint's server with the faucet behind the same proxy.
244    pub async fn faucet(&self) -> Result<FaucetInfo, Error> {
245        self.send(self.route(Method::GET, "/v1/faucet")).await
246    }
247
248    /// `POST /v1/faucet`: a note of the faucet's mint, bound to `holder`.
249    /// Past the faucet's limits it answers `429` with the code
250    /// `rate_limited` or `exhausted`.
251    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    /// `POST /v1/mints/{mint}/rotate`, with the mint's admin token.
259    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    /// `POST /v1/mints/{mint}/pause`, with the mint's admin token.
265    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    /// `POST /v1/mints/{mint}/resume`, with the mint's admin token.
272    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    /// Sends a request and returns the answer, or the refusal as an error.
291    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
301/// When an answer was made, in seconds since the Unix epoch by the server's
302/// clock, from its `Date` header.
303fn 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
309/// Reads an error answer, keeping what it says even without a code.
310async fn refusal(response: Response) -> Refusal {
311    let status = response.status().as_u16();
312    // In seconds; the server never sends the date form.
313    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
345/// Seconds since the Unix epoch, as mints count time.
346fn unix_now() -> u64 {
347    SystemTime::now()
348        .duration_since(UNIX_EPOCH)
349        .map_or(0, |elapsed| elapsed.as_secs())
350}