Commit graph

32 commits

Author SHA1 Message Date
3a7f536409 feat(dao-config): Phase 4 prerequisite fields + ScriptRefs
DaoConfig gains optional fields for Phase 4 (proposal_create) work:
- proposal_addr        — proposal validator address (bech32)
- stake_st_policy      — StakeST minting policy id (56 hex)
- proposal_st_policy   — ProposalST minting policy id (56 hex)
- script_refs          — cached reference UTxO refs for each Agora script
                         (governor / stake / proposal / treasury validators
                         + stake_st / proposal_st minting policies)

All fields optional with serde defaults so existing configs keep loading.
Will be populated by upcoming `dao_discover_scripts` MCP tool that audits
on-chain state under a known governor_addr.

Test fixture also corrected: stakes_addr now uses Sulkta's real per-DAO
parameterized stake-validator address (`addr1w8msu7p...`) instead of the
shared MLabs deployer (`addr1w9gexmeunzsy...`) — matches audit findings.

aldabra-mcp dao_register tool initializes new optionals to None so
DaoConfig construction stays explicit.
2026-05-05 15:42:59 -07:00
4501700328 fix(dao): correct Proposal #0 starting_time assertion (2026-04-21 not 2026-05-03) 2026-05-05 15:41:32 -07:00
a8ecdfa45d fix(dao): EnumIsData → plain Integer, not Constr i []
Verified against Sulkta's live Proposal #0 datum 2026-05-05:
status field is bare BigInt(3), not Constr 3 []. Plutarch's
EnumIsData derive emits Integer-as-index in this Agora version.

Affected:
- ProposalStatus.{to,from}_plutus_data
- GovernorRedeemer.to_plutus_data (consistency; no on-chain
  governor-redeemer evidence yet, but same EnumIsData derive)

ProposalDatum.to_plutus_data signature updated for the new fallible
ProposalStatus encoding (now returns DaoResult).

Added regression test `decodes_sulkta_live_proposal_zero` that decodes
Proposal #0's actual on-chain datum hex and asserts:
  proposal_id=0, status=Finished, cosigners=[Cobb's pkh],
  thresholds=20/100/100/1/1, votes={0:0, 1:0} (zero votes ever cast),
  starting_time=1772666551575ms.

Closes audit findings 1 + 2 from memory/audit-sulkta-agora-2026-05-05.md.
2026-05-05 15:41:03 -07:00
5fb616c6c5 feat(dao): wire 8 dao_* MCP tools (Phase 1)
Tools added to WalletService:

DAO management (filesystem-only, no chain calls):
- dao_register      — save a DaoConfig under \$ALDABRA_DATA/daos/<name>.json
- dao_list          — show all registered DAO names + active marker
- dao_use           — set active DAO; subsequent dao_* calls without
                      explicit `dao` arg target this one
- dao_remove        — delete config; clears active if it was the active one
- dao_show          — render full DaoConfig JSON for audit

DAO live-state reads (Koios-backed, decoded into typed Rust):
- dao_governor_state  — singleton governor UTxO + thresholds + timing
                        + nextProposalId + per-stake proposal cap
- dao_stake_list      — all stakes for the DAO (filtered to gov-token
                        policy so the shared MLabs stakes addr doesn't
                        leak other DAOs into output). Renders pkh,
                        amount, locks, delegation per stake.
- dao_my_stake        — filters dao_stake_list to just THIS wallet's
                        stake (matches wallet pkh against StakeDatum.owner).
                        Empty array if not staked yet.

Plumbing:
- WalletService::new gains data_dir param (for DaoStore root)
- WalletInner gains dao_store + dao_reader fields
- wallet_pkh() helper extracts the wallet's payment-credential hash from
  bech32 for owner-match in dao_my_stake
- get_info() instructions advertise the new dao_* surface
- aldabra-mcp/Cargo.toml: aldabra-dao path dep + hex + pallas-addresses
2026-05-05 13:51:04 -07:00
14902f4e01 chore(dao): drop unused as_constr import from governor.rs top-level 2026-05-05 13:46:50 -07:00
d1167b5a15 fix(dao): ProductIsData encodes as CBOR Array, not Constr 0
The Plutarch `ProductIsData` derive (used by every record datum in
Agora) emits a CBOR list of fields, NOT the generic Constr 0
encoding I assumed during Phase 0. Verified by decoding Sulkta's
live governor UTxO datum: outer bytes start `9f 9f` (indef array of
indef arrays), not `d8 79` (Constr tag 121).

Affected types:
- StakeDatum, ProposalLock        (was Constr 0, now Array)
- ProposalDatum, ProposalThresholds, ProposalTimingConfig
- GovernorDatum

Sum types untouched — they keep Constr-encoding (makeIsDataIndexed
or EnumIsData both produce Constr i [...]):
- Credential, ProposalAction, StakeRedeemer, ProposalRedeemer,
  GovernorRedeemer, ProposalStatus

New helpers in plutus_data.rs:
- `product(fields)` — emit indefinite-length CBOR Array
- `as_product(pd)` — decode (alias for as_array, named for intent)

Added end-to-end validation test `decodes_sulkta_live_governor_datum`
that decodes the real on-chain datum hex from Sulkta's governor UTxO
(7c8db14...221c47#1) and asserts the parsed struct matches README
parameters: thresholds [20/100/100/1/1], 7d draft, 7d vote, 48h lock,
24h exec, 30min ranges, max 20 proposals per stake.
2026-05-05 13:46:21 -07:00
c059c1ff1c fix(dao): build errors in plutus_data — or-pattern parens, Int→i128, BoundedBytes AsRef ambiguity 2026-05-05 13:42:33 -07:00
41195ece4f feat(dao): scaffold aldabra-dao crate (Phase 1 reads)
Adds a 4th workspace crate `aldabra-dao` for native Agora-on-Cardano
DAO interaction. Multi-DAO from day one — DaoConfig per DAO at
\$ALDABRA_DATA/daos/<name>.json + .active selector. Sulkta DAO and any
community Agora deployment (Bob's DAO, Alice's DAO) are first-class.

Phase 0 type port complete:
- StakeDatum, StakeRedeemer, ProposalAction, ProposalLock, Credential
- ProposalDatum, ProposalRedeemer, ProposalStatus, ProposalThresholds,
  ProposalTimingConfig, ProposalVotes
- GovernorDatum, GovernorRedeemer
- All Constr indices verified against Agora source makeIsDataIndexed
  + EnumIsData declarations (Stake/Proposal/Governor/Action/Status all
  cross-referenced)
- Round-trip tests for every type

Phase 1 read surface (this commit):
- DaoStore: DaoConfig load/save/list/remove + active-DAO selector with
  first-register-becomes-active UX. 8 unit tests.
- DaoReader trait + KoiosDaoReader impl for get_governor + list_stakes.
  list_proposals stubbed pending Phase 4 proposal-script-address discovery.
- Stake address sharing handled: list_stakes filters on gov_token_policy
  (the shared MLabs stakes addr serves many DAOs).

Stubs for upcoming phases:
- agora/treasury.rs (Phase 4 — treasury spend helpers)
- agora/authority_token.rs (Phase 4 — GAT mint/burn)
- agora/reference_scripts.rs (Phase 2/3 — independent script-hash discovery
  per Cobb's choice 2026-05-05; computed locally, never trust MLabs registry)
- builder/mod.rs (per-operation Plutus tx builders, populated phase-by-phase)

Spec doc + decisions: memory/spec-aldabra-dao-agora-port.md in workspace.

Effects map (`ProposalDatum.effects`) kept as raw PlutusData for round-trip
integrity until Phase 4 (proposal create) needs typed access.

ExUnits strategy locked: Koios tx_evaluate from day one (no hardcoded values).
Wired up in Phase 2 alongside reference-script discovery.
2026-05-05 13:40:12 -07:00
66829c9aea mcp: declare tools capability in ServerInfo
rmcp 0.1.5's #[tool(tool_box)] macro doesn't backfill
ServerInfo::capabilities. Without an explicit ToolsCapability,
clients read "capabilities":{} from initialize and skip tools/list
entirely — the server looks connected (instructions field lands)
but the tool surface is empty. Claude Code's MCP log:
  "hasTools":false,"hasPrompts":false,"hasResources":false

Fix: capabilities = ServerCapabilities::builder().enable_tools().build()
in get_info(). Adds a regression test on the wire shape.
2026-05-05 09:41:19 -07:00
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
1ee124b545 AUDIT4-3 fix: optional inline datum on wallet_send
wallet_send + wallet_send_unsigned now accept an optional
datum_inline_cbor_hex field. When set, the recipient output
carries the bytes as an inline datum — the right shape for
locking funds at a script address with a datum the validator
can read.

Without this, sends to script addresses created un-spendable
utxos (Babbage/Conway rejects spending script utxos that
don't carry a datum). Surfaced 2026-05-04 audit-4 phase F2
when the always-succeeds Aiken validator's locked utxo
couldn't be spent back due to NotAllowedSupplementalDatums +
PPViewHashesDontMatch chain errors.

Plumbed through:
  build_signed_payment_with_assets (added arg)
  build_unsigned_payment_with_assets (added arg)
  prepare_payment (added arg)
  build_staging_with_fee (added arg)
  output_with_assets (added arg)
  SendArgs / UnsignedSendArgs (new optional MCP field)

Change outputs never get a datum — they go back to the wallet
which has no validator to satisfy, so the field is wired only
to the recipient output.

Test lock_with_inline_datum_attaches_datum_to_output decodes
the resulting tx CBOR and confirms the recipient output's
datum_option is populated.

Unblocks mainnet Plutus testing — the spend round trip can
now build a lock that the spend side can satisfy.
2026-05-05 06:58:15 -07:00
e712f370f0 aldabra: --bootstrap-from-xprv power-user import path
Adds RootKey::from_root_xsk_bech32() / from_xprv_bytes() /
to_root_xsk_bech32() so RootKey can ingest + emit the same
bech32 root extended secret key shape that cardano-cli +
cardano-address + the IOG node priv/wallet/<name>/root.prv
file already use. HRP is strictly root_xsk — refuses
acct_xsk/addr_xsk to keep the import scoped to the actual
HD root.

New CLI flag --bootstrap-from-xprv runs an interactive
import: paste root_xsk1... bech32, prompt passphrase,
encrypt, persist as root-xprv.age (parallel to mnemonic.age).
Refuses to overwrite either existing key file (per Cobb's
no-delete-crypto-keys rule — caller has to move aside, not
delete).

Startup path now checks for either mnemonic.age OR
root-xprv.age; refuses if both exist (ambiguous). Same
RootKey downstream — derivation tree, signing, all of it
works identically whether the key came in via mnemonic
or xprv import.

Test root_xprv_round_trip proves the imported xprv derives
to the same address as the mnemonic-imported equivalent.
2026-05-05 06:38:01 -07:00
057f623312 AUDIT5-1: relax coin selector for ada-only drain-to-fee
Two coupled fixes for the same root cause: the coin selector was too
conservative for "send most of what I have" cases.

1. min_change_required now drops to 0 for ada-only sends (kept at
   min_utxo_lovelace for asset-bearing sends where change has to
   carry leftover policy IDs). Downstream pass2 already folds
   sub-min change into fee on the ada-only happy path; the selector
   was reserving slack the chain doesn't actually need.

2. fee_pass1 dropped from 500_000 to 200_000. Real fees:
     1-in 1-out ada-only send : ~166 k
     1-in 2-out (with change) : ~178 k
     CIP-25 mint w/ metadata  : ~210 k
   500_000 was overgenerous safety budget. 200_000 is enough headroom
   for the basic-send case (which is the one that needed to drain to
   fee) without crowding mint paths (which typically have plenty of
   lovelace headroom anyway).

Surfaced 2026-05-05 zeroing out the mainnet test wallet:
2 ADA balance, 1.8 ADA send refused upstream as
"need 3300000 (target+fee+min_change), have 2000000"
even though the chain math was fine. New regression
ada_only_send_can_drain_to_fee covers the case.
2026-05-05 06:06:24 -07:00
30761039ea AUDIT4-G2 fix: client-side min_utxo guard on ada-only wallet_send
wallet_send now rejects sub-min-utxo (1 ADA) ada-only sends with a
clear local error before any koios round-trip. Asset-bearing sends
still go through to chain so the dynamic per-asset min computation
is what surfaces in the error — no static guard would be right
there.

Saves the chain round-trip + the bewildering "tx submitted... wait
30 seconds... actually it failed" UX. Surfaced 2026-05-04 audit-4
phase G2 against the deployed container.
2026-05-04 21:22:28 -07:00
d5fb00c9f5 mainnet prep: tighten max_send_lovelace default + cost model docs
max_send_lovelace default is now network-aware: mainnet 10 ADA,
preprod/preview 100 (t)ADA. Mainnet handles real value, so the cap
should bite earlier — anything > 10 ADA needs explicit force=true.
Test ada on preprod/preview is faucet-replaceable, no need to
sand off the test surface. New regression
mainnet_default_max_send_is_tighter locks the rule in.

PLUTUS_V3_COST_MODEL_PREPROD docstring updated: confirmed identical
to mainnet PV3 cost model (preprod epoch 286 = mainnet epoch 629,
both 297 params, byte-identical). Cost models are protocol-version
parameters, not network parameters; using the same constant on
both is correct. Re-snapshot from mainnet Koios after any major
hard fork. Naming kept as _PREPROD for git churn reasons.
2026-05-04 21:09:31 -07:00
e4914a14ba AUDIT4-2 fix: invert plutus collateral/funding utxo picker
build_signed_plutus_spend was picking the LARGEST ada-only utxo
for collateral and the next-largest for funding. Wallets with
one big change utxo + a small leftover (the typical shape after
any send) hit this with funding=tiny, collateral=huge —
funding+locked couldn't cover payout + script-execution fee +
change min_utxo even with billions of lovelace sitting unused
in collateral.

Fix: pick the SMALLEST ada-only utxo that still qualifies (≥5 ADA)
for collateral, and the LARGEST for funding. Collateral never
gets consumed on the happy path, so its size beyond the 5-ADA
floor is wasted budget; funding has to cover real spend.

Surfaced 2026-05-04 audit-4 phase F2 on the deployed Lucy
container against the always-succeeds Aiken validator.

New regression test picks_smallest_qualifying_collateral_largest_funding
covers the mixed-size-utxo scenario the prior tests missed
(both old utxos were 50-100M ada, so the inversion didn't show).
2026-05-04 20:59:29 -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
f23ff65dad audit-3 (code cleanup): zero clippy warnings, zero build warnings
prep for deployment. cargo clippy --workspace --all-targets now passes
clean. cargo audit unchanged (same 2 unmaintained-warning macro-support
transitives; no cves).

cleanup applied:
- ProtocolParams construction in tools.rs uses struct-update syntax
  (clippy::field_reassign_with_default).
- main.rs collapsed two else-if branches with identical bodies
  (clippy::if_same_then_else).
- mint/plutus/stake sort_by(|a,b| b.cmp(&a)) → sort_by_key(Reverse(_))
  (clippy::manual_sort_by). 4 sites.
- metadata/mint/tx odd-length hex check uses .is_multiple_of(2)
  (clippy::manual_is_multiple_of). 3 sites.
- stake.rs witness_overhead conditional removed — both branches
  produced TWO_WITNESS_OVERHEAD_BYTES (left over from when
  registration was thought to add a third witness; it doesn't).
  WITNESS_OVERHEAD_BYTES const removed (only the two-witness one
  is used).
- Public spend/mint/stake build_signed_*_with_assets fns get
  #[allow(clippy::too_many_arguments)] — they ARE the API surface.
- ex_units_default_is_generous test gets explicit allow for the
  tautological-on-const assertion (kept the intent comment).

97 unit tests still pass. release build clean.
2026-05-04 18:40:35 -07:00
7d59ceffd2 plutus spend: fix all 4 chain-level bugs surfaced in preprod audit
PLUTUS-1 (HIGH) — value-not-conserved on happy path. collateral isn't
consumed unless script fails, so total_in counted lovelace that wasn't
actually available for outputs. now picks a SEPARATE ada-only funding
utxo as a regular input alongside the locked utxo; collateral stays
collateral. error message tells callers to "split a UTXO first or top
up" if a second ada-only utxo isn't available.

PLUTUS-2 (HIGH) — collateral containing native assets. chain forbids
that; our picker grabbed largest-overall. now filters available_utxos
to assets.is_empty() before picking, errors clearly if no ada-only
utxo ≥ 5 ADA exists.

PLUTUS-3 (HIGH) — fee underestimation. plutus tx fees are
size_fee + exunits_fee. only size_fee was being charged. new
ProtocolParams::ex_units_fee() does ceil(mem * priceMem) +
ceil(steps * priceStep). conway-era prices in defaults
(577/10000 mem, 721/10_000_000 steps). fee jumps from ~0.17 ADA →
~1.7 ADA for the default ExUnits budget — matches what chain demanded.

PLUTUS-4 (LOW, becomes blocking under the others) — script_data_hash
not computed. pallas-txbuilder only computes the body hash field when
language_view is set on staging. plutus v3 path now calls
.language_view(version, cost_model) when the caller-supplied
ProtocolParams::plutus_v3_cost_model is Some. mcp wallet_script_spend
populates with the canonical preprod V3 cost model from
plutus_cost_models::PLUTUS_V3_COST_MODEL_PREPROD (297 i64 params,
fetched from koios epoch_params 2026-05). when ProtocolParams has no
cost model, we skip language_view and the chain rejects with
PPViewHashesDontMatch — explicit-failure mode, no silent shipping
of broken txs.

new tests:
- ex_units_fee_matches_known_values: 14M mem * 0.0577 + 10B steps *
  7.21e-5 ≈ 1.529 ADA ± ceil-rounding. locks the conway price math.
- rejects_when_no_funding_input_separate_from_collateral: catches
  the PLUTUS-1 single-utxo case.
- rejects_when_collateral_candidate_has_assets: PLUTUS-2 ada-only.

verified on preprod against a real script-locked utxo (the placeholder
script we locked 5 tADA at earlier). chain rejection went from 5
distinct errors to 1 (MalformedScriptWitnesses — expected, our
placeholder UPLC isn't valid). structural body shape now passes
every chain-rule check; only the script bytecode itself fails to
compile, which is a test-env limitation (no aiken in our toolchain
yet) not a wallet-code limitation.

97 unit tests pass. ProtocolParams gained 5 new fields + ex_units_fee
helper; went from Copy to Clone (cost_model is a Vec).
2026-05-04 17:27:47 -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
36bbd8033f mcp: rename tool names to underscore-only (Claude Code compat)
Claude Code's MCP client validates tool names against
[a-zA-Z0-9_-]{1,64} and silently drops names containing dots.
aldabra was registering wallet.address etc. with dots; despite the
daemon running fine and rmcp accepting the names, Claude Code's
tools/list cache was empty for aldabra after `/exit + relaunch`.

discovered integration-time 2026-05-04 after first real session
restart with the wallet registered.

renamed:
  wallet.address          → wallet_address
  wallet.network          → wallet_network
  wallet.balance          → wallet_balance
  wallet.utxos            → wallet_utxos
  wallet.send             → wallet_send
  wallet.send.unsigned    → wallet_send_unsigned
  wallet.tx_status        → wallet_tx_status
  wallet.tx_summary       → wallet_tx_summary
  wallet.sign_partial     → wallet_sign_partial   (already underscored)
  wallet.submit_signed_tx → wallet_submit_signed_tx (ditto)
  wallet.policy.create    → wallet_policy_create
  wallet.mint             → wallet_mint           (no change)
  wallet.mint.cip68_nft   → wallet_mint_cip68_nft
  wallet.mint.unsigned    → wallet_mint_unsigned
  wallet.script.spend     → wallet_script_spend
  wallet.stake.address    → wallet_stake_address
  wallet.stake.delegate   → wallet_stake_delegate

instructions blurb + module docstring updated. all 93 unit tests
still pass. fresh tools/list smoke confirmed: 17 tools all
underscore-only.

cobb needs to /exit + relaunch one more time for Claude Code to
re-handshake with the rebuilt binary.
2026-05-04 15:59:46 -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
7ea4c4cd33 phase 4.1-4.3: plutus script spend
new aldabra-core::plutus module:
- PlutusVersion enum (V1, V2, V3) → maps to ScriptKind on the
  pallas-txbuilder side.
- PlutusExUnits (mem, steps) — public mirror of pallas's so callers
  don't drag pallas types in. From<> impl converts internally.
- DEFAULT_EX_UNITS = (14M mem, 10B steps) — generous budget that
  validates trivial validators ("always succeeds", simple equality);
  real validators tune via the ex_units arg.
- MIN_COLLATERAL_LOVELACE = 5_000_000 (Conway protocol floor).
- build_signed_plutus_spend(payment, network, locked, script, redeemer,
  witness_datum?, available_utxos, change_addr, payout_addr,
  payout_lovelace, ex_units, params) → signed cbor.
  - picks the largest wallet UTXO ≥ 5 ADA as collateral, errors out
    if none qualifies.
  - happy path: locked + collateral as inputs, payout + change as
    outputs, script + redeemer + (optional witness) datum as
    witnesses, wallet's payment key signs the body.
  - reference inputs (4.2 expansion) and live ExUnits estimation
    (4.4) are follow-ups.
- looks_like_script_address(bech32) bool sanity helper for callers
  that want to filter by address kind before constructing a spend.

mcp tool wallet.script.spend: full args surface for one-shot
spend. plutus_version is a string ("v1"|"v2"|"v3"). ex_units optional.

84 → 88 unit tests. 15 → 16 mcp tools.

phase 4 status:
- 4.1 ☑ inline datum (already supported via Output::set_inline_datum
  used by cip-68 mint)
- 4.2 ◐ reference input (txbuilder has the API; not yet exposed in
  build_signed_plutus_spend — followup)
- 4.3 ☑ wallet.script.spend
- 4.4 ☐ ExUnits estimation — needs uplc / aiken integration, defer
- 4.5 ☑ stake key derivation
- 4.6 ☑ wallet.stake.delegate
2026-05-04 12:44:06 -07:00
0ba95c1709 phase 4.5, 4.6, 3.6 close-out: stake delegation + multisig mint primitive
stake key + reward address (4.5):
- StakeKey::stake_address(network) — bech32 (`stake1...` mainnet,
  `stake_test1...` testnet) via pallas_addresses::StakeAddress::new
  (added to the fork in the same commit since the upstream tuple
  struct had no public constructor).
- StakeKey::xprv() — crate-internal accessor for signing.
- WalletInner now holds the stake_key alongside the payment_key.
- mcp tool wallet.stake.address surfaces the bech32.

stake delegation (4.6):
- new aldabra-core::stake module:
  - parse_pool_id(bech32) → Hash<28>
  - build_signed_stake_delegation(payment, stake, network, utxos,
    change_addr, pool_bech32, register_first, params) → signed cbor.
  - if register_first: prepends a StakeRegistration cert (consumes
    a 2 ADA deposit from inputs). otherwise just delegates.
  - signs with both payment_key (body witness) and stake_key (cert
    witness). reuses sign::add_witness for both — same body-hash
    ed25519 signing path regardless of CIP-1852 chain index.
- mcp tool wallet.stake.delegate: pool_id, register_first (defaults
  true). signs + submits.

3.6 close-out — wallet.mint.unsigned mcp tool:
- exposes the existing build_unsigned_mint with caller-supplied
  PolicySpec (json), so multi-sig / treasury flows can build through
  this wallet without it auto-signing. round-trip with
  wallet.sign_partial chain → wallet.submit_signed_tx.

depends on Sulkta-Coop/pallas@feat-aux-data which gained two more
patches in the same branch:
- StakeAddress::new public constructor.
- StagingTransaction::add_certificate / clear_certificates +
  Conway::build_conway_raw decode-and-plumb for certs (filling in the
  `certificates: None, // TODO` upstream).

mcp tools: 12 → 15 (wallet.stake.address, wallet.stake.delegate,
wallet.mint.unsigned).

79 → 84 unit tests. new coverage: stake address bech32 round-trip,
pool_id bech32 parse + reject-wrong-hrp, delegation tx with + without
registration (asserts cert count, witness count, cert variants).
fork tests grew: certificates_plumb_through_to_tx_body and
no_certificates_means_none.
2026-05-04 12:41:10 -07:00
f376481a8f phase 3.3, 3.6: cip-68 ref-nft pair + sign_partial primitive
new aldabra-core::cip68 module:
- asset name prefixes 100 (0x000643b0 ref) / 222 (0x000de140 user) /
  333 (0x0014df10 ft). prefixed() guards 32-byte total cap so caller
  can't blow past the cardano protocol limit by accident.
- json_to_plutus_data: serde_json::Value → PlutusData (recursive).
  numbers must fit i64. strings → BoundedBytes (cip-68 convention is
  bytes-keyed datum maps, not text). null is rejected, floats rejected.
- build_cip68_datum_cbor wraps the metadata in the canonical
  Constr 0 [meta_map, version_int=2, Constr 0 []] shape.

new aldabra-core::mint::build_signed_cip68_nft_mint:
- mints two assets simultaneously under one policy (ref + user, qty 1
  each), three outputs (ref @ ref_addr w/ inline datum, user @ user_addr,
  change). same two-pass fee refinement as the rest of the path.
- mutable nfts: pass ref_addr == change_addr. wallet's payment key can
  later spend the ref UTXO and re-create with new datum.
- immutable: caller passes an always-fails script address (phase 4
  concern; today this fn trusts whatever's passed).

new aldabra-core::sign module + add_witness:
- decodes a conway tx (any state — unsigned or partially signed),
  signs the body hash with the wallet's payment key, appends a
  VKeyWitness to the witness_set, re-encodes. body is invariant
  (regression test asserts the body hash before and after the witness
  append are identical).
- this is the missing primitive for n-of-k multisig flows: each party
  calls add_witness on the previous party's output cbor; any party
  submits via wallet.submit_signed_tx.

mcp tools: 10 → 12.
- wallet.mint.cip68_nft — args: user_address, name_body_hex (≤28b),
  metadata (json object), user_lovelace? ref_address? ref_lovelace?
  invalid_after_slot? — defaults provided for the ergonomic case
  (ref_addr=wallet, lovelace=1.5 ADA each).
- wallet.sign_partial — args: cbor_hex — appends our witness, returns
  updated hex. usable for MAP treasury 2-of-2 once a
  wallet.mint.unsigned-with-policy-arg lands (TODO, deferred).

65 → 79 unit tests. cip68 module: 9 tests covering prefix+datum
shape. sign module: 4 tests covering one-witness, two-witness,
body-hash invariant, garbage rejection. integration test in mint
verifies cip68 build produces 3 outputs with inline datum on the
ref output.
2026-05-04 12:27:43 -07:00
a93a2b7cfa phase 3.2: cip-25 metadata via the pallas fork
unblocks named mints. wallet.mint now accepts an optional `metadata`
arg (json object); explorers + wallets render the asset with name/image
instead of <asset1xyz...>.

new aldabra-core::metadata module:
- json_to_metadatum: serde_json::Value → Metadatum (recursive). numbers
  must fit i64 (cardano metadata Int width). strings >64 bytes split
  into Array<Text> chunks at utf-8 char boundaries (CIP-25 v2
  long-string convention). null is rejected.
- build_cip25_aux_data(policy_id_hex, asset_name_hex, json_value):
  builds the label-721 wrapper (Map { 721: Map { policy_bytes:
  Map { name_bytes: attrs }, "version": "2.0" } }), wraps in
  AuxiliaryData::PostAlonzo, returns cbor bytes.

mint module:
- new build_signed_mint_with_metadata + build_unsigned_mint now take
  optional cip25_metadata. backward-compat: build_signed_mint is a
  thin no-metadata wrapper.
- prepare_mint + build_mint_staging plumb aux_data_cbor through.
  staging.auxiliary_data(bytes) is the new fork API surface — when
  set, conway::build_conway_raw decodes + computes
  auxiliary_data_hash automatically.
- regression test build_signed_mint_with_metadata_produces_aux_hash:
  decodes the resulting signed cbor, asserts both
  body.auxiliary_data_hash is Some and tx.auxiliary_data is present.
  catches the failure mode where metadata is silently dropped.

mcp wallet.mint gains a `metadata` arg field surfaced via schemars
JsonSchema. tools/list shape correctly carries the optional json
object.

depends on Sulkta-Coop/pallas@feat-aux-data — vendored via
[patch.crates-io] in the workspace Cargo.toml. PR upstream pending.

56 → 65 unit tests. 8 → 8 mcp tools (count unchanged, wallet.mint
gained an arg).
2026-05-04 12:11:11 -07:00
2f3d975c0f phase 3.1, 3.4, 3.5: native policy + mint path (no metadata yet)
new aldabra-core::mint module:
- PolicySpec enum: SingleSig, SingleSigTimelock, NofK
  - SingleSig{pkh}: ScriptPubkey native script
  - SingleSigTimelock{pkh, slot}: ScriptAll[ScriptPubkey, InvalidHereafter(slot)]
  - NofK{n, [pkhs]}: ScriptNOfK
- PolicySpec::single_sig(payment) + single_sig_timelock(payment, slot)
  convenience constructors that derive the pkh from a PaymentKey.
- policy_id() = pallas_traverse::ComputeHash<28>::compute_hash, which
  is blake2b-224 of (0x00 || cbor) — the canonical native-script hash.
- to_cbor() for callers that want the script bytes raw.

build_signed_mint / build_unsigned_mint:
- two-pass fee like the send path, plus a few extras specific to mint:
  staging.mint_asset(policy, name, qty), .script(Native, cbor),
  .disclosed_signer(payment_pkh) — the disclosed_signer surfaces the
  required signature in the tx body so the chain knows which witness
  to verify against the script.
- positive qty mints (asset goes into dest output), negative qty burns
  (asset comes out of input holdings, change preserves leftover).
- token-bearing change must hold ≥ min_utxo lovelace — same guard as
  the send path.

mcp tools:
- wallet.policy.create — args: invalid_after_slot? — returns
  {policy_id_hex, script_cbor_hex, type}.
- wallet.mint — args: dest_address, dest_lovelace (≥ 1 ADA),
  asset_name_hex, quantity (i64), invalid_after_slot? — auto-generates
  a single-sig policy bound to the wallet's payment key, builds, signs,
  submits.

8 → 10 mcp tools. 48 → 56 unit tests.

3.2 (CIP-25 metadata) is BLOCKED on pallas-txbuilder 0.32/0.35 — both
hardcode `auxiliary_data: None` in the conway builder. options for next
session: (a) post-build CBOR injection, (b) assemble tx via
pallas-primitives directly, (c) wait for upstream. flagged in the
spec doc.

3.3 (CIP-68) depends on 3.2. 3.6 (MAP 2-of-2) needs the multi-key
signing flow on the build side; PolicySpec::NofK variant is ready but
build_signed_mint only sign with one key today.
2026-05-04 11:44:16 -07:00
46b6f6efa3 phase 2.5-2.6: native asset send + cold-sign flow
InputUtxo gains an `assets: BTreeMap<String, u64>` field matching
aldabra-chain::Utxo's shape (`policy_id_hex(56) || asset_name_hex`
key). new AssetSpec type for the recipient asset list.

asset-aware select_utxos:
- phase 1: per-asset greedy by holding size, pulls UTXOs containing
  each requested asset until coverage ≥ target
- phase 2: ada-only greedy to top up lovelace need
this preserves the prior ada-only behavior when assets list is empty.

build_signed_payment_with_assets / build_unsigned_payment_with_assets
build outputs with .add_asset() for each requested + each leftover
(change-side). guards: token-bearing change must hold ≥ min_utxo
ADA — surfaced as a clearer error than letting the chain reject a
sub-min output.

cold-sign flow (phase 2.6):
- new tools wallet.send.unsigned (returns {cbor_hex, summary} json
  for human review + cold-signer consumption) and
  wallet.submit_signed_tx (takes hex-encoded signed cbor → submit).
- PaymentSummary now carries send_assets + change_assets vecs so the
  human reviewer can spot accidental token transfers.
- summary.tx_hash is the predicted body hash; signed CBOR will hash
  to the same value (signature is over the body, not the cbor wrapper).

helpers: hex_encode/decode, parse_policy_id, parse_asset_name,
split_asset_key. mcp side defines its own McpAssetSpec with
schemars::JsonSchema derive so the schemars dep doesn't bleed into
the security-boundary core crate.

48 unit tests (was 41). new coverage: asset-aware selection (greedy +
missing-asset error), policy/asset-name parsers, multi-asset cbor
build, change-asset summary correctness.

phase 2.7 (live preprod smoke against funded wallet) procedure
documented in memory/spec-aldabra-buildout.md; needs cobb's faucet ada.
2026-05-04 11:35:06 -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
489b58cc1e phase 1 scaffold: cargo workspace + 3 crates + roadmap + architecture
Repo skeleton for sulkta-wallet, 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 Cobb's first Rust project + a real-world workout for
crafting-table's rust toolchain.
2026-05-04 10:02:32 -07:00