linera_faucet_client/
lib.rs1#![deny(missing_docs)]
7
8use 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#[derive(Debug, thiserror::Error)]
24#[non_exhaustive]
25pub enum ErrorInner {
26 #[error("JSON parsing error: {0:?}")]
28 Json(#[from] serde_json::Error),
29 #[error("GraphQL error: {0:?}")]
31 GraphQl(Vec<serde_json::Value>),
32 #[error("HTTP error: {0:?}")]
34 Http(#[from] reqwest::Error),
35 #[error(transparent)]
37 ArithmeticError(#[from] ArithmeticError),
38}
39
40pub use error::Error;
41
42mod error {
43 #![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#[derive(Clone, Debug, serde::Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct ClaimOutcome {
59 pub chain_id: ChainId,
61 pub certificate_hash: CryptoHash,
63 pub amount: Amount,
65}
66
67#[derive(Clone, Debug, serde::Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct InitialClaim {
71 pub chain_id: ChainId,
73 pub timestamp: Timestamp,
75}
76
77#[derive(Debug, Clone)]
79pub struct Faucet {
80 url: String,
81}
82
83impl Faucet {
84 pub fn new(url: String) -> Self {
86 Self { url }
87 }
88
89 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 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 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 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 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 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 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 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 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 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}