Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
82521fa
feat: symlink `hydra-node` next to our binary
michalrus Dec 18, 2025
3517462
chore: add `feature = "dev_mock_db"` for local testing
michalrus Dec 18, 2025
b01c767
chore: make `config/development.toml` use Preview by default
michalrus Dec 18, 2025
a5e8c47
feat: add `mod find_libexec`
michalrus Dec 18, 2025
a6ecb22
chore: add `cardano-cli` to the `devshell`
michalrus Dec 18, 2025
aa0bb57
chore: update `crate::find_libexec` to match the Platform’s
michalrus Dec 18, 2025
626a5df
chore: define all modules in `src/lib.rs`, not `src/main.rs`
michalrus Dec 18, 2025
5de02c4
chore: make the Hydra code from Platform compile in Gateway
michalrus Dec 18, 2025
ba8eb23
feat: “exchange” the Hydra keys in the Gateway
michalrus Dec 18, 2025
20c372c
chore: update `rustPackages`, and use the same `rustfmt.toml` as the …
michalrus Dec 18, 2025
03f3723
chore: rename `HydraManager` → `HydraController`
michalrus Dec 18, 2025
1a6a629
feat: add `crate::hydra::HydrasManager`
michalrus Dec 18, 2025
0928b80
chore: prefix the Hydra persistence directory with network and IceBre…
michalrus Dec 18, 2025
78a779e
chore: add another, richer, shared `struct HydraConfig`
michalrus Dec 18, 2025
87281f0
feat: perform the key exchange
michalrus Dec 18, 2025
2012da1
chore: add a `kex_done: bool` marker to KEx response
michalrus Dec 18, 2025
4716940
feat: make the Hydras connect
michalrus Dec 18, 2025
20a3c1c
feat: add `hydra::tunnel::*`
michalrus Dec 18, 2025
125f24f
feat: make sure there’s only a single `hydra-node` per WebSocket
michalrus Dec 18, 2025
c76fa7e
feat: verify `hydra_head_peers_connected`
michalrus Dec 18, 2025
eb3bb3d
feat: initialize the Hydra head
michalrus Dec 18, 2025
11e59f7
feat: add and verify commit/flush config values
michalrus Dec 18, 2025
3931df8
feat: check that we have enough funds for a commit tx
michalrus Dec 18, 2025
56c2ac4
feat: commit specified funds to the Hydra Head
michalrus Dec 18, 2025
dc6c5a0
chore: add `originator` to a few more traces
michalrus Dec 18, 2025
a693605
feat: account for handled requests
michalrus Dec 18, 2025
381f1e5
feat: send the Hydra transaction
michalrus Dec 18, 2025
c45ed1a
feat: finish the Hydra flow
michalrus Dec 18, 2025
03cca38
fix: a few bugs
michalrus Dec 18, 2025
818d3e7
fix: evaluation on `aarch64-linux`
michalrus Dec 18, 2025
4bf4700
fix: export `HYDRA_SCRIPTS_TX_ID_MAINNET` in pure Cargo build, too
michalrus Dec 18, 2025
699e2b9
chore: set `CARDANO_NODE_NETWORK_ID` etc. in Rust
michalrus Dec 18, 2025
a2c09fd
fix: a few Hydra (CI) jobs
michalrus Dec 18, 2025
e77f223
fix: the `cargo-deny` check
michalrus Dec 18, 2025
b6342dc
fix: the `cargo-clippy` check
michalrus Dec 18, 2025
7012728
feat: add a better, more self-contained TCP tunnelling
michalrus Dec 19, 2025
108d8c9
feat: don’t set up the TCP-over-WebSocket tunnels if running on the s…
michalrus Dec 19, 2025
35a134b
fix: `cargo-deny` and `cargo-clippy`
michalrus Dec 19, 2025
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
526 changes: 452 additions & 74 deletions Cargo.lock

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ publish = false
edition = "2021"
build = "build.rs"

[features]
default = []
dev_mock_db = []

