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