Commit graph

49 commits

Author SHA1 Message Date
4d3ef03978 feat(governance): Phase 5 — vote_delegate + drep_register/deregister
Conway-era governance MCP tools, key-credentialed (script credentials
deferred to Phase 6).

aldabra-core/src/governance.rs (new ~500 LOC):
- DRepTarget enum + parse_drep_target (handles bech32 drep1.../
  drep_script1... + named 'abstain' / 'no_confidence')
- build_signed_vote_delegation — Certificate::VoteDeleg(stake_cred,
  drep), reuses the dual-witness 2-pass-fee pattern from stake.rs.
  Optional register_first prepends StakeRegistration.
- build_signed_drep_registration — Certificate::RegDRepCert with
  optional CIP-100/119 anchor + 500 ADA deposit
- build_signed_drep_deregistration — Certificate::UnRegDRepCert with
  refund-aware change calc (deposit returns to wallet)
- DREP_REGISTRATION_DEPOSIT_LOVELACE constant (500 ADA, mainnet)

Made stake_key_as_payment_proxy pub(crate) so governance.rs can reuse
the stake-key-as-witness trick.

aldabra-mcp/src/tools.rs:
- wallet_vote_delegate (drep + register_first)
- wallet_drep_register (optional anchor_url + anchor_data_hash_hex)
- wallet_drep_deregister (no args)

3 unit tests on parse_drep_target + DRepTarget→DRep round-trip.

Phase 6 (vote_cast for DReps voting on Conway gov actions) blocked
on extending Sulkta-Coop/pallas-txbuilder to thread voting_procedures
through StagingTransaction (currently TODO at conway.rs:254). Same
pattern as the aux_data + certificates patches already in the fork.
Estimated ~300-500 LOC fork patch + ~400 LOC vote-cast builder. Surface
to Cobb before starting.
2026-05-06 07:08:08 -07:00
d007817796 feat(dao): proposal_advance state machine + stake_destroy + MCP tools
Phase 4c + 4d. Closes the DAO write-path arc (excluding GAT minting,
which is Phase 4c-bis since Sulkta has never executed a proposal).

## proposal_advance (Phase 4c)

State-machine builder with 5 transitions:
- Draft → VotingReady (cosigner threshold met, all cosigner stakes
  ref'd as txInfo.referenceInputs, sum staked_amount ≥ to_voting)
- Draft → Finished (drafting period elapsed without enough cosigners)
- VotingReady → Locked (winner outcome exists with votes ≥ execute,
  no tie)
- VotingReady → Finished (locking period elapsed without winner)
- Locked → Finished (executing period elapsed; for InfoOnly proposals)

Validator (PAdvanceProposal in Proposal/Scripts.hs:657) requires
output proposal datum equals input with ONLY status mutated. Builder
mirrors exactly. Per-transition preflights match validator gates.

Cosigner stake refs go in as txInfo.referenceInputs (not regular
inputs) per witnessStakes pattern (Proposal/Scripts.hs:366) — sum
of staked_amount is computed from the ref-input set.

GAT-minting Locked→Finished path (effected proposals) deferred to
4c-bis. The pmintGATs governor redeemer is a separate tx that fires
ONLY when the executing period is in window AND the winner outcome
has effects to mint GATs for. Sulkta's first proposal was InfoOnly
so this path never exercised on chain yet.

11 unit tests covering every transition + every preflight reject.

## stake_destroy (Phase 4d)

Burns StakeST token + returns gov-tokens to owner. From
Stake/Redeemers.hs pdestroy (~L432): owner signs (no delegatees),
all locks empty, no stake output at stakes_addr. From stakePolicy
burn branch (~L161): burntST quantity = -spentST.

Tx shape: spend stake (Destroy redeemer) + maybe a funding utxo +
collateral; mint -1 StakeST; one wallet output carrying gov-tokens
+ (stake.lovelace + funding - fee). Funding optional — stake's own
lovelace usually covers fees.

4 unit tests including the funding-optional path.

## MCP tools

dao_proposal_advance_unsigned auto-picks the right transition from
proposal status + chain tip vs window boundaries. Mainnet-only gate.
Fetches cosigner stake refs by matching owner pkh against
proposal.cosigners.

dao_stake_destroy_unsigned fetches the wallet's stake (via owner
pkh match), pulls StakeST asset name from chain, burns it.
2026-05-06 07:00:48 -07:00
39b56223f9 feat(dao): proposal_cosign builder + dao_proposal_cosign_unsigned tool
Phase 4b. Cosign extends a Draft proposal's cosigners list — the
multi-stake bridge for clearing to_voting threshold when a single
stake doesn't have enough TRP. Validator (PCosign branch in
Proposal/Scripts.hs:433) requires:

