Production-grade Holochain AppWebsocket client wrapper used by the unyt
server-side services (bridge orchestrator, unyt_cli daemon, pricing oracle,
watchtower).
Ham— a connect-once wrapper aroundholochain_client::AppWebsocketthat handles admin-interface discovery, app-interface attach, zome-call signing, and typed msgpack zome calls with an explicit per-request timeout. Signs via lair as the cell's own agent key, committing no capability grant to the chain. The other path, authorizing a throwaway signing key by committing one cap grant per connect, is reachable only by asking for it: a config with neither is refused beforeHam::connectopens a socket, so connecting never writes to a chain no caller asked it to write to.HamConfig::with_signing(LairCredentials, CapGrantOptIn): the lair-or-refuse decision, made here once, so a consumer supplies its inputs rather than rebuilding it.SigningPolicy::resolveis the same decision without a config, for a caller that decides at startup and connects later. See their rustdoc for the rules.errors::is_connection_error(&anyhow::Error) -> bool— string-based classifier that decides whether an error warrants rebuilding the socket (covered by unit tests).errors::is_signing_refusalis its opposite: a config no retry can fix, so the caller should stop rather than wait.reconnect::connect_with_backoff— shutdown-aware exponential-backoff reconnect loop with jitter and log-level escalation.compute_delay_msis exposed as a pure function for testing.shutdown::install_shutdown_handler()— returns aShutdownRx(tokio::sync::watch::Receiver<bool>) that flips totrueon SIGINT or SIGTERM.
use anyhow::Context;
use ham::{
BackoffConfig, CapGrantOptIn, Ham, HamConfig, LairCredentials, connect_with_backoff,
install_shutdown_handler,
};
use std::path::PathBuf;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut shutdown = install_shutdown_handler();
let backoff = BackoffConfig::default();
let cfg = HamConfig::new(30000, 30001, "bridging-app")
.with_request_timeout_secs(120)
.with_signing(
LairCredentials::Node {
conductor_config: PathBuf::from("/etc/holochain/conductor-config.yaml"),
passphrase_file: PathBuf::from("/var/lib/holochain/lair-passphrase"),
},
CapGrantOptIn::Withheld,
)
// Name your own variables or flags here. ham states the fault; the
// caller states which knob to turn.
.context("CONDUCTOR_CONFIG / LAIR_PASSPHRASE_FILE must name a node running an external lair_server")?;
let mut ham = match connect_with_backoff(
|| Ham::connect(cfg.clone()),
&backoff,
&mut shutdown,
).await {
Some(h) => h,
None => return Ok(()),
};
loop {
if *shutdown.borrow() { break }
if let Err(e) = ham.ping().await {
if ham::is_connection_error(&e) {
if let Some(h) = connect_with_backoff(
|| Ham::connect(cfg.clone()),
&backoff,
&mut shutdown,
).await {
ham = h;
} else {
break;
}
}
}
tokio::select! {
_ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {}
_ = shutdown.changed() => break,
}
}
Ok(())
}This crate pins holochain_client = "=0.9.0" exactly (the Holochain 0.7 line). All consumers must align to the same holochain_client version because its types flow across the ham crate boundary. Lair signing additionally uses lair_keystore_api = "0.7.1" (the version holochain_client 0.9.0 resolves) to open the keystore connection for the built-in holochain_client::LairAgentSigner.
The crate emits structured events with stable event field names that
deployment dashboards can alert on:
| Event | Level | When |
|---|---|---|
ham.connecting |
info |
Ham::connect is invoked and the signing path is settled. |
ham.connected |
info |
App websocket connected and signing set up; the signing field is lair (no cap grant) or client (cap grant committed). |
ham.connect.refused |
error |
A reconnect attempt failed because the config has no signing path that avoids writing to the chain. Logged from the first attempt: retrying cannot clear it. |
ham.cap_grant_unused |
info |
The caller permits a capability grant and has lair too, so lair was used and nothing was written. |
ham.cap_grant |
warn |
About to commit the capability grant allow_cap_grant_signing asked for. |
ham.lair_unavailable |
warn |
There is no lair to reach, so the capability grant the caller permitted is taken instead. Credentials that were supplied but cannot be used are fatal and never reach this. |
ham.call_zome |
debug |
Per zome call. |
ham.reconnect.attempt |
warn / error |
Each failed reconnect attempt (error after escalate_after). |
ham.reconnected |
info |
Reconnect succeeded after one or more failed attempts. |
Daemons using connect_with_backoff typically also emit their own
ham.disconnected / ham.probe.failed events at the call sites.
Semver from 0.1.0. Consumers pin rev = "<sha>" (not a tag) so rollouts are
reproducible; tags are cut once a compatible set of consumer updates has
landed.