phase 1 scaffold: cargo workspace + 3 crates + roadmap + architecture
Repo skeleton for aldabra, the rust-native cardano lite wallet
with MCP server interface. Builds end-to-end, types in place,
real cardano primitives land next pass.
Crates:
wallet-core — pure crypto + types. mnemonic, key derivation,
signing. No I/O. Security boundary.
wallet-chain — pluggable backends. ChainBackend trait, Koios
client (stub for now). Ogmios + submit in phase 2.
wallet-mcp — the binary. stdio MCP transport via rmcp.
Phase plan in ROADMAP.md, threat model in docs/architecture.md.
This is also Sulkta's first Rust project + a real-world workout for
ci-runner's rust toolchain.
This commit is contained in:
commit
56bcceb593
11 changed files with 735 additions and 0 deletions
142
crates/wallet-core/src/lib.rs
Normal file
142
crates/wallet-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
//! aldabra core — keys, addresses, signing.
|
||||
//!
|
||||
//! This crate is the security boundary. Everything that touches private
|
||||
//! key material lives here, and only here. No I/O, no network, no MCP.
|
||||
//!
|
||||
//! ## Layout (target)
|
||||
//!
|
||||
//! - [`mnemonic`] — 24-word BIP-39 input → root key (CIP-3)
|
||||
//! - [`derive`] — Root key → payment + stake key (CIP-1852 paths)
|
||||
//! - [`address`] — Public keys → bech32 addresses (mainnet / testnet)
|
||||
//! - [`signing`] — Sign an unsigned transaction body
|
||||
//!
|
||||
//! ## Phase 1 (this scaffold)
|
||||
//!
|
||||
//! Just types + a placeholder address-derivation function. Real impl
|
||||
//! lands as we wire up `pallas-crypto`'s key derivation API.
|
||||
//!
|
||||
//! ## Memory hygiene rule
|
||||
//!
|
||||
//! Anything that holds a private key MUST `derive(ZeroizeOnDrop)` or
|
||||
//! manually zeroize when going out of scope. Use the `zeroize` crate.
|
||||
//! This is non-negotiable — RAM-resident keys leak via core dumps,
|
||||
//! swap, hibernate state, etc.
|
||||
|
||||
use thiserror::Error;
|
||||
use zeroize::ZeroizeOnDrop;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WalletError {
|
||||
#[error("invalid mnemonic: {0}")]
|
||||
InvalidMnemonic(String),
|
||||
|
||||
#[error("derivation failed: {0}")]
|
||||
Derivation(String),
|
||||
|
||||
#[error("address encoding failed: {0}")]
|
||||
Address(String),
|
||||
|
||||
#[error("not yet implemented (phase 1 scaffold)")]
|
||||
NotYetImplemented,
|
||||
}
|
||||
|
||||
/// A 24-word BIP-39 mnemonic. Held in memory only while deriving keys;
|
||||
/// callers should drop this immediately after `derive_root_key`.
|
||||
#[derive(ZeroizeOnDrop)]
|
||||
pub struct Mnemonic {
|
||||
/// Stored as a single string (joined with spaces). The
|
||||
/// `ZeroizeOnDrop` derive ensures this gets wiped from RAM when
|
||||
/// the struct is dropped.
|
||||
phrase: String,
|
||||
}
|
||||
|
||||
impl Mnemonic {
|
||||
/// Parse a 24-word mnemonic from a whitespace-separated string.
|
||||
/// Validates word count + checksum via the `bip39` crate.
|
||||
///
|
||||
/// # Phase 1
|
||||
/// TODO: wire up `bip39::Mnemonic::parse_in` once we lock the
|
||||
/// API. For now this just stores the phrase verbatim — DO NOT
|
||||
/// rely on validation yet.
|
||||
pub fn from_phrase(phrase: &str) -> Result<Self, WalletError> {
|
||||
// TODO(phase 1): real validation
|
||||
if phrase.split_whitespace().count() != 24 {
|
||||
return Err(WalletError::InvalidMnemonic(
|
||||
"expected 24 words".into(),
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
phrase: phrase.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Derive the Cardano CIP-3 root key from this mnemonic. Consumes
|
||||
/// the mnemonic so the source phrase is dropped + zeroized
|
||||
/// immediately after.
|
||||
pub fn into_root_key(self) -> Result<RootKey, WalletError> {
|
||||
// TODO(phase 1): pallas-crypto's PBKDF2 + entropy + chain code
|
||||
// derivation per CIP-3. Reference:
|
||||
// https://input-output-hk.github.io/cardano-wallet/concepts/master-key-generation
|
||||
Err(WalletError::NotYetImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
/// CIP-3 root key. Holds the seed material from which payment + stake
|
||||
/// keys are derived via CIP-1852 paths. Zeroized on drop.
|
||||
#[derive(ZeroizeOnDrop)]
|
||||
pub struct RootKey {
|
||||
/// 96 bytes per CIP-3 (extended secret + chain code).
|
||||
bytes: [u8; 96],
|
||||
}
|
||||
|
||||
/// Network parameter — bech32 prefix + protocol magic.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Network {
|
||||
Mainnet,
|
||||
Preview,
|
||||
Preprod,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
pub fn bech32_hrp_prefix(&self) -> &'static str {
|
||||
match self {
|
||||
Network::Mainnet => "addr",
|
||||
Network::Preview | Network::Preprod => "addr_test",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive a base address (payment + stake) at account 0, address index 0.
|
||||
///
|
||||
/// # Phase 1
|
||||
/// TODO: real CIP-1852 derivation using pallas-crypto's HD key derivation.
|
||||
/// For now this returns a placeholder that lets the MCP layer be tested
|
||||
/// without real keys.
|
||||
pub fn derive_base_address(
|
||||
_root: &RootKey,
|
||||
network: Network,
|
||||
_account: u32,
|
||||
_index: u32,
|
||||
) -> Result<String, WalletError> {
|
||||
// TODO(phase 1): real implementation
|
||||
let prefix = network.bech32_hrp_prefix();
|
||||
Ok(format!("{prefix}1placeholder_phase_1_scaffold"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mnemonic_word_count_validation() {
|
||||
let too_few = "one two three";
|
||||
assert!(Mnemonic::from_phrase(too_few).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_address_has_network_prefix() {
|
||||
let dummy_root = RootKey { bytes: [0u8; 96] };
|
||||
let addr = derive_base_address(&dummy_root, Network::Mainnet, 0, 0).unwrap();
|
||||
assert!(addr.starts_with("addr1"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue