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::{AccountOwner, ChainId},
16};
17use linera_client::config::GenesisConfig;
18use linera_execution::{committee::ValidatorState, Committee, ResourceControlPolicy};
19use linera_version::VersionInfo;
20
21#[derive(Debug, thiserror::Error)]
23#[non_exhaustive]
24pub enum Error {
25 #[error("JSON parsing error: {0:?}")]
27 Json(#[from] serde_json::Error),
28 #[error("GraphQL error: {0:?}")]
30 GraphQl(Vec<serde_json::Value>),
31 #[error("{}", describe_http_failure(.0))]
33 Http(#[from] reqwest::Error),
34 #[error(transparent)]
36 ArithmeticError(#[from] ArithmeticError),
37 #[error("failed to execute query {query:?}: {}", describe_http_failure(.source))]
39 Query {
40 query: String,
42 #[source]
44 source: reqwest::Error,
45 },
46}
47
48fn describe_http_failure(error: &reqwest::Error) -> String {
57 let where_ = match error.url() {
58 Some(url) => format!(" at {url}"),
59 None => String::new(),
60 };
61 if let Some(status) = error.status() {
62 format!("the faucet{where_} returned {status}")
63 } else if error.is_timeout() {
64 format!("the request to the faucet{where_} timed out")
65 } else if error.is_decode() {
66 format!("could not decode the faucet's response{where_}")
67 } else if error.is_body() {
68 format!("the request body sent to the faucet{where_} was rejected")
69 } else {
70 format!("could not reach the faucet{where_}")
71 }
72}
73
74#[derive(Clone, Debug, serde::Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct ClaimOutcome {
78 pub chain_id: ChainId,
80 pub certificate_hash: CryptoHash,
82 pub amount: Amount,
84}
85
86#[derive(Clone, Debug, serde::Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct InitialClaim {
90 pub chain_id: ChainId,
92 pub timestamp: Timestamp,
94}
95
96fn destination_argument(destination: &AccountOwner) -> String {
99 if destination.is_chain() {
100 String::new()
101 } else {
102 format!(", destination: \"{destination}\"")
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct Faucet {
109 url: String,
110}
111
112impl Faucet {
113 pub fn new(url: String) -> Self {
115 Self { url }
116 }
117
118 pub fn url(&self) -> &str {
120 &self.url
121 }
122
123 async fn query<Response: serde::de::DeserializeOwned>(
124 &self,
125 query: impl AsRef<str>,
126 ) -> Result<Response, Error> {
127 let query = query.as_ref();
128
129 #[derive(serde::Deserialize)]
130 struct GraphQlResponse<T> {
131 data: Option<T>,
132 errors: Option<Vec<serde_json::Value>>,
133 }
134
135 let builder = reqwest::ClientBuilder::new();
136
137 #[cfg(not(target_arch = "wasm32"))]
138 let builder = builder.timeout(linera_base::time::Duration::from_secs(30));
139
140 let response: GraphQlResponse<Response> = builder
141 .build()
142 .unwrap()
143 .post(&self.url)
144 .json(&serde_json::json!({
145 "query": query,
146 }))
147 .send()
148 .await
149 .map_err(|source| Error::Query {
150 query: query.to_string(),
151 source,
152 })?
153 .error_for_status()?
154 .json()
155 .await?;
156
157 if let Some(errors) = response.errors {
158 let messages = errors
160 .iter()
161 .filter_map(|error| {
162 error
163 .get("message")
164 .and_then(|msg| msg.as_str())
165 .map(|s| s.to_string())
166 })
167 .collect::<Vec<_>>();
168
169 if messages.is_empty() {
170 Err(Error::GraphQl(errors))
171 } else {
172 Err(Error::GraphQl(vec![serde_json::Value::String(
173 messages.join("; "),
174 )]))
175 }
176 } else {
177 Ok(response
178 .data
179 .expect("no errors present but no data returned"))
180 }
181 }
182
183 pub async fn genesis_config(&self) -> Result<GenesisConfig, Error> {
185 #[derive(serde::Deserialize)]
186 #[serde(rename_all = "camelCase")]
187 struct Response {
188 genesis_config: GenesisConfig,
189 }
190
191 Ok(self
192 .query::<Response>("query { genesisConfig }")
193 .await?
194 .genesis_config)
195 }
196
197 pub async fn version_info(&self) -> Result<VersionInfo, Error> {
199 #[derive(serde::Deserialize)]
200 struct Response {
201 version: VersionInfo,
202 }
203
204 Ok(self.query::<Response>("query { version }").await?.version)
205 }
206
207 pub async fn claim(&self, owner: &AccountOwner) -> Result<ChainDescription, Error> {
210 self.claim_to(owner, &AccountOwner::CHAIN).await
211 }
212
213 pub async fn claim_to(
218 &self,
219 owner: &AccountOwner,
220 destination: &AccountOwner,
221 ) -> Result<ChainDescription, Error> {
222 #[derive(serde::Deserialize)]
223 struct Response {
224 claim: ChainDescription,
225 }
226 Ok(self
227 .query::<Response>(format!(
228 "mutation {{ claim(owner: \"{owner}\"{}) }}",
229 destination_argument(destination)
230 ))
231 .await?
232 .claim)
233 }
234
235 pub async fn daily_claim(&self, owner: &AccountOwner) -> Result<ClaimOutcome, Error> {
239 self.daily_claim_to(owner, &AccountOwner::CHAIN).await
240 }
241
242 pub async fn daily_claim_to(
244 &self,
245 owner: &AccountOwner,
246 destination: &AccountOwner,
247 ) -> Result<ClaimOutcome, Error> {
248 #[derive(serde::Deserialize)]
249 #[serde(rename_all = "camelCase")]
250 struct Response {
251 daily_claim: ClaimOutcome,
252 }
253
254 Ok(self
255 .query::<Response>(format!(
256 "mutation {{ dailyClaim(owner: \"{owner}\"{}) }}",
257 destination_argument(destination)
258 ))
259 .await?
260 .daily_claim)
261 }
262
263 pub async fn initial_claim(&self, owner: &AccountOwner) -> Result<Option<InitialClaim>, Error> {
265 #[derive(serde::Deserialize)]
266 #[serde(rename_all = "camelCase")]
267 struct Response {
268 initial_claim: Option<InitialClaim>,
269 }
270
271 Ok(self
272 .query::<Response>(format!(
273 "query {{ initialClaim(owner: \"{owner}\") {{ chainId timestamp }} }}"
274 ))
275 .await?
276 .initial_claim)
277 }
278
279 pub async fn next_daily_claim(&self, owner: &AccountOwner) -> Result<Option<Timestamp>, Error> {
283 #[derive(serde::Deserialize)]
284 #[serde(rename_all = "camelCase")]
285 struct Response {
286 next_daily_claim: Option<Timestamp>,
287 }
288
289 Ok(self
290 .query::<Response>(format!("query {{ nextDailyClaim(owner: \"{owner}\") }}"))
291 .await?
292 .next_daily_claim)
293 }
294
295 pub async fn current_validators(&self) -> Result<Vec<(ValidatorPublicKey, String)>, Error> {
297 #[derive(serde::Deserialize)]
298 #[serde(rename_all = "camelCase")]
299 struct Validator {
300 public_key: ValidatorPublicKey,
301 network_address: String,
302 }
303
304 #[derive(serde::Deserialize)]
305 #[serde(rename_all = "camelCase")]
306 struct Response {
307 current_validators: Vec<Validator>,
308 }
309
310 Ok(self
311 .query::<Response>("query { currentValidators { publicKey networkAddress } }")
312 .await?
313 .current_validators
314 .into_iter()
315 .map(|validator| (validator.public_key, validator.network_address))
316 .collect())
317 }
318
319 pub async fn current_committee(&self) -> Result<Committee, Error> {
321 #[derive(serde::Deserialize)]
322 struct CommitteeResponse {
323 validators: BTreeMap<ValidatorPublicKey, ValidatorState>,
324 policy: ResourceControlPolicy,
325 }
326
327 #[derive(serde::Deserialize)]
328 #[serde(rename_all = "camelCase")]
329 struct Response {
330 current_committee: CommitteeResponse,
331 }
332
333 let response = self
334 .query::<Response>(
335 "query { currentCommittee { \
336 validators \
337 policy \
338 } }",
339 )
340 .await?;
341
342 let committee_response = response.current_committee;
343
344 Ok(Committee::new(
345 committee_response.validators,
346 committee_response.policy,
347 )?)
348 }
349}