Commit graph

7 commits

Author SHA1 Message Date
ffdafc2028 v0.2: 8 chain_* read-only Koios passthrough MCP tools
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_*.
2026-05-05 07:01:32 -07:00
47b63f2024 AUDIT4-1 fix: switch tx_status from Koios /tx_info to /tx_status
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).
2026-05-04 20:45:10 -07:00
05292f182e preprod live-test fixes: 4 real bugs surfaced in real-koios + chain integration
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.
2026-05-04 16:57:40 -07:00
f17479ab92 audit fixes: all 9 findings resolved + wallet generation tooling
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.
2026-05-04 14:52:08 -07:00
dd84303885 phase 2.1-2.4: send path — submit + status, txbuilder, wallet.send, wallet.tx_status
chain backend grew submit_tx (POST /submittx, raw cbor body) and
tx_status (POST /tx_info → Confirmed{block,epoch}|NotFound). serde
tag-based status enum so the mcp tool returns clean json.

new core::tx module: ProtocolParams + InputUtxo + build_signed_payment.
two-pass fee refinement — build unsigned, measure size, add witness
overhead constant (128 bytes for vkey+sig+cbor framing), recompute
real fee, build with final fee, sign once (PrivateKey doesn't impl
Clone in pallas-wallet, so we don't double-sign). change below
min-utxo merges into fee instead of emitting dust.

added pallas-txbuilder + pallas-wallet 0.32 deps. PaymentKey gains
crate-private xprv() accessor; payment_key_to_private converts
ed25519-bip32 XPrv → pallas-wallet PrivateKey::Extended via the
64-byte extended secret bytes.

mcp tools.rs: 4 → 6 tools.
- wallet.send (to_address, lovelace, force) with hard-cap guard
- wallet.tx_status (tx_hash) → status json
SendArgs/TxStatusArgs use schemars derive so rmcp generates proper
input schemas. config.rs adds max_send_lovelace (default 100 ADA,
ALDABRA_MAX_SEND_LOVELACE env override).

37 unit tests. mcp tools/list smoke confirms all 6 tools register
with correct schemas (force defaults false, lovelace required uint64,
to_address required string).

phase 2.5 (native-asset send), 2.6 (cold-sign offline mode), and
2.7 (real preprod smoke against a funded wallet) still open.
2026-05-04 11:18:33 -07:00
bc39148b63 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.
2026-05-04 11:09:00 -07:00
1f1993ed97 rename: sulkta-wallet → aldabra (per Cobb 2026-05-04)
Aldabra giant tortoise (Aldabrachelys gigantea) — endemic to the
Aldabra atoll, up to 250 kg, 150-year lifespan. Long-lived,
defended, slow but unstoppable. Better metaphor for the wallet
than 'sulkta-wallet' which was on-the-tin descriptive.

All renames in one pass:
- repo: Sulkta-Coop/sulkta-wallet → Sulkta-Coop/aldabra (via gitea API)
- workspace dir: sulkta-wallet → aldabra
- crate dirs: wallet-{core,chain,mcp} → aldabra-{core,chain,mcp}
- crate names + path imports in Cargo.toml workspace + each crate
- binary name: sulkta-wallet → aldabra
- README, ROADMAP, docs/architecture: all references swept
2026-05-04 10:11:23 -07:00