[dependencies]
anyhow = "1.0.98"
axum = { version = "0.7.5", features = ["ws"] }
tokio = { version = "1.39.2", features = ["full"] }
tokio-util = "0.7"
futures = "0.3"
futures-util = "0.3"
kameo = "0.19"
bytes = "1"
tokio-tungstenite = "0.24"
tungstenite = "0.24"
tracing = "0.1.40"
Expand All @@ -22,6 +30,7 @@ serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.122"
colored = "2.1.0"
clap = { version = "4.5.14", features = ["derive"] }
dirs = "6.0.0"
toml = "0.9.5"
thiserror = "1.0.63"
chrono = { version = "0.4.3", features = ["serde"] }
Expand All @@ -33,10 +42,24 @@ rand = "0.8.5"
base64 = "0.21"
uuid = "1.10"
hyper = "1.4.1"
machine-uid = "0.5"
blake3 = "1"
getrandom = "0.3"

[target.'cfg(unix)'.dependencies]
nix = { version = "0.30", default-features = false, features = ["signal"] }

[lib]
name = "blockfrost_gateway"
path = "src/lib.rs"

[dev-dependencies]
rstest = "0.26.1"

[build-dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", default-features = false, features = [
"blocking",
"rustls-tls",
] }
170 changes: 169 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
fn main() {
git_revision::set();
hydra_scripts_id::set();
}

mod git_revision {
Expand Down Expand Up @@ -27,9 +28,176 @@ mod git_revision {
.args(["rev-parse", "HEAD"])
.output()
.expect("git-rev-parse");
String::from_utf8_lossy(&git_rev_parse.stdout).trim().to_string()
String::from_utf8_lossy(&git_rev_parse.stdout)
.trim()
.to_string()
};

println!("cargo:rustc-env={}={}", GIT_REVISION, revision);
}
}

mod hydra_scripts_id {
use std::{
collections::HashMap,
env, fs,
path::{Path, PathBuf},
time::Duration,
};

pub fn set() {
println!("cargo:rerun-if-env-changed=HYDRA_SCRIPTS_TX_ID_MAINNET");
println!("cargo:rerun-if-env-changed=HYDRA_SCRIPTS_TX_ID_PREPROD");
println!("cargo:rerun-if-env-changed=HYDRA_SCRIPTS_TX_ID_PREVIEW");

// If user already provided the values at build time, honor them and avoid network.
if let (Ok(m), Ok(p), Ok(v)) = (
env::var("HYDRA_SCRIPTS_TX_ID_MAINNET"),
env::var("HYDRA_SCRIPTS_TX_ID_PREPROD"),
env::var("HYDRA_SCRIPTS_TX_ID_PREVIEW"),
) {
set_envs(&m, &p, &v, None, None);
return;
}

let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let flake_lock = find_upwards(&manifest_dir, "flake.lock").unwrap_or_else(|| {
panic!(
"Could not find flake.lock by walking up from {}",
manifest_dir.display()
)
});

println!("cargo:rerun-if-changed={}", flake_lock.display());

let flake_lock_json = fs::read_to_string(&flake_lock)
.unwrap_or_else(|e| panic!("Failed to read {}: {e}", flake_lock.display()));

let (rev, href) = read_hydra_rev_and_ref(&flake_lock_json);

let url = format!(
"https://raw.githubusercontent.com/cardano-scaling/hydra/{}/hydra-node/networks.json",
rev
);

let networks_json = fetch_cached(&url, &rev);
let networks_map: HashMap<String, HashMap<String, String>> =
serde_json::from_str(&networks_json).unwrap_or_else(|e| {
panic!("Failed to parse networks.json downloaded from {url}: {e}")
});

let mainnet = lookup(&networks_map, "mainnet", &href);
let preprod = lookup(&networks_map, "preprod", &href);
let preview = lookup(&networks_map, "preview", &href);

set_envs(&mainnet, &preprod, &preview, Some(&rev), Some(&href));
}

fn set_envs(
mainnet: &str,
preprod: &str,
preview: &str,
rev: Option<&str>,
href: Option<&str>,
) {
// These are what your env!("...") will see.
println!("cargo:rustc-env=HYDRA_SCRIPTS_TX_ID_MAINNET={mainnet}");
println!("cargo:rustc-env=HYDRA_SCRIPTS_TX_ID_PREPROD={preprod}");
println!("cargo:rustc-env=HYDRA_SCRIPTS_TX_ID_PREVIEW={preview}");

// Extra metadata (optional, but often handy)
if let Some(rev) = rev {
println!("cargo:rustc-env=HYDRA_INPUT_REV={rev}");
}
if let Some(href) = href {
println!("cargo:rustc-env=HYDRA_INPUT_REF={href}");
}
}

fn read_hydra_rev_and_ref(flake_lock_json: &str) -> (String, String) {
let v: serde_json::Value = serde_json::from_str(flake_lock_json)
.unwrap_or_else(|e| panic!("Failed to parse flake.lock JSON: {e}"));

let rev = v
.pointer("/nodes/hydra/locked/rev")
.and_then(|x| x.as_str())
.unwrap_or_else(|| panic!("flake.lock missing /nodes/hydra/locked/rev"))
.to_string();

let href = v
.pointer("/nodes/hydra/original/ref")
.and_then(|x| x.as_str())
.unwrap_or_else(|| panic!("flake.lock missing /nodes/hydra/original/ref"))
.to_string();

(rev, href)
}

fn lookup(
networks: &HashMap<String, HashMap<String, String>>,
network: &str,
href: &str,
) -> String {
networks
.get(network)
.unwrap_or_else(|| panic!("networks.json missing top-level key {network:?}"))
.get(href)
.cloned()
.unwrap_or_else(|| {
let mut versions: Vec<_> = networks[network].keys().cloned().collect();
versions.sort();
panic!(
"networks.json has no entry for network {network:?} version/ref {href:?}. \
Available versions: {}",
versions.join(", ")
)
})
}

fn fetch_cached(url: &str, rev: &str) -> String {
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
let cache_path = out_dir.join(format!("hydra-networks.{rev}.json"));

// If it's already in OUT_DIR, reuse it (build scripts can run a lot).
if let Ok(s) = fs::read_to_string(&cache_path) {
return s;
}

let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(20))
.user_agent("cargo-build-script (hydra networks.json fetch)")
.build()
.expect("Failed to build reqwest client");

let resp = client
.get(url)
.send()
.unwrap_or_else(|e| panic!("Failed to GET {url}: {e}"));

if !resp.status().is_success() {
panic!("GET {url} failed with status {}", resp.status());
}

let text = resp
.text()
.unwrap_or_else(|e| panic!("Failed to read response body from {url}: {e}"));

// Best-effort cache; ignore failures.
let _ = fs::write(&cache_path, &text);

text
}

