Drops the ~60 ticket-prefix comments (CRIT-N, HIGH-N, MED-N, LOW-N, L-N, M-N, AUDIT-N, PLUTUS-N, "audit fix (date):", "Phase N" labels, "Adversarial-review fix:") that had accumulated in inline + doc comments over several audit cycles. Where the surrounding prose still carried useful WHY context it gets kept and tightened; where the ticket WAS the comment it gets dropped entirely. No logic, no renames, no behavior change. Audit history lives in commit messages and the audits/ tree where it belongs — eternal comments don't need to mirror it. Net 138 LOC shorter. 253 tests pass, no new clippy or fmt warnings.
161 lines
5.9 KiB
Rust
161 lines
5.9 KiB
Rust
//! aldabra — MCP server entry point.
|
|
//!
|
|
//! Speaks MCP over stdio. Any MCP client (e.g. Claude Code)
|
|
//! launches this as a subprocess and gets a wallet's worth of tools.
|
|
//!
|
|
//! Logging: stderr only — stdout is the MCP transport and must stay
|
|
//! clean.
|
|
|
|
mod bootstrap;
|
|
mod config;
|
|
mod tools;
|
|
|
|
use std::process::ExitCode;
|
|
|
|
use anyhow::Result;
|
|
use rmcp::{transport::stdio, ServiceExt};
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
use crate::config::Config;
|
|
use crate::tools::WalletService;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> ExitCode {
|
|
tracing_subscriber::fmt()
|
|
.with_writer(std::io::stderr)
|
|
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
|
|
.init();
|
|
|
|
match run().await {
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
Err(e) => {
|
|
tracing::error!("{e:#}");
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn run() -> Result<()> {
|
|
let cfg = Config::load()?;
|
|
tracing::info!(
|
|
network = ?cfg.network,
|
|
koios = %cfg.koios_base,
|
|
koios_bearer_set = cfg.koios_bearer.is_some(),
|
|
account = cfg.account,
|
|
index = cfg.index,
|
|
data_dir = %cfg.data_dir.display(),
|
|
safe_reads_root = %cfg.safe_reads_root.display(),
|
|
"aldabra starting"
|
|
);
|
|
|
|
// Make sure the sandbox root exists and is daemon-only readable.
|
|
// Tools canonicalize() against this dir to validate `*_path`
|
|
// args, which requires the dir to be a real directory on disk.
|
|
// Chmod is `?`'d not swallowed — if the filesystem refuses it
|
|
// (noexec, selinux, broken mount), fail loudly instead of
|
|
// silently falling back to umask 0o755.
|
|
if !cfg.safe_reads_root.exists() {
|
|
std::fs::create_dir_all(&cfg.safe_reads_root).map_err(|e| {
|
|
anyhow::anyhow!(
|
|
"create safe_reads_root {}: {e}",
|
|
cfg.safe_reads_root.display()
|
|
)
|
|
})?;
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::set_permissions(&cfg.safe_reads_root, std::fs::Permissions::from_mode(0o700))
|
|
.map_err(|e| {
|
|
anyhow::anyhow!(
|
|
"chmod 0o700 safe_reads_root {}: {e}",
|
|
cfg.safe_reads_root.display()
|
|
)
|
|
})?;
|
|
}
|
|
}
|
|
|
|
// CLI mode flags — all out-of-band, before the MCP transport
|
|
// takes over stdio.
|
|
//
|
|
// `--generate-mnemonic` — print a fresh phrase, exit. No disk write.
|
|
// `--bootstrap` — paste an existing phrase, encrypt, derive.
|
|
// `--bootstrap-new` — generate, display, encrypt, derive (one shot).
|
|
// `--bootstrap-from-xprv` — paste a root_xsk1... bech32 (cnode root.prv,
|
|
// cardano-address output), encrypt, derive.
|
|
// Power-user import path for keys that came
|
|
// from outside the BIP-39 mnemonic flow.
|
|
// (none) — load existing, start MCP server.
|
|
let args: Vec<String> = std::env::args().collect();
|
|
let generate_only = args.iter().any(|a| a == "--generate-mnemonic");
|
|
let bootstrap_only = args.iter().any(|a| a == "--bootstrap");
|
|
let bootstrap_new = args.iter().any(|a| a == "--bootstrap-new");
|
|
let bootstrap_from_xprv = args.iter().any(|a| a == "--bootstrap-from-xprv");
|
|
|
|
if generate_only {
|
|
bootstrap::print_fresh_mnemonic()?;
|
|
return Ok(());
|
|
}
|
|
|
|
let mnemonic_path = bootstrap::mnemonic_path(&cfg.data_dir);
|
|
let xprv_path = bootstrap::root_xprv_path(&cfg.data_dir);
|
|
let any_key_exists = mnemonic_path.exists() || xprv_path.exists();
|
|
|
|
// Scope `root` to a block so its XPrv drops + wipes as soon as
|
|
// we've extracted the keys we need.
|
|
let (payment_key, stake_key, address) = {
|
|
let root = if bootstrap_new {
|
|
bootstrap::generate_and_save_root_key(&cfg.data_dir)?
|
|
} else if bootstrap_from_xprv {
|
|
bootstrap::import_root_xprv(&cfg.data_dir)?
|
|
} else if any_key_exists || bootstrap_only {
|
|
// Loads existing (or runs the mnemonic-paste flow on
|
|
// first-run with --bootstrap). load_or_create_root_key
|
|
// itself picks between mnemonic.age and root-xprv.age.
|
|
bootstrap::load_or_create_root_key(&cfg.data_dir)?
|
|
} else {
|
|
anyhow::bail!(
|
|
"no key at {}. run one of:\n \
|
|
`aldabra --bootstrap` (paste existing 24-word phrase)\n \
|
|
`aldabra --bootstrap-new` (generate a fresh wallet)\n \
|
|
`aldabra --bootstrap-from-xprv` (paste a root_xsk1... bech32)",
|
|
cfg.data_dir.display()
|
|
);
|
|
};
|
|
|
|
let address =
|
|
aldabra_core::derive_base_address(&root, cfg.network, cfg.account, cfg.index)?;
|
|
let payment_key = aldabra_core::derive_payment_key(&root, cfg.account, cfg.index);
|
|
let stake_key = aldabra_core::derive_stake_key(&root, cfg.account);
|
|
(payment_key, stake_key, address)
|
|
// root drops here — XPrv::Drop wipes the 96 bytes
|
|
};
|
|
tracing::info!(%address, "derived base address");
|
|
|
|
if bootstrap_only || bootstrap_new {
|
|
eprintln!("aldabra: bootstrap complete. address = {address}");
|
|
return Ok(());
|
|
}
|
|
|
|
// Hand off to the MCP server. From this point on stdin/stdout
|
|
// belong to the JSON-RPC transport — no more eprintln-prompting.
|
|
let service = WalletService::new(
|
|
cfg.network,
|
|
address,
|
|
cfg.koios_base,
|
|
cfg.koios_bearer,
|
|
payment_key,
|
|
stake_key,
|
|
cfg.max_send_lovelace,
|
|
cfg.data_dir.clone(),
|
|
cfg.safe_reads_root.clone(),
|
|
);
|
|
let server = service
|
|
.serve(stdio())
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("rmcp serve failed: {e}"))?;
|
|
server
|
|
.waiting()
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("rmcp wait failed: {e}"))?;
|
|
Ok(())
|
|
}
|