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