discovered during preprod smoke 2026-05-04 — 7 txs submitted (3 sends, 2 mints, 1 cip68 nft mint, 1 burn). all confirmed on chain. unit-test coverage missed these because hand-crafted koios fixtures didn't match real-world response shapes. bugs: PREPROD-1 (HIGH) — KoiosUtxo::asset_list deserializer rejected `null`. real /address_utxos returns asset_list:null for ada-only utxos (vs /address_info which returns []). Vec<T> can't deserialize null, killing the entire utxo response. Option<Vec<T>>.unwrap_or_default fixes it + new regression test deserializes_utxo_with_null_asset_list locks it in. PREPROD-2 (HIGH) — /address_utxos needs `_extended: true` to populate asset_list. without it, koios returns asset_list:[] (or null) for asset-bearing utxos, making the wallet think it has zero of its own tokens. native-asset send fails with "insufficient asset". new AddressesExtendedBody serializer; get_utxos sets _extended=true. PREPROD-3 (MEDIUM) — wallet_mint_cip68_nft default lovelace was 1.5 ADA but the babbage min-utxo formula for inline-datum-bearing outputs clears ~1.79 ADA. chain rejected with BabbageOutputTooSmallUTxO. bumped default_token_lovelace 1_500_000 → 2_500_000 (covers typical cip-68 metadata; large metadata still requires caller override). PREPROD-4 (LOW, audit-process) — submit_tx error path called .error_for_status() which discards koios's response body. chain-rule rejections came through as bare HTTP codes, no diagnostic. now we capture status + body before checking; rejections include the actual ledger error (e.g. BabbageOutputTooSmallUTxO with the offending coin amounts) so future debugging is one-shot. 7 successful preprod txs: - e3e52cf9 self-send 3 ADA - 397fe6b7 self-send 5 ADA via cold-sign flow (build_unsigned → tx_summary → sign_partial → submit_signed_tx; predicted tx_hash matched submitted tx_hash, body invariant under signing confirmed) - d23e4c60 mint 100 ALDABRA_TEST with CIP-25 metadata - 25cc489c mint cip-68 nft pair (ref label 100 + user label 222) - 2ce72b6f mint 50 more ALDABRA_TEST via unsigned-mint flow - 19a909df native-asset send (25 ALDABRA_TEST + 5 ADA) - f949d29c burn 10 ALDABRA_TEST (negative-quantity mint) guards verified: - max_send_lovelace cap rejects 200 ADA without force ✓ - mint with insufficient holdings rejected with clear error ✓ - mcp tool names with dots silently dropped by Claude Code validator (already fixed in previous commit by renaming to underscore-only) 94 unit tests pass. |
||
|---|---|---|
| crates | ||
| docs | ||
| .dockerignore | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| Dockerfile | ||
| LICENSE | ||
| README.md | ||
| ROADMAP.md | ||
aldabra
Rust-native Cardano lite wallet with an MCP-server interface — built for LLM-first usage (send, receive, mint, Plutus interaction).
Status: Phase 1 scaffold (2026-05-04). Compiles, structure in place, real wallet primitives still landing. See
ROADMAP.md.
Why
The existing Cardano MCP servers are either read-only doc gateways (Jimmyh-world/Cardano_MCP) or built on Blockfrost (web3-mcp) which is a centralized API we deliberately don't depend on. Sulkta runs its own Koios + Ogmios endpoints on Rackham; we want a wallet that talks directly to those.
Also: it's the first Sulkta Rust project — useful as a workout for
crafting-table's Rust toolchain (per Sulkta-Coop/lucy-infra
spec-crafting-table.md).
Architecture
Three crates in a Cargo workspace:
| Crate | Responsibility |
|---|---|
aldabra-core |
Pure crypto + types. Mnemonic → root key (CIP-3), root → payment + stake key (CIP-1852), address construction, signing. No I/O, no network. This is the security boundary. |
aldabra-chain |
Pluggable backends for chain queries. ChainBackend trait, with Koios as the phase-1 implementation. Ogmios + submission paths in phase 2. |
aldabra-mcp |
Binary. MCP server speaking stdio. Glues core + chain together, exposes tools to the LLM client. |
┌─────────────────────────────┐
LLM client │ aldabra-mcp (bin) │ stdio
─────────► │ tool handlers, lifecycle │ ────►
└──────────┬──────────────────┘
│
┌────────┴────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ aldabra-core │ │ aldabra-chain │
│ keys, sign │ │ Koios/Ogmios │
└──────────────┘ └──────────────┘
MCP tools (target)
Phase 1:
wallet.address— derived base address at account 0, index 0wallet.balance— ADA + native asset balance at the wallet's addresswallet.utxos— list UTXOs
Phase 2:
wallet.send— build, sign, submit a payment (ADA or native)wallet.tx_status— poll a submitted tx hash
Phase 3:
wallet.mint— mint a CIP-25 / CIP-68 native assetwallet.policy.create— generate a policy script (timelock, multisig)
Phase 4:
wallet.script.attach— attach an inline datum + reference scriptwallet.script.spend— spend a Plutus-locked UTXO with redeemerwallet.stake.delegate— delegate to a pool
Build
# Local (requires rustc 1.75+)
cargo build --release
# Through crafting-table (preferred — validates the toolchain there)
crafting-table build aldabra
Run
# Direct invocation (smoke test only — does nothing useful in phase 1)
./target/release/aldabra
# As an MCP server registered with Claude Code:
# add to ~/.claude.json:
# "aldabra": {
# "command": "/path/to/aldabra",
# "env": {
# "ALDABRA_DATA": "/mnt/cache/appdata/aldabra"
# }
# }
Security model
- Mnemonic source: interactive bootstrap on first run, paste once, encrypted at rest with age. Never written to disk in plaintext.
- Derived keys: in-memory only,
ZeroizeOnDropon every container. - Network exposure: stdio MCP transport — never opens a TCP socket. Only the spawning client process can reach it.
- Multi-network: mainnet by default, but
--network preview/--network preprodfor testing without real ADA.
See also
ROADMAP.md— phased buildoutdocs/architecture.md— deeper design notes- txpipe/pallas — the Rust Cardano building blocks we depend on
- Emurgo/cardano-serialization-lib — reference TX builder if pallas-txbuilder doesn't cover something
- modelcontextprotocol/rust-sdk — rmcp, the Rust MCP server SDK we use