- Status == Draft
- Exactly one stake input (ptryFromSingleton)
- New cosigner = stake.owner (delegatees rejected)
- Cosigner inserted into list via pinsertUniqueBy (sorted, no dupes)
- len(cosigners) ≤ max_cosigners (DaoConfig.max_cosigners)
- stake.staked_amount ≥ thresholds.cosign

Stake-side (ppermitVote PCosign branch): owner signs (not delegatee),
single stake input, new lock = ProposalLock { proposal_id, Cosigned }
prepended via paddNewLock = pcons.

Insertion order mirrors Plutarch's pfromOrdBy-derived Credential Ord:
variant index first (PubKey=0 < Script=1), then 28-byte hash lex.
`insert_unique_sorted` test-covered for low/mid/high positions + the
PubKey-before-Script invariant.

Also extract pull_wallet_utxos free function in tools.rs — shared
between the (future) refactor of create/vote and immediately by
cosign. Inline duplication in create/vote left as a future cleanup.

11 unit tests on the builder. Tool args: dao? + proposal_id +
fee_lovelace.
2026-05-06 06:51:53 -07:00
68e493dd2f refactor(dao): wire KoiosDaoReader::list_proposals + use it from vote tool
The first attempt's vote MCP tool inlined a Koios address_info pull
helper in tools.rs that needed reqwest + pallas_codec + pallas_primitives
as direct deps on aldabra-mcp — which it doesn't have. Compile failed.

Cleaner: move the work into the dao crate where those deps already live.

- ProposalUtxo gains `lovelace` + `proposal_st_asset_name_hex`. The
  vote builder needs both to construct the new proposal output.
- KoiosDaoReader::list_proposals (was stubbed) now reads cfg.proposal_addr,
  decodes every UTxO's inline datum to ProposalDatum, and matches the
  ProposalST asset name against cfg.proposal_st_policy when set, falling
  back to the first asset on the utxo when not (Sulkta convention is one
  ProposalST + nothing else).
- KoiosAsset.asset_name no longer #[allow(dead_code)] — it's read now.
- tools.rs::dao_proposal_vote_unsigned switches to dao_reader.list_proposals
  + drops the inline pull helper. ~150 LOC simpler.
2026-05-06 06:41:52 -07:00
3b0e0dd9bf feat(dao-mcp): wire dao_proposal_vote_unsigned + slot↔ms helper
- DaoProposalVoteArgs (dao? + proposal_id + result_tag + fee_lovelace)
- mainnet_slot_to_posix_ms: Shelley genesis constants (slot 4_492_800,
  posix 1_596_059_091_000) for converting tip+VALIDITY_RANGE_SLOTS
  into the Voted lock's posix_time field
- pull_proposal_utxos helper: address_info → decode every UTxO's
  inline datum into ProposalDatum, return matching proposal_id by id
  match (`KoiosDaoReader::list_proposals` is still stubbed; this is
  a focused write-path read)
- mainnet-only network gate (preprod/preview slot↔ms is Phase 5)
- get_info instructions text mentions write tools
- drop unused pallas_addresses::Address + pallas_txbuilder::ExUnits
  imports surfaced by clippy
2026-05-06 06:37:31 -07:00
a19439f640 feat(dao): proposal_vote.rs builder — Phase 3 unsigned tx
Mirrors proposal_create's shape: 3 inputs (stake script + proposal
script + funding wallet), 2 reference inputs (stake validator +
proposal validator), 2 outputs (mutated stake + mutated proposal +
maybe change), 2 plutus spends (PermitVote + Vote tag), no mints.

Pre-flight matches every Plutarch validator check from
Agora/Proposal/Scripts.hs PVote (~L484) + Stake/Redeemers.hs
ppermitVote (~L196):

- voter pkh is owner OR delegatee
- proposal status == VotingReady
- stake doesn't already have Voted lock for this proposal_id
- stake.staked_amount >= proposal.thresholds.vote (single-stake v1)
- result_tag is in proposal.votes keys (== effects keys)
- validity upper bound inside [starting_time + draft_time,
  starting_time + draft_time + voting_time]

New stake datum prepends the Voted lock per paddNewLock = pcons.
New proposal datum increments votes[result_tag] by stake amount;
all other fields preserved bit-exact since validator does record `==`.

Voted.posix_time = caller-supplied validity_upper_ms — matches
PFullyBoundedTimeRange _ upperBound the validator extracts. Caller
(MCP tool) computes ms-from-slot via mainnet Shelley genesis.

