phase 1: full read path — bip39 + cip-3 + cip-1852 + koios + age-mnemonic + rmcp
end-to-end working wallet: paste 24-word mnemonic, age-encrypt at rest,
on unlock derive root + payment + stake keys, build cip-19 base address,
serve four tools over mcp stdio (wallet.address, wallet.network,
wallet.balance, wallet.utxos).
deps added: ed25519-bip32 0.4 (pallas only ships raw ed25519, not the
cardano variant of bip32 hd derivation), cryptoxide 0.4 for pbkdf2-hmac-sha512,
age 0.10 for at-rest mnemonic encryption, rpassword 7 for tty-only passphrase
prompts, toml 0.9 for config.toml.
new modules:
- crates/aldabra-core/src/derive.rs — payment + stake key derivation, hash
- crates/aldabra-chain/src/koios.rs — real reqwest impl, asset aggregation
- crates/aldabra-mcp/src/{bootstrap,config,tools}.rs
caught one bug pre-flight: get_balance was clobbering same-asset
quantities across utxos instead of summing. fixed + regression test.
headless support via ALDABRA_PASSPHRASE env (mcp clients own stdin so
the rpassword prompt path can't run). docker secret / systemd
EnvironmentFile sources it in production.
dockerfile: multi-stage rust:1.95-bookworm → debian:bookworm-slim, tini
as pid1, non-root aldabra user, /var/lib/aldabra owned 700.
29 unit tests + 1 ignored live-koios test. preprod smoke test exercised
initialize → tools/list → tools/call wallet.address end-to-end via
piped json-rpc; correct preprod address came back from canonical
abandon-art mnemonic.
phase 2 (send) is next.
This commit is contained in:
parent
edc976e5d9
commit
2b4fdff0c0
14 changed files with 4389 additions and 167 deletions
|
|
@ -1,15 +1,28 @@
|
|||
//! aldabra chain backends — Koios first, Ogmios next.
|
||||
//!
|
||||
//! Trait-first design: the MCP server depends on `ChainBackend`, not on
|
||||
//! a specific implementation. Swapping Koios → Ogmios is a config change.
|
||||
//! Trait-first design: the MCP server depends on [`ChainBackend`], not
|
||||
//! on a specific implementation. Swapping Koios → Ogmios is a config
|
||||
//! change.
|
||||
//!
|
||||
//! ## Phase 1
|
||||
//! Just the trait + a stub `KoiosClient` that returns hardcoded data.
|
||||
//! Real HTTP wired up next pass.
|
||||
//! Phase 1: read-only queries (`get_utxos`, `get_balance`) against
|
||||
//! Koios over HTTPS.
|
||||
//!
|
||||
//! Phase 2 (TODO): submission paths — `submit_tx`, `tx_status`.
|
||||
//!
|
||||
//! ## Backends
|
||||
//!
|
||||
//! - [`koios::KoiosClient`] — Koios REST client (POST `/address_utxos`,
|
||||
//! `/address_info`). Sulkta runs its own Koios on dedicated servers; the
|
||||
//! public `https://api.koios.rest/api/v1` works as a fallback.
|
||||
//! - Ogmios (TODO) — websocket client.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod koios;
|
||||
|
||||
pub use koios::KoiosClient;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ChainError {
|
||||
#[error("network error: {0}")]
|
||||
|
|
@ -23,9 +36,9 @@ pub enum ChainError {
|
|||
}
|
||||
|
||||
/// One UTXO at an address. Multi-asset bundle is a flat map of
|
||||
/// {policy_id+asset_name → quantity} for now; we'll model it more
|
||||
/// strictly when minting lands in phase 3.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
/// `policy_id || asset_name_hex` → quantity for now. We'll model it
|
||||
/// more strictly when minting lands in phase 3.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Utxo {
|
||||
pub tx_hash: String,
|
||||
pub output_index: u32,
|
||||
|
|
@ -36,7 +49,7 @@ pub struct Utxo {
|
|||
}
|
||||
|
||||
/// Aggregated balance at an address.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Balance {
|
||||
pub lovelace: u64,
|
||||
pub assets: std::collections::BTreeMap<String, u64>,
|
||||
|
|
@ -50,48 +63,3 @@ pub trait ChainBackend: Send + Sync {
|
|||
// async fn submit_tx(&self, raw_tx_cbor: &[u8]) -> Result<String, ChainError>;
|
||||
// async fn tx_status(&self, tx_hash: &str) -> Result<TxStatus, ChainError>;
|
||||
}
|
||||
|
||||
/// Stub Koios client. Phase 1: returns deterministic placeholder data
|
||||
/// so the MCP server can be smoke-tested end-to-end without a chain
|
||||
/// dependency. Phase 2: real reqwest calls to a Koios endpoint.
|
||||
pub struct KoiosClient {
|
||||
/// Base URL — typically https://api.koios.rest/api/v1
|
||||
/// or your own self-hosted Koios deployment.
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
impl KoiosClient {
|
||||
pub fn new(base_url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
base_url: base_url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ChainBackend for KoiosClient {
|
||||
async fn get_utxos(&self, _address: &str) -> Result<Vec<Utxo>, ChainError> {
|
||||
// TODO(phase 1): POST /address_utxos with {"_addresses": [<address>]}
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
async fn get_balance(&self, _address: &str) -> Result<Balance, ChainError> {
|
||||
// TODO(phase 1): POST /address_info, sum balances across UTXOs
|
||||
Ok(Balance {
|
||||
lovelace: 0,
|
||||
assets: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn stub_koios_returns_empty() {
|
||||
let client = KoiosClient::new("https://api.koios.rest/api/v1");
|
||||
let bal = client.get_balance("addr1...").await.unwrap();
|
||||
assert_eq!(bal.lovelace, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue