Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate faucet functionality into linera-faucet crate #3240

Merged
merged 10 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ members = [
"linera-ethereum",
"linera-execution",
"linera-explorer",
"linera-faucet",
"linera-faucet/client",
"linera-faucet/server",
"linera-indexer/example",
"linera-indexer/graphql-client",
"linera-indexer/lib",
Expand Down Expand Up @@ -204,6 +207,9 @@ linera-client = { version = "0.14.0", path = "./linera-client" }
linera-core = { version = "0.14.0", path = "./linera-core", default-features = false }
linera-ethereum = { version = "0.14.0", path = "./linera-ethereum", default-features = false }
linera-execution = { version = "0.14.0", path = "./linera-execution", default-features = false }
linera-faucet = { version = "0.14.0", path = "linera-faucet" }
linera-faucet-client = { version = "0.14.0", path = "linera-faucet/client" }
linera-faucet-server = { version = "0.14.0", path = "linera-faucet/server" }
linera-indexer = { version = "0.14.0", path = "./linera-indexer/lib" }
linera-indexer-graphql-client = { version = "0.14.0", path = "./linera-indexer/graphql-client" }
linera-indexer-plugins = { version = "0.14.0", path = "./linera-indexer/plugins" }
Expand Down
1 change: 1 addition & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ COPY linera-core linera-core
COPY linera-ethereum linera-ethereum
COPY linera-execution linera-execution
COPY linera-explorer linera-explorer
COPY linera-faucet linera-faucet
COPY linera-indexer linera-indexer
COPY linera-rpc linera-rpc
COPY linera-sdk linera-sdk
Expand Down
1 change: 0 additions & 1 deletion linera-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ repository.workspace = true
version.workspace = true

[features]
default = ["fs"]
test = ["linera-views/test", "linera-execution/test"]
benchmark = ["linera-base/test", "dep:linera-sdk"]
wasmer = [
Expand Down
15 changes: 15 additions & 0 deletions linera-faucet/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "linera-faucet"
version.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
license.workspace = true
edition.workspace = true

[dependencies]
linera-base.workspace = true

[dependencies.async-graphql]
workspace = true
optional = true
13 changes: 13 additions & 0 deletions linera-faucet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- cargo-rdme start -->

Common definitions for the Linera faucet.

<!-- cargo-rdme end -->

## Contributing

See the [CONTRIBUTING](../CONTRIBUTING.md) file for how to help out.

## License

This project is available under the terms of the [Apache 2.0 license](../LICENSE).
18 changes: 18 additions & 0 deletions linera-faucet/client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "linera-faucet-client"
version.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
license.workspace = true
edition.workspace = true

[dependencies]
anyhow.workspace = true
linera-base.workspace = true
linera-client.workspace = true
linera-execution.workspace = true
linera-faucet = { version = "0.14.0", path = ".." }
linera-version.workspace = true
reqwest.workspace = true
serde_json.workspace = true
13 changes: 13 additions & 0 deletions linera-faucet/client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- cargo-rdme start -->

This module provides the executables needed to operate a Linera service, including a placeholder wallet acting as a GraphQL service for user interfaces.

<!-- cargo-rdme end -->

## Contributing

See the [CONTRIBUTING](../CONTRIBUTING.md) file for how to help out.

## License

This project is available under the terms of the [Apache 2.0 license](../LICENSE).
212 changes: 212 additions & 0 deletions linera-faucet/client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// Copyright (c) Zefchain Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! The client component of the Linera faucet.

use anyhow::{bail, Context, Result};
use linera_base::identifiers::Owner;
use linera_client::config::GenesisConfig;
use linera_execution::committee::ValidatorName;
use linera_faucet::ClaimOutcome;
use linera_version::VersionInfo;
use serde_json::{json, Value};

fn truncate_query_output(input: &str) -> String {
let max_len = 200;
if input.len() < max_len {
input.to_string()
} else {
format!("{} ...", input.get(..max_len).unwrap())
}
}

fn reqwest_client() -> reqwest::Client {
let builder = reqwest::ClientBuilder::new();

#[cfg(not(target_arch = "wasm32"))]
let builder = builder.timeout(std::time::Duration::from_secs(30));

builder.build().unwrap()
}

/// A faucet instance that can be queried.
#[derive(Debug, Clone)]
pub struct Faucet {
url: String,
}

impl Faucet {
pub fn new(url: String) -> Self {
Self { url }
}

pub fn url(&self) -> &str {
&self.url
}

pub async fn genesis_config(&self) -> Result<GenesisConfig> {
let query = "query { genesisConfig }";
let client = reqwest_client();
let response = client
.post(&self.url)
.json(&json!({ "query": query }))
.send()
.await
.context("genesis_config: failed to post query")?;
anyhow::ensure!(
response.status().is_success(),
"Query \"{}\" failed: {}",
query,
response
.text()
.await
.unwrap_or_else(|error| format!("Could not get response text: {error}"))
);
let mut value: Value = response.json().await.context("invalid JSON")?;
if let Some(errors) = value.get("errors") {
bail!("Query \"{}\" failed: {}", query, errors);
}
serde_json::from_value(value["data"]["genesisConfig"].take())
.context("could not parse genesis config")
}

pub async fn version_info(&self) -> Result<VersionInfo> {
let query =
"query { version { crateVersion gitCommit gitDirty rpcHash graphqlHash witHash } }";
let client = reqwest_client();
let response = client
.post(&self.url)
.json(&json!({ "query": query }))
.send()
.await
.context("version_info: failed to post query")?;
anyhow::ensure!(
response.status().is_success(),
"Query \"{}\" failed: {}",
query,
response
.text()
.await
.unwrap_or_else(|error| format!("Could not get response text: {error}"))
);
let mut value: Value = response.json().await.context("invalid JSON")?;
if let Some(errors) = value.get("errors") {
bail!("Query \"{}\" failed: {}", query, errors);
}
let crate_version = serde_json::from_value(value["data"]["version"]["crateVersion"].take())
.context("could not parse crate version")?;
let git_commit = serde_json::from_value(value["data"]["version"]["gitCommit"].take())
.context("could not parse git commit")?;
let git_dirty = serde_json::from_value(value["data"]["version"]["gitDirty"].take())
.context("could not parse git dirty")?;
let rpc_hash = serde_json::from_value(value["data"]["version"]["rpcHash"].take())
.context("could not parse rpc hash")?;
let graphql_hash = serde_json::from_value(value["data"]["version"]["graphqlHash"].take())
.context("could not parse graphql hash")?;
let wit_hash = serde_json::from_value(value["data"]["version"]["witHash"].take())
.context("could not parse wit hash")?;
Ok(VersionInfo {
crate_version,
git_commit,
git_dirty,
rpc_hash,
graphql_hash,
wit_hash,
})
}

pub async fn claim(&self, owner: &Owner) -> Result<ClaimOutcome> {
let query = format!(
"mutation {{ claim(owner: \"{owner}\") {{ \
messageId chainId certificateHash \
}} }}"
);
let client = reqwest_client();
let response = client
.post(&self.url)
.json(&json!({ "query": &query }))
.send()
.await
.with_context(|| {
format!(
"claim: failed to post query={}",
truncate_query_output(&query)
)
})?;
anyhow::ensure!(
response.status().is_success(),
"Query \"{}\" failed: {}",
query,
response
.text()
.await
.unwrap_or_else(|error| format!("Could not get response text: {error}"))
);
let value: Value = response.json().await.context("invalid JSON")?;
if let Some(errors) = value.get("errors") {
bail!("Query \"{}\" failed: {}", query, errors);
}
let data = &value["data"]["claim"];
let message_id = data["messageId"]
.as_str()
.context("message ID not found")?
.parse()
.context("could not parse message ID")?;
let chain_id = data["chainId"]
.as_str()
.context("chain ID not found")?
.parse()
.context("could not parse chain ID")?;
let certificate_hash = data["certificateHash"]
.as_str()
.context("Certificate hash not found")?
.parse()
.context("could not parse certificate hash")?;
let outcome = ClaimOutcome {
message_id,
chain_id,
certificate_hash,
};
Ok(outcome)
}

pub async fn current_validators(&self) -> Result<Vec<(ValidatorName, String)>> {
let query = "query { currentValidators { name networkAddress } }";
let client = reqwest_client();
let response = client
.post(&self.url)
.json(&json!({ "query": query }))
.send()
.await
.context("current_validators: failed to post query")?;
anyhow::ensure!(
response.status().is_success(),
"Query \"{}\" failed: {}",
query,
response
.text()
.await
.unwrap_or_else(|error| format!("Could not get response text: {error}"))
);
let mut value: Value = response.json().await.context("invalid JSON")?;
if let Some(errors) = value.get("errors") {
bail!("Query \"{}\" failed: {}", query, errors);
}
let validators = match value["data"]["currentValidators"].take() {
serde_json::Value::Array(validators) => validators,
validators => bail!("{validators} is not an array"),
};
validators
.into_iter()
.map(|mut validator| {
let name = serde_json::from_value::<ValidatorName>(validator["name"].take())
.context("could not parse current validators: invalid name")?;
let addr = validator["networkAddress"]
.as_str()
.context("could not parse current validators: invalid address")?
.to_string();
Ok((name, addr))
})
.collect()
}
}
Loading