Skip to main content

linera_faucet_client/
lib.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The client component of the Linera faucet.
5
6#![deny(missing_docs)]
7
8// TODO(#3362): generate this code
9
10use std::collections::BTreeMap;
11
12use linera_base::{
13    crypto::{CryptoHash, ValidatorPublicKey},
14    data_types::{Amount, ArithmeticError, ChainDescription, Timestamp},
15    identifiers::ChainId,
16};
17use linera_client::config::GenesisConfig;
18use linera_execution::{committee::ValidatorState, Committee, ResourceControlPolicy};
19use linera_version::VersionInfo;
20use thiserror_context::Context;
21
22/// The kinds of error that the faucet client can return.
23#[derive(Debug, thiserror::Error)]
24#[non_exhaustive]
25pub enum ErrorInner {
26    /// A response from the faucet could not be parsed as JSON.
27    #[error("JSON parsing error: {0:?}")]
28    Json(#[from] serde_json::Error),
29    /// The faucet returned one or more GraphQL errors.
30    #[error("GraphQL error: {0:?}")]
31    GraphQl(Vec<serde_json::Value>),
32    /// An HTTP request to the faucet failed.
33    #[error("HTTP error: {0:?}")]
34    Http(#[from] reqwest::Error),
35    /// An arithmetic operation overflowed.
36    #[error(transparent)]
37    ArithmeticError(#[from] ArithmeticError),
38}
39
40pub use error::Error;
41
42mod error {
43    // `impl_context!` generates a public `Error` newtype (with accessors) that cannot carry
44    // doc comments, so this wrapper module is exempted from the crate's `missing_docs` policy.
45    // `expect` (rather than `allow`) flags this if the macro ever stops generating such items.
46    #![expect(missing_docs)]
47
48    use thiserror_context::Context;
49
50    use super::ErrorInner;
51
52    thiserror_context::impl_context!(Error(ErrorInner));
53}
54
55/// The result of a successful claim mutation.
56#[derive(Clone, Debug, serde::Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct ClaimOutcome {
59    /// The ID of the chain.
60    pub chain_id: ChainId,
61    /// The hash of the certificate containing the operation.
62    pub certificate_hash: CryptoHash,
63    /// The amount of tokens transferred.
64    pub amount: Amount,
65}
66
67/// Information about the initial chain claim.
68#[derive(Clone, Debug, serde::Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct InitialClaim {
71    /// The chain ID that was created.
72    pub chain_id: ChainId,
73    /// The block timestamp when the chain was created.
74    pub timestamp: Timestamp,
75}
76
77/// A faucet instance that can be queried.
78#[derive(Debug, Clone)]
79pub struct Faucet {
80    url: String,
81}
82
83impl Faucet {
84    /// Creates a faucet client querying the faucet service at the given URL.
85    pub fn new(url: String) -> Self {
86        Self { url }
87    }
88
89    /// Returns the URL of the faucet service.
90    pub fn url(&self) -> &str {
91        &self.url
92    }
93
94    async fn query<Response: serde::de::DeserializeOwned>(
95        &self,
96        query: impl AsRef<str>,
97    ) -> Result<Response, Error> {
98        let query = query.as_ref();
99
100        #[derive(serde::Deserialize)]
101        struct GraphQlResponse<T> {
102            data: Option<T>,
103            errors: Option<Vec<serde_json::Value>>,
104        }
105
106        let builder = reqwest::ClientBuilder::new();
107
108        #[cfg(not(target_arch = "wasm32"))]
109        let builder = builder.timeout(linera_base::time::Duration::from_secs(30));
110
111        let response: GraphQlResponse<Response> = builder
112            .build()
113            .unwrap()
114            .post(&self.url)
115            .json(&serde_json::json!({
116                "query": query,
117            }))
118            .send()
119            .await
120            .with_context(|| format!("executing query {query:?}"))?
121            .error_for_status()?
122            .json()
123            .await?;
124
125        if let Some(errors) = response.errors {
126            // Extract just the error messages, ignore locations and path
127            let messages = errors
128                .iter()
129                .filter_map(|error| {
130                    error
131                        .get("message")
132                        .and_then(|msg| msg.as_str())
133                        .map(|s| s.to_string())
134                })
135                .collect::<Vec<_>>();
136
137            if messages.is_empty() {
138                Err(ErrorInner::GraphQl(errors).into())
139            } else {
140                Err(
141                    ErrorInner::GraphQl(vec![serde_json::Value::String(messages.join("; "))])
142                        .into(),
143                )
144            }
145        } else {
146            Ok(response
147                .data
148                .expect("no errors present but no data returned"))
149        }
150    }
151
152    /// Fetches the network's genesis configuration from the faucet.
153    pub async fn genesis_config(&self) -> Result<GenesisConfig, Error> {
154        #[derive(serde::Deserialize)]
155        #[serde(rename_all = "camelCase")]
156        struct Response {
157            genesis_config: GenesisConfig,
158        }
159
160        Ok(self
161            .query::<Response>("query { genesisConfig }")
162            .await?
163            .genesis_config)
164    }
165
166    /// Fetches the faucet's version information.
167    pub async fn version_info(&self) -> Result<VersionInfo, Error> {
168        #[derive(serde::Deserialize)]
169        struct Response {
170            version: VersionInfo,
171        }
172
173        Ok(self.query::<Response>("query { version }").await?.version)
174    }
175
176    /// Claims a new chain for the given owner, returning its chain description.
177    pub async fn claim(
178        &self,
179        owner: &linera_base::identifiers::AccountOwner,
180    ) -> Result<ChainDescription, Error> {
181        #[derive(serde::Deserialize)]
182        struct Response {
183            claim: ChainDescription,
184        }
185        Ok(self
186            .query::<Response>(format!("mutation {{ claim(owner: \"{owner}\") }}"))
187            .await?
188            .claim)
189    }
190
191    /// Claims daily tokens for the given owner.
192    /// The user must have already claimed a chain. Each user can claim once per
193    /// 24-hour period.
194    pub async fn daily_claim(
195        &self,
196        owner: &linera_base::identifiers::AccountOwner,
197    ) -> Result<ClaimOutcome, Error> {
198        #[derive(serde::Deserialize)]
199        #[serde(rename_all = "camelCase")]
200        struct Response {
201            daily_claim: ClaimOutcome,
202        }
203
204        Ok(self
205            .query::<Response>(format!("mutation {{ dailyClaim(owner: \"{owner}\") }}"))
206            .await?
207            .daily_claim)
208    }
209
210    /// Returns the initial claim for the given owner, if any.
211    pub async fn initial_claim(
212        &self,
213        owner: &linera_base::identifiers::AccountOwner,
214    ) -> Result<Option<InitialClaim>, Error> {
215        #[derive(serde::Deserialize)]
216        #[serde(rename_all = "camelCase")]
217        struct Response {
218            initial_claim: Option<InitialClaim>,
219        }
220
221        Ok(self
222            .query::<Response>(format!(
223                "query {{ initialClaim(owner: \"{owner}\") {{ chainId timestamp }} }}"
224            ))
225            .await?
226            .initial_claim)
227    }
228
229    /// Returns the earliest time at which the owner can make a daily claim.
230    /// If the returned timestamp is in the past (or now), the user can claim immediately.
231    /// Returns `None` if the user has not yet completed the initial claim.
232    pub async fn next_daily_claim(
233        &self,
234        owner: &linera_base::identifiers::AccountOwner,
235    ) -> Result<Option<Timestamp>, Error> {
236        #[derive(serde::Deserialize)]
237        #[serde(rename_all = "camelCase")]
238        struct Response {
239            next_daily_claim: Option<Timestamp>,
240        }
241
242        Ok(self
243            .query::<Response>(format!("query {{ nextDailyClaim(owner: \"{owner}\") }}"))
244            .await?
245            .next_daily_claim)
246    }
247
248    /// Returns the current validators' public keys and network addresses.
249    pub async fn current_validators(&self) -> Result<Vec<(ValidatorPublicKey, String)>, Error> {
250        #[derive(serde::Deserialize)]
251        #[serde(rename_all = "camelCase")]
252        struct Validator {
253            public_key: ValidatorPublicKey,
254            network_address: String,
255        }
256
257        #[derive(serde::Deserialize)]
258        #[serde(rename_all = "camelCase")]
259        struct Response {
260            current_validators: Vec<Validator>,
261        }
262
263        Ok(self
264            .query::<Response>("query { currentValidators { publicKey networkAddress } }")
265            .await?
266            .current_validators
267            .into_iter()
268            .map(|validator| (validator.public_key, validator.network_address))
269            .collect())
270    }
271
272    /// Returns the current committee: its validators and resource-control policy.
273    pub async fn current_committee(&self) -> Result<Committee, Error> {
274        #[derive(serde::Deserialize)]
275        struct CommitteeResponse {
276            validators: BTreeMap<ValidatorPublicKey, ValidatorState>,
277            policy: ResourceControlPolicy,
278        }
279
280        #[derive(serde::Deserialize)]
281        #[serde(rename_all = "camelCase")]
282        struct Response {
283            current_committee: CommitteeResponse,
284        }
285
286        let response = self
287            .query::<Response>(
288                "query { currentCommittee { \
289                    validators \
290                    policy \
291                } }",
292            )
293            .await?;
294
295        let committee_response = response.current_committee;
296
297        Ok(Committee::new(
298            committee_response.validators,
299            committee_response.policy,
300        )?)
301    }
302}