Adds a parallel read-only API surface alongside wallet_*:
chain_tx_info full Koios tx_info (any hash)
chain_address_info balance + utxos at any address
chain_pool_list filter by ticker / pool_id_bech32
chain_pool_info detail per pool (delegators, blocks)
chain_epoch_params protocol params for an epoch
chain_asset_info supply, holders, mint history
chain_account_info stake address state
chain_tip current chain tip
All passthrough — Koios JSON returned verbatim, no re-shaping.
Network-aware via existing ALDABRA_KOIOS_BASE; mainnet vs preprod
just changes the URL. No keys touched, no signing path. Saves
the bash-curl friction Cobb flagged 2026-05-05 mid-mainnet
testing arc.
Wire-up: KoiosClient gets `post_raw_json` + `get_raw_json`
helpers that return raw response strings instead of decoding
into typed structures. The chain_* tools are thin wrappers
around those.
ServerInfo `instructions` updated to advertise the chain_*
surface alongside wallet_*.
The old impl called Koios /tx_info to learn confirmation state. For
confirmed txs that endpoint streams the full tx body — multi-MB on
complex txs, hundreds of KB on trivial ones — and the public Koios
endpoint either rate-limits or chunks slowly enough to escape our
10s reqwest timeout. Result: wallet_tx_status hung 120s+ and the
container subprocess died, surfaced 2026-05-04 audit-4 phase C7.
Fix: call the lighter /tx_status endpoint, which returns a single
{tx_hash, num_confirmations} record per tx — bytes, not MB.
API change: TxStatus::Confirmed { block_height, epoch } becomes
TxStatus::Confirmed { num_confirmations }. The endpoint doesn't
return block_height / epoch anyway; num_confirmations is what
callers actually want for polling-until-final flows. wallet_tx_status
docstring updated to spell out the three returnable shapes.
Tests: drops the KoiosTxInfo-shape unit tests, adds
parses_koios_tx_status_shapes covering the three live response
shapes we observed (confirmed-with-count, known-but-no-confs,
empty array).
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.
HIGH:
- HIGH-1 enforce_value_cap helper applied to wallet.send,
wallet.mint, wallet.mint.cip68_nft, wallet.script.spend. each
gained a `force` arg; cap also covers the user_lovelace+ref_lovelace
sum on cip68_nft. wallet.stake.delegate skipped (2 ada deposit is
protocol-fixed, not a transfer to a non-wallet destination).
- HIGH-2 wallet.tx_summary mcp tool — read-only decode of a conway
tx cbor → typed TxSummary (inputs, outputs+assets, fee, certs,
mint, witness count, aux-data presence). new aldabra-core::inspect
module. callers MUST run this before wallet.sign_partial /
wallet.submit_signed_tx on any cbor they didn't build themselves.
MEDIUM:
- M-1 zeroize stack-resident extended_bytes after SecretKeyExtended
consumes them. tx.rs::payment_key_to_private + sign.rs::add_witness.
- M-2 atomic 0o600 mnemonic file create via OpenOptions+
OpenOptionsExt. removes the prior toctou window between fs::write
(default umask) and chmod 600.
- M-3 prompt_or_env_passphrase + unlock_passphrase helpers wrap the
passphrase in Zeroizing<String>. ALDABRA_PASSPHRASE env still
unzeroizable in the env block itself (documented headless tradeoff).
- M-4 is_hex_64 validator on submit_tx response — koios error wrapped
in quotes can no longer round-trip as a fake tx_hash.
LOW + cleanup:
- L-1 checked_add for inner sums of checked_sub patterns in tx.rs.
remaining sites (mint.rs, stake.rs, plutus.rs) deferred — same
pattern, can't overflow with realistic cardano amounts but
defensive. picked up next.
- L-2 root key scoped to a block in main.rs — XPrv drops + wipes
after deriving payment_key + stake_key + address. saves ~96 bytes
of secret material lifetime.
- L-3 TxStatus gained a Pending variant for the mempool-but-not-yet-
confirmed case. previously rendered as Confirmed{block_height: None}
which was misleading.
- L-4 .expect("we built this key") → typed ? propagation in
tx.rs::prepare_payment.
- L-5 removed dead fns (build_and_sign, decode_hex) + unused imports.
WALLET GENERATION (audit prompted gap-find):
aldabra had only an import path. no "generate fresh wallet" tool.
- Mnemonic::generate() — bip39::Mnemonic::generate_in(English, 24)
with the rand feature. returns (Mnemonic, Zeroizing<String>) so
the caller can display the phrase once for cold backup.
- aldabra --generate-mnemonic — print fresh phrase, exit. no disk.
- aldabra --bootstrap-new — generate + display + encrypt one-shot.
- bip39 dep gains the rand feature for OsRng-backed generation.
- standard 24-word BIP-39, recoverable from any cardano wallet.
mcp tools: 16 → 17 (added wallet.tx_summary).
unit tests: 88 → 93. cargo audit clean (0 cves), cargo build clean
(0 warnings). all four cli flags smoke-tested:
--generate-mnemonic prints + exits; --bootstrap-new generates +
encrypts + derives a real preprod address; mnemonic.age has 0o600
perms confirmed atomic.
audit doc memory/spec-aldabra-audit-2026-05-04.md updated with
status markers.
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.