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::{crypto::ValidatorPublicKey, data_types::ChainDescription};
11use linera_client::config::GenesisConfig;
12use linera_execution::{committee::ValidatorState, Committee, ResourceControlPolicy};
13use linera_version::VersionInfo;
14use thiserror_context::Context;
15
16#[derive(Debug, thiserror::Error)]
17#[non_exhaustive]
18pub enum ErrorInner {
19    #[error("JSON parsing error: {0:?}")]
20    Json(#[from] serde_json::Error),
21    #[error("GraphQL error: {0:?}")]
22    GraphQl(Vec<serde_json::Value>),
23    #[error("HTTP error: {0:?}")]
24    Http(#[from] reqwest::Error),
25}
26
27thiserror_context::impl_context!(Error(ErrorInner));
28
29/// A faucet instance that can be queried.
30#[derive(Debug, Clone)]
31pub struct Faucet {
32    url: String,
33}
34
35impl Faucet {
36    pub fn new(url: String) -> Self {
37        Self { url }
38    }
39
40    pub fn url(&self) -> &str {
41        &self.url
42    }
43
44    async fn query<Response: serde::de::DeserializeOwned>(
45        &self,
46        query: impl AsRef<str>,
47    ) -> Result<Response, Error> {
48        let query = query.as_ref();
49
50        #[derive(serde::Deserialize)]
51        struct GraphQlResponse<T> {
52            data: Option<T>,
53            errors: Option<Vec<serde_json::Value>>,
54        }
55
56        let builder = reqwest::ClientBuilder::new();
57
58        #[cfg(not(target_arch = "wasm32"))]
59        let builder = builder.timeout(linera_base::time::Duration::from_secs(30));
60
61        let response: GraphQlResponse<Response> = builder
62            .build()
63            .unwrap()
64            .post(&self.url)
65            .json(&serde_json::json!({
66                "query": query,
67            }))
68            .send()
69            .await
70            .with_context(|| format!("executing query {query:?}"))?
71            .error_for_status()?
72            .json()
73            .await?;
74
75        if let Some(errors) = response.errors {
76            // Extract just the error messages, ignore locations and path
77            let messages = errors
78                .iter()
79                .filter_map(|error| {
80                    error
81                        .get("message")
82                        .and_then(|msg| msg.as_str())
83                        .map(|s| s.to_string())
84                })
85                .collect::<Vec<_>>();
86
87            if messages.is_empty() {
88                Err(ErrorInner::GraphQl(errors).into())
89            } else {
90                Err(
91                    ErrorInner::GraphQl(vec![serde_json::Value::String(messages.join("; "))])
92                        .into(),
93                )
94            }
95        } else {
96            Ok(response
97                .data
98                .expect("no errors present but no data returned"))
99        }
100    }
101
102    pub async fn genesis_config(&self) -> Result<GenesisConfig, Error> {
103        #[derive(serde::Deserialize)]
104        #[serde(rename_all = "camelCase")]
105        struct Response {
106            genesis_config: GenesisConfig,
107        }
108
109        Ok(self
110            .query::<Response>("query { genesisConfig }")
111            .await?
112            .genesis_config)
113    }
114
115    pub async fn version_info(&self) -> Result<VersionInfo, Error> {
116        #[derive(serde::Deserialize)]
117        struct Response {
118            version: VersionInfo,
119        }
120
121        Ok(self.query::<Response>("query { version }").await?.version)
122    }
123
124    pub async fn claim(
125        &self,
126        owner: &linera_base::identifiers::AccountOwner,
127    ) -> Result<ChainDescription, Error> {
128        #[derive(serde::Deserialize)]
129        struct Response {
130            claim: ChainDescription,
131        }
132        Ok(self
133            .query::<Response>(format!("mutation {{ claim(owner: \"{owner}\") }}"))
134            .await?
135            .claim)
136    }
137
138    pub async fn current_validators(&self) -> Result<Vec<(ValidatorPublicKey, String)>, Error> {
139        #[derive(serde::Deserialize)]
140        #[serde(rename_all = "camelCase")]
141        struct Validator {
142            public_key: ValidatorPublicKey,
143            network_address: String,
144        }
145
146        #[derive(serde::Deserialize)]
147        #[serde(rename_all = "camelCase")]
148        struct Response {
149            current_validators: Vec<Validator>,
150        }
151
152        Ok(self
153            .query::<Response>("query { currentValidators { publicKey networkAddress } }")
154            .await?
155            .current_validators
156            .into_iter()
157            .map(|validator| (validator.public_key, validator.network_address))
158            .collect())
159    }
160
161    pub async fn current_committee(&self) -> Result<Committee, Error> {
162        #[derive(serde::Deserialize)]
163        struct CommitteeResponse {
164            validators: BTreeMap<ValidatorPublicKey, ValidatorState>,
165            policy: ResourceControlPolicy,
166        }
167
168        #[derive(serde::Deserialize)]
169        #[serde(rename_all = "camelCase")]
170        struct Response {
171            current_committee: CommitteeResponse,
172        }
173
174        let response = self
175            .query::<Response>(
176                "query { currentCommittee { \
177                    validators \
178                    policy \
179                } }",
180            )
181            .await?;
182
183        let committee_response = response.current_committee;
184
185        Ok(Committee::new(
186            committee_response.validators,
187            committee_response.policy,
188        ))
189    }
190}