fn find_upwards(start: &Path, file_name: &str) -> Option<PathBuf> {
let mut dir = Some(start);

while let Some(d) = dir {
let candidate = d.join(file_name);
if candidate.is_file() {
return Some(candidate);
}
dir = d.parent();
}
None
}
}
11 changes: 10 additions & 1 deletion config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,14 @@ url = 'https://api.domain.com'
connection_string = 'postgresql://user:pass@host:port/db'

[blockfrost]
project_id = 'BLOCKFROST_PROJECT_ID'
project_id = 'preview_BLOCKFROST_PROJECT_ID'
nft_asset = '4213fc3eac8c781ac85514dd1de9aaabcd5a3a81cc2df4f413b9b295'

[hydra]
max_concurrent_hydra_nodes = 2
cardano_signing_key = "/home/mw/.config/blockfrost-platform/hydra/tmp_their_keys/payment.sk"
node_socket_path = "/home/mw/.local/share/blockfrost-platform/preview/node.socket"
commit_ada = 3.0
lovelace_per_request = 100_000
requests_per_microtransaction = 10
microtransactions_per_fanout = 2
19 changes: 18 additions & 1 deletion deny.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
[licenses]
# See <https://spdx.org/licenses/> for list of possible licenses
allow = ["Apache-2.0", "BSD-3-Clause", "MIT", "MPL-2.0", "Zlib", "Unicode-3.0"]
allow = [
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"MIT",
"MPL-2.0",
"Zlib",
"Unicode-3.0",
"ISC",
"CDLA-Permissive-2.0",
"OpenSSL",
]
private = { ignore = true }
confidence-threshold = 0.8

[[licenses.clarify]]
name = "ring"
version = "0.17.8"
expression = "Apache-2.0 AND ISC AND MIT AND OpenSSL"
license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
Loading