9 unit tests covering happy path + every preflight reject + delegated
voter accepted.
2026-05-06 06:31:22 -07:00
5102c77972 chore(dao): drop unused imports — ScriptRefs to test scope, StakeDatum gone from tools.rs 2026-05-05 20:58:03 -07:00
893e3f23da feat(dao-mcp): wire dao_proposal_create_unsigned to fetch C-2 inputs from chain 2026-05-05 20:57:36 -07:00
afd0cfb298 test(dao): update proposal_create test fixture for new args (stake_in + tip_slot + GST) 2026-05-05 20:56:09 -07:00
ea2ee01503 fix(dao): audit C-1 + C-2 + C-3 — informed by reference tx 7c8db1432a07 2026-05-05 20:55:20 -07:00
9556b7812d fix(dao): audit H-2 + H-5 + H-6 (per memory/audit-aldabra-dao-2026-05-05)
H-2: drop ExUnits to 5M/2G for spend, 2M/1G for mint
  Was 14M/10G each = per-tx Conway cap. With 3 plutus contracts running
  (governor spend + stake spend + ProposalST mint), total claim 42M/30G
  exceeds per-tx limit and node rejects pre-phase-2.

H-5: propagate malformed wallet asset keys instead of silently dropping
  Previous filter_map silently dropped any key < 56 chars. Could let a
  corrupt Koios response burn assets on submit. Now returns explicit
  Err with the offending UTxO + key.

H-6: tighten StakeST detection to asset_name == stake_validator_hash
  Per Stake/Scripts.hs:188-190 (pscriptHashToTokenName), StakeST
  asset_name is the stake validator's script hash. Previous code took
  "first non-gov-token asset" which would silently pick a wrong policy
  if a stake UTxO accidentally carried a junk NFT. Regression test
  h6_junk_token_does_not_pollute_stake_st_detection added.

3 of 7 audit punch-list items closed. C-1 + C-2 + C-3 next.
2026-05-05 20:52:22 -07:00
101c85c0a0 fix(dao): scope as_constr import to test that uses it 2026-05-05 20:14:33 -07:00
edd1948dec feat(dao): dao_discover_scripts MCP tool + Koios discovery client
New `aldabra-dao::discovery` module:
- `DiscoveryClient` trait + `KoiosDiscoveryClient` impl
- `discover_scripts(cfg, client, deployers)` — auto-finds:
  - governor_validator_ref + stake_validator_ref via deployer ref-script search
  - stake_st_policy from any existing stake UTxO (gov-token + non-gov-token asset)
  - stake_st_policy_ref via deployer search
- `apply_discovery(cfg, report)` — merges into DaoConfig (never overwrites)
- `script_hash_from_addr(bech32)` — extract 28-byte script hash from a script address

New MCP tool:
- `dao_discover_scripts { dao?, extra_deployers? }` — runs the audit logic
  against any registered DAO + persists the discovered fields back to the
  DaoConfig. Returns JSON with what was found + a gaps list for things
  v1 can't auto-discover (proposal_addr, proposal_st_policy).

Plus 4 unit tests with stub Koios responses validating the full pipeline:
script-hash extraction, StakeST discovery from stake UTxO assets,
validator ref-utxo matching at deployer, apply_discovery merge semantics.

WalletInner now caches `koios_base` so the discovery client can be
constructed on demand without re-passing the URL through args.
2026-05-05 20:14:13 -07:00
5913b9266a feat(dao-mcp): dao_register accepts Phase-4 fields in one call
Closes the gap between DaoConfig schema (already has fields) and the
dao_register tool (was rejecting/ignoring them). Now Sulkta DAO can be
registered with all audit-discovered values in one call:
  proposal_addr / stake_st_policy / proposal_st_policy +
  5 reference UTxO refs (governor / stake / proposal validators +
  StakeST / ProposalST minting policies).

All fields remain optional. dao_proposal_create_unsigned errors clearly
when one's missing. Future dao_discover_scripts tool will auto-populate
from chain queries.
2026-05-05 20:10:22 -07:00
3d95369536 chore(dao): drop unused as_constr import after EnumIsData fix 2026-05-05 19:50:34 -07:00
93edf0c9c3 feat(dao-mcp): wire dao_proposal_create_unsigned + drop unused imports 2026-05-05 19:50:17 -07:00
3ac10f7f4b feat(dao): proposal_create.rs skeleton + InfoOnly builder + tests 2026-05-05 19:48:21 -07:00
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