Commit graph

109 commits

Author SHA1 Message Date
Sulkta
564ba3ccb5 chore: scrub internal session-log narrative from code comments
Wide sweep across the codebase to remove leftover artifacts of internal
development sessions, internal entity naming, and audit-code references
that point at non-public docs. The technical reasoning for each piece
of code stays; the "Caught 2026-05-XX while debugging XYZ at preprod"
narrative goes.

Categories scrubbed:
- Dated session-log comments ("Caught/Surfaced/Discovered 2026-05-XX")
  → rewritten as neutral technical reasoning.
- Internal audit codes (AUDIT-H2, AUDIT-C2, AUDIT-M2, AUDIT-H5, etc.)
  referencing a non-public audit doc → labels stripped, fix reasoning
  kept.
- Internal-entity names in code comments (Sulkta-specific, Sulkta runs
  X, Terrapin/TRP as gov-token names) → generic phrasing.
- Test fixture helper `sulkta_cfg` → `test_dao_cfg`; test DAO name
  string `"sulkta"` → `"test-dao"`. On-chain addresses in test fixtures
  kept (they're real-world wire-byte test data on public chain).
- Cross-references to memory files / non-public audit docs
  (`internal notes`, `aiken-escrow/README.md`)
  → reasoning inlined or removed.
- Test names renamed: `decodes_sulkta_live_governor_datum` →
  `decodes_live_governor_datum`, `decodes_sulkta_live_proposal_zero` →
  `decodes_live_finished_proposal`, etc.

Kept (legitimate):
- Cross-references to in-repo audit docs (aiken-escrow/README.md, aiken-escrow/README.md) — they ARE the
  public artifacts being referenced.
- HIGH-1/HIGH-2/MED-2/LOW labels on escrow fixes — these correspond to
  findings in the in-repo audit doc.
- TODO markers — legitimate work-still-to-do.
2026-05-10 21:29:40 -07:00
Sulkta
93f0d2ebde docs: rewrite for users — drop internal infra context
README + supporting docs were written for ourselves (deployment paths,
internal product comparisons, internal task lists, build pipeline
artifacts) instead of for users of the software. This pass refocuses
them on what the software is, how to install, configure, and use it.

- README.md: full rewrite. New shape — What it does / Architecture /
  Build / Run / Configuration / MCP tools / Security model / Status /
  License / Dependencies. Drops the internal "why we built it"
  narrative, drops phase-status claims that drifted stale, drops
  internal deployment paths.
- ROADMAP.md: deleted. Was an internal task-list with [x]/[ ] items
  showing incremental private development. The README's Status
  section now communicates what's actually shipped.
- docs/architecture.md: scrub cross-project comparisons referencing
  unrelated internal Sulkta codebases.
- aiken-escrow/README.md: drop reference to a non-existent spec file;
  rewrite the Status checklist to reflect what's actually done
  rather than what was open at the time of writing.
- aiken-escrow/README.md: scrub internal image names +
  container paths; the audit findings (chain hashes, validator hash,
  what each tx proved) are the public-useful part and stay.
- aiken-escrow/README.md: drop references to
  feature-flag-gated branches that no longer exist.
- Dockerfile: drop the dead `escrow_wip surface` phrase from comments.
- Cargo.toml: drop the cross-project comparison comment that named
  an unrelated internal service.
- crates/aldabra-{core,dao}: scrub internal preprod-test naming from
  source comments — same technical content, generic phrasing.
2026-05-10 20:56:25 -07:00
Sulkta
a7877c32d0 docs: scrub remaining internal repo + tooling references
- README.md: drop "first Sulkta Rust project — workout for crafting-
  table's Rust toolchain" paragraph + the `ci-runner build aldabra`
  recipe. Both reference non-public internal infrastructure.
- Dockerfile: drop "Built nightly on the build host (see infra-repo/scripts/
  build.sh)" comment + the `registry.example.com:5000/aldabra/mcp`
  internal image-name advertisement.
- Cargo.toml: drop the comment block referencing the deleted
  `docs/internal-build-rewrites.md` + `ci-runner + the build host + dev
  hosts` internal-builds note. The patch block stands on its
  own.
2026-05-10 20:51:21 -07:00
Sulkta
91dcde6aab docs: scrub internal host references from public docs
Removed mentions of dedicated servers + Sulkta-runs-its-own-Koios claims from
README + module doc-comments + Cargo.toml descriptions. aldabra works
against any Koios endpoint — public api.koios.rest, preprod/preview,
or operator-self-hosted — so the docs now reflect that capability
neutrally instead of advertising our internal infra.
2026-05-10 18:24:57 -07:00
Sulkta
2587143a03 chore(build): drop URL rewrite layer + remove internal-rewrites doc
GitHub is canonical for aldabra now (per 2026-05-10 architecture call —
Gitea is a pull-mirror cache, not a forge we publish to). Build process
fetches pallas direct from github.com, no rewrite needed.

- Dockerfile: drop the `--mount=type=secret` git_credentials dance + the
  url.insteadOf rewrite. cargo fetches pallas straight from
  github.com/Sulkta-Coop/pallas at the locked SHA. No secret needed.
- docs/internal-build-rewrites.md: removed. The rewrite was the entire
  reason for the doc, and the rewrite is gone.

Internal builds (the build host / ci-runner / dev hosts) still hit
github.com for pallas, same as external builds. One extra WAN hop per
crate, but consistent everywhere and no environment-specific config.
2026-05-10 17:32:25 -07:00
Sulkta
2372bd2e46 chore: replace LAN-IP refs with public mirror URLs
Cargo.toml + Cargo.lock now point at https://github.com/Sulkta-Coop/pallas
for the pallas-fork patch entries. External clones from either public
mirror (github.com/Sulkta-Coop or gitlab.com/sulkta) build out of the box
— no LAN access needed.

internal builds short-circuit to the git remote via a `git config
url.X.insteadOf` rewrite on each host. Symmetric: covers both github and
gitlab → gitea. Same locked SHA either way; routing is environment-level,
not source-of-truth. See docs/internal-build-rewrites.md.

Dockerfile build-time rewrite also flipped to take public-URL inputs;
uses `git.sulkta.com` instead of the bare LAN IP.
2026-05-10 14:46:05 -07:00
Sulkta
fc29ad6e03 feat(escrow): drop escrow_wip feature gate, ship as default surface
Per Sulkta 2026-05-09 directive: after the audit + preprod E2E green-light
(6/6 builders, 9 successful txs, 0 failures), drop the compile-time gate
and integrate escrow as a default-on feature. The "not third-party
audited" framing becomes a runtime notice carried by escrow_open_unsigned
rather than a Cargo feature.

Changes:
- aldabra-dao/Cargo.toml: drop [features] block + escrow_wip = []
- aldabra-dao/src/agora/mod.rs: pub mod escrow (no cfg gate)
- aldabra-dao/src/builder/mod.rs: 6 escrow_* modules unconditional
- aldabra-mcp/Cargo.toml: drop features = ["escrow_wip"] from dao dep
- aldabra-mcp/src/tools.rs:
  - Drop "WIP — UNAUDITED:" prefix from all 6 escrow tool descriptions
  - Drop "wip_warning" JSON field from all 6 spend-tool responses
  - Add "audit_notice" field on escrow_open_unsigned response only
    (per Sulkta's framing — once-per-escrow-conversation, not repeated
    on every subsequent tool)
  - Update section header comment to reflect post-WIP status
- 7 escrow source files (1 agora + 6 builder): replace
  "WIP / UNAUDITED. Feature-gated behind escrow_wip" docstring with
  "Not third-party audited — preprod-only" + audit doc reference

Verified: 133 dao tests pass (was 132 under --features escrow_wip;
+1 from the rejects_no_initial_contributor test that's now always
compiled). aldabra-mcp release build clean.

The runtime audit_notice on escrow_open_unsigned reads:
  "This escrow validator has had an internal review and a 9-tx preprod
   E2E pass, but has NOT been audited by an external third party. Use
   at your own risk. If the user is opening this with anything beyond
   test-net or low-value funds, pass this notice along and confirm they
   accept the risk. Validator hash: a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d."

This carries the same caveat the WIP framing did, but in a form the
calling agent can surface inline to the user opening the escrow.
2026-05-09 23:08:30 -07:00
Sulkta
7daa62b5e5 fix(escrow_wip): apply 2026-05-09 internal audit findings
Two HIGH validator-side bugs + several MED/LOW off-chain issues found
in the subagent-driven audit on this branch. New validator hash:
a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d.

## HIGH-1: Deposit redeemer let depositors drain tokens

aiken-escrow/validators/escrow.ak Deposit branch now requires
`value_geq_value(new_value, in_value)` before computing net_added.
Previously net_added could carry negative quantities (when new_value
< in_value component-wise), letting a depositor write a matching
new_d.deposits with reduced values and pocket the difference as
wallet change. Latent under v1 ADA-only MCP usage but the validator
must hold against all callers.

## HIGH-2: Empty/partial deposits enabled funds drain via Veto/Refund

Veto and Refund branches now require
`value_eq(deposits_to_value(d.deposits), in_value)` — the tracked
deposits must account for the full locked value. Previously
`refund_outputs_satisfy(_, [])` was vacuously true on empty deposits,
so a driver could fire Veto/Refund on an escrow opened with
`initial_contributor=None` (deposits=[], in_value>0) and pocket the
input's lovelace as change.

Defense in depth: escrow_open builder now refuses
`initial_contributor=None`. New helper `deposits_to_value` folds
deposit FlatValues into a Value via `assets.add` for the equality
check.

## MED: off-chain fixes

- escrow_open min-utxo bumped 1M → 2M (Conway-era inline-datum
  + script-address outputs need ~1.4-1.7 ADA, NOT the 1 ADA default).
- escrow_settle_unsigned + escrow_refund_timeout_unsigned now derive
  `validity_lower_ms` via slot_to_posix_ms(network, slot) instead of
  Koios's `block_time*1000` — the chain reconstructs `lower` from the
  slot, so Koios's ~1s drift could pass off-chain preflight while the
  chain rejects at the strict-`>` boundary.
- escrow_open_unsigned MCP tool no longer accepts (and silently
  discards) `fee_lovelace` — the unsigned-tx builder auto-estimates.

## LOW: defensive depth

- escrow_veto + escrow_refund_timeout: `qty as u64` → `u64::try_from`
  so a corrupt or adversarial datum with negative i128 qty can't slip
  through with a wraparound.

## Tests

- 36 escrow builder tests pass (added rejects_no_initial_contributor)
- 132 dao tests pass under --features escrow_wip
- aldabra-mcp release build clean

## Infra

- Validator artifact files (plutus.json, validator.cbor.hex)
  regenerated. Dockerfile already wired to bake them at
  /etc/aldabra/escrow/ for MCP tools' validator_script_path arg.
- Internal audit findings written up at
  aiken-escrow/README.md including the v2-deferred
  items (multi-asset spend-input, lovelace-not-cross-checked, etc.)

Third-party audit still required before any mainnet deployment.
2026-05-09 14:06:17 -07:00
Sulkta
ef38ff0e57 feat(escrow_wip): MCP escrow spend-tool surface (deposit/agree/veto/settle/refund_timeout)
Five new MCP tools wrapping the four Plutus V3 spend builders shipped
earlier in this branch:

- escrow_deposit_unsigned     → Deposit redeemer (continuing-output state)
- escrow_agree_unsigned       → Agree redeemer (Open → Agreed{at=upper}, both sign)
- escrow_veto_unsigned        → Veto redeemer (Agreed → multi-output refund)
- escrow_settle_unsigned      → Settle redeemer (Agreed → recipient payout)
- escrow_refund_timeout_unsigned → Refund redeemer (Open after open_deadline → multi-output refund)

Each takes the existing escrow UTxO ref + lovelace + datum_cbor_hex
(caller pulls via chain_address_info), the V3 validator UPLC
(inline hex OR file-path to dodge the >4500-char MCP transport bug),
the redeemer-specific args, and fee_lovelace. Validity windows
default to 30 min from chain tip; agree's window auto-clamps to
open_deadline_ms when needed.

Helper additions:
- EscrowDatum::from_cbor_hex on aldabra-dao keeps pallas-codec /
  pallas-primitives direct deps OUT of aldabra-mcp.
- decode_pkh28, resolve_validator_required, build_escrow_spend_in,
  fetch_tip_slot_ms in tools.rs — small helpers shared by all 5
  spend tools.

Drops the [features] section on aldabra-mcp's Cargo.toml. rmcp 0.1.5's
#[tool(tool_box)] macro scans the impl AST and references every
#[tool]-annotated method's generated wrapper regardless of cfg
eligibility — cfg-on-method gating fails to compile when the feature
is off because the macro emits unresolved symbol references. Pivot:
always-pull aldabra-dao/escrow_wip via the dep itself. The runtime
gate is the "WIP — UNAUDITED:" prefix in every tool description plus
the "wip_warning" field in JSON responses; the dao crate's escrow_wip
feature still gates downstream Rust consumers that want source-level
opt-out.

Verified: aldabra-mcp builds clean (default + release). 132 aldabra-
dao tests pass under --features escrow_wip including all 35 escrow
builder tests. Release binary produced.
2026-05-09 13:36:44 -07:00
Sulkta
54a90a5f8d feat(escrow_wip): MCP escrow_open_unsigned tool + Cargo feature
Adds the first MCP tool surface for the escrow validator: takes
party_a/b/recipient pkh hex + open_deadline + lock_period + optional
initial_contributor + initial_lovelace, calls
build_unsigned_escrow_open, returns CBOR-hex of the unsigned tx.

Tool description prefixed with "WIP — UNAUDITED:" per the handoff
discipline. Body includes a wip_warning field reminding callers this
is preprod-only.

Also propagates the escrow_wip feature from aldabra-mcp to
aldabra-dao so building the MCP binary with --features escrow_wip
correctly enables the dao crate's escrow surface.

Cleaned up unused-import warnings (ESCROW_SPEND_EX_UNITS pulled to
test-only scope in agree/settle/veto/refund_timeout — the impl bodies
take ex_units explicitly via args, only tests reference the constant).
35/35 escrow builder tests still pass; aldabra-mcp builds clean with
--features escrow_wip.

The remaining 4 unsigned-write tools (deposit/agree/veto/settle/
refund_timeout) follow the same pattern: pull wallet utxos, optionally
discover the escrow UTxO from chain at script_address, decode datum,
call the corresponding builder, return CBOR-hex JSON.
2026-05-09 12:49:46 -07:00
Sulkta
0feb0800dd feat(escrow_wip): build_unsigned_escrow_refund_timeout builder
Plutus V3 spend that consumes an Open escrow whose open_deadline has
elapsed and refunds every contributor. Same multi-output refund shape
as Veto, different validator gates: state==Open AND lower >
open_deadline_ms (strict gt). No required signer — time gate is the
only gate.

Reuses enterprise_address_for from escrow_veto. 5 tests: not-Open
reject, open-window-not-elapsed reject (off-by-one strict gt),
empty-escrow reject, two-contributor full refund happy path,
outsider driver works.

Closes the 5-redeemer escrow surface: open + deposit + agree + veto +
settle + refund_timeout. 35/35 escrow builder tests pass.
2026-05-09 12:42:16 -07:00
Sulkta
3cf61c79da feat(escrow_wip): build_unsigned_escrow_settle builder
Plutus V3 spend that consumes an Agreed escrow whose lock window has
elapsed and pays the entire in_value to the recipient's enterprise
address. Validator gate: state==Agreed AND lower > agreed_at_ms +
lock_period_ms (strict gt). No signer required by validator.

Driver pays fee via funding utxo + collateral; doesn't need to be a
party (test asserts this — anyone can push Settle once the lock
elapses).

Made enterprise_address_for pub(super) in escrow_veto so settle and
refund_timeout can share it. Mirrors the validator's
pkh_to_base_address byte-for-byte.

5 tests: not-Agreed reject, lock-not-elapsed reject (off-by-one strict
gt), empty-escrow reject, happy-path pays full in_value, outsider
driver works.
2026-05-09 12:40:06 -07:00
Sulkta
aeac3e8e0a feat(escrow_wip): build_unsigned_escrow_veto builder
Plutus V3 spend that consumes an Agreed escrow and refunds every
contributor to their enterprise (null-stake) address. Either party
can fire — validator gate is signed_by(a) || signed_by(b).

Off-chain reconstructs the validator's pkh_to_base_address via
ShelleyAddress::new(net, payment=key_hash(pkh), delegation=Null).
Below-floor deposits are topped up from the driver's funding utxo
to keep refund outputs submittable (validator's value_geq_flat
permits paying more than the deposit value).

7 tests: not-Agreed reject, outsider-driver reject, no-deposits
reject, two-contributor full refund happy path, below-floor topup,
either-party-drives, enterprise-addr construction is testnet for
preprod (sanity check on the network bit).
2026-05-09 12:37:51 -07:00
Sulkta
415c099240 feat(escrow_wip): build_unsigned_escrow_agree builder
Plutus V3 spend that flips state Open → Agreed { agreed_at_ms = upper }.
Both party_a and party_b sign — driver returns partially-signed CBOR,
co-signer adds witness via wallet_sign_partial, signed tx submitted.

Validator enforces validity_upper ≤ open_deadline_ms (preflighted),
value preserved bit-exact, deposits preserved bit-exact via cbor.serialise
equality (passes naturally since we copy the Vec verbatim and
to_plutus_data emits deterministic KeyValuePairs).

7 tests cover negative paths (non-Open state, outsider driver,
post-deadline upper) and happy paths (a-drives, b-drives, datum
state flips correctly, immutable fields + deposits preserved).
2026-05-09 12:34:57 -07:00
Sulkta
ec3e98a1c5 feat(escrow_wip): build_unsigned_escrow_deposit builder
Plutus V3 spend with continuing-output state transition. Validator runs
Deposit { contributor } redeemer.

v1 limitation: ADA-only deposits. Multi-asset deposits are deferred —
Aiken's flat_merge uses right-fold which produces REVERSED appended-
policy ordering vs naive forward iteration. Single-entry net_added
(lovelace-only) trivially collapses any ordering ambiguity, so Rust's
existing value_merge matches Aiken's flat_merge byte-for-byte for v1.

Mirrors all six validator checks client-side as preflight (state==Open,
contributor in {a,b}, signed_by, continuing-output exists, datum equal
except deposits, deposits canonicality). 9 unit tests cover both
negative paths and happy-path datum mutations (new entry append,
existing entry merge, immutable fields preserved).

Inlines V3 validator CBOR in the tx witness for v1; reference-script
optimization is a v2 follow-up. Sets V3 cost model via language_view
to satisfy script_data_hash.
2026-05-09 12:29:12 -07:00
Sulkta
23dfbbae80 feat(escrow_wip): build_unsigned_escrow_open builder
⚠ WIP — UNAUDITED. Feature-gated.

The simplest of the five escrow paths: create a fresh escrow UTxO at
the validator script address with an inline EscrowDatum + initial
deposit (or empty deposits for two-step funding). No Plutus spend
involved — this is just a wallet send to the script address with the
typed datum encoded as inline CBOR.

Wraps aldabra-core::build_unsigned_payment_extras to add:
- Typed EscrowDatum construction (avoids hand-encoding CBOR)
- Preflight: initial_contributor must be party_a or party_b
- Preflight: initial_lovelace clears protocol min_utxo floor
- Standard summary string for MCP wrappers

Tests: 2 preflight rejections (unauthorized contributor, sub-min-utxo
funding) plus the 10 codec roundtrips. All 12 pass under
`cargo test -p aldabra-dao --features escrow_wip escrow`.

Workspace also builds clean with default features (escrow_wip disabled);
the entire escrow surface compiles out of release builds.

Remaining builders (deposit / agree / veto / settle / refund) all
involve script spends, continuing-output datum diffs, and time-bound
validity — deferred to next session for proper care.
2026-05-09 11:42:39 -07:00
Sulkta
b1a95284b5 feat(escrow_wip): aiken validator + plutus.json blueprint
⚠ WIP — UNAUDITED. Plutus V3, Aiken v1.1.21. Preprod-only.

Five-redeemer two-party agreement-with-veto escrow validator. Mirrors the
off-chain codecs at crates/aldabra-dao/src/agora/escrow.rs.

Validator script hash: 223aa7ace4a98ff5b8f8988c1c07b846c046de1a2bc9e8dc77411486
Compiled UPLC size: 7902 bytes.

Datum: ProductIsData (Constr 0 [a, b, recipient, deadline, lock, state, deposits]).
Redeemer: Constr 0..4 (Deposit | Agree | Veto | Settle | Refund).
DepositEntry.value uses concrete Pairs<ByteArray, Pairs<ByteArray, Int>>
since cardano/assets.Value is opaque (datums require concrete types).
Pairs encode as Plutus Map at the dataType layer — matches the off-chain
EscrowValue codec's PlutusData::Map(KeyValuePairs) emission.

Build: cd aiken-escrow && aiken build (produces plutus.json blueprint).

Threat-model gaps explicitly documented in aiken-escrow/README.md:
- CBOR canonicality of Pairs serialisation (validator equality check)
- Stake-credential null'd on refund outputs (intentional — protects pool
  delegation privacy at cost of stake reward routing)
- No min-utxo enforcement on refund legs (off-chain builder's job)
- No multi-script-input cross-UTxO consistency

External audit gates mainnet deployment.
2026-05-09 11:38:45 -07:00
Sulkta
aff39a668f feat(escrow_wip): EscrowDatum / EscrowRedeemer / EscrowValue codecs
⚠ WIP — UNAUDITED. Feature-gated behind `escrow_wip`; out of default builds.

Mirrors aiken validator at aiken-escrow/escrow/validators/escrow.ak (also
WIP). Spec at aiken-escrow/README.md.

Five-redeemer two-party agreement-with-veto escrow:
- Open → both sign Agree → Agreed{at} → Settle (after lock_period)
- Agreed → either party Veto → per-contributor refund
- Open → Refund (after open_deadline) → per-contributor refund

Datum: ProductIsData (Constr 0 [a, b, recipient, deadline, lock, state, deposits]).
EscrowState: enum Open | Agreed{at} encoded as Constr 0/1 (with payload).
EscrowDeposit: per-contributor (pkh, Value) entry, Constr 0.
EscrowValue: Plutus Map<PolicyId, Map<AssetName, Int>>; preserves on-chain
ordering for cbor-equality checks the validator does on continuing-output
deposits diff.

Tests: 10 codec roundtrips (Open/Agreed states, ada-only/multi-asset values,
deposit lookup by pkh, value-merge sum). All pass under
`cargo test -p aldabra-dao --features escrow_wip escrow::`.

Builders + MCP tools land in follow-up commits on this branch.
2026-05-09 11:32:26 -07:00
Sulkta
761a0105c8 merge: bring audit-cleanup into main — DAO + Conway + audit closeouts
audit-cleanup is the consolidating branch from Tracks #20-#38: 4 DAO MCP
write paths (proposal_create/cosign/vote/advance), 1 stake retract path,
8 chain_* Koios passthroughs, Conway DRep tools, vote-TTL/posix_time fix
(Track #30), MCP json_object_schema fix (Track #37), retract-cooldown
gating fix (Track #38), pallas large-bytestring fix, ref-script wallet
support, fmt + clippy --fix sweep.

main was 18 wallet/plutus-phase commits ahead since the branch fork at
532b3d3. audit-cleanup carried equivalent functionality (chain_tx_info,
min_utxo guard, plutus mint, etc.) so all 11 conflicts resolved by
taking audit-cleanup's version. Tree post-merge is byte-identical to
origin/audit-cleanup.
2026-05-09 11:26:29 -07:00
Sulkta
03b5efb3b2 audit: cargo fmt + clippy --fix across workspace + retract_votes cooldown bug fix
Surfaced by Track #38 code audit (2026-05-09):

1. cargo fmt --all: 217 formatting diffs across 35 files. Pure
   whitespace; no semantic changes.

2. cargo clippy --fix: 30 warnings -> 10. Auto-applied:
   - useless format!() (3 sites in builder/proposal_*.rs)
   - needless_borrow_for_generic_args (4 sites)
   - cloned_ref_to_slice_refs (1 site, builder/proposal_cosign.rs)
   - derivable_impls (1 site, dao/config.rs)
   - unused imports/variables (3 sites)

   Remaining 10 warnings are non-trivial (too_many_arguments on a
   constructor at 8 args, FromStr trait shadow, doc_lazy_continuation
   on a few comment blocks). Filed as tech-debt; no action this pass.

3. cargo audit: 0 vulnerabilities. 2 unmaintained advisories on
   transitive deps:
   - paste 1.0.15 (RUSTSEC-2024-0436) via rmcp + pallas-traverse
   - proc-macro-error 1.0.4 (RUSTSEC-2024-0370) via age->i18n-embed-fl
   Both upstream; tracked but no action needed locally.

4. Test failure surfaced: builder::proposal_retract_votes::tests::
   voting_ready_in_window_subtracts_vote_weight failed — cooldown
   check was applied unconditionally for RemoveVoterLockOnly mode,
   blocking the legitimate 'retract during voting window' path
   where the proposal datum mutates (vote weight subtraction). Per
   Agora's premoveLocks rule, cooldown only applies when retracting
   AFTER voting closed but BEFORE Finished — not during the active
   voting window. Fixed by gating cooldown on
   '!proposal_datum_will_change' so the in-window retract path
   bypasses cooldown the same way RemoveAllLocks does.

   Test: 87/87 aldabra-dao lib tests pass post-fix (was 86/87).
2026-05-09 10:27:48 -07:00
Sulkta
0987d5de12 merge: schema fix for Value-typed metadata/policy MCP args 2026-05-09 09:23:45 -07:00
Sulkta
aa9b16995e fix(mcp): force type:object on Value-typed metadata/policy args
schemars derives an empty (no-type) JSON Schema for Option<serde_json::Value>
and serde_json::Value fields. Claude Code's MCP client interprets schema-
without-type as 'string-encoded' and JSON-stringifies the user's {...}
before sending — at which point the server-side validation
`value.is_object()` returns false and the tool errors with
'CIP-25 metadata must be a JSON object' / 'CIP-68 metadata must be a
JSON object'.

Surfaced during Track #37 E2E test (2026-05-09): wallet_mint and
wallet_mint_cip68_nft both rejected metadata-as-object args from
Claude Code while the SAME object via raw stdio MCP works fine —
proves the issue is client-side schema interpretation, not server.

Fix: add a json_object_schema helper that emits {type: object,
additionalProperties: true} and annotate every metadata + policy
field with #[schemars(schema_with = "json_object_schema")].
Affected fields:
- MintArgs.metadata
- MintUnsignedArgs.policy + .metadata
- Cip68NftArgs.metadata

additionalProperties is left wide-open since these args really do
accept arbitrary keys (CIP-25/CIP-68 are open-ended schemas).
2026-05-09 09:23:34 -07:00
Sulkta
8f49668b86 merge: vote TTL/posix_time consistency fix
Validates Track #30 vote bug. Working vote tx d9142849ac0f2525ab942a979c5662bf805a9e0c2184c4473f77e586ff3eeaee
on preprod_test2 prop #8, block 4690176. Three-day chase reduced to:
TX TTL slot ≠ slot underlying validity_upper_ms (which the MCP layer
clamped to voting_end). Validator's ppermitVote synthesizes the
expected Voted lock from chain's reconstructed validRange.upperBound
(derived from TTL slot); ours used the clamped value. Mismatch →
ponlyLocksUpdated assertion fails → silent UPLC error.

See audits/agora-vote-bug-2026-05-08/10-fix-validated.md for the
byte-level proof + comparison vs Clarity's working vote 4f2fac98....
2026-05-09 08:27:12 -07:00
Sulkta
e98be0749f fix(dao,mcp): tie vote tx TTL to validity_upper_slot so Voted.posix_time matches chain's reconstructed validRange.upperBound
Prior to this fix, proposal_vote.rs:486 set the tx TTL to
`tip_slot + VALIDITY_RANGE_SLOTS` (the unclamped default) while the
new stake output's Voted lock embedded `posix_time = validity_upper_ms`
which the MCP layer at tools.rs:3197 may have CLAMPED to
`voting_end_slot` to keep the validity range inside the voting window.
The TX TTL slot and the slot underlying validity_upper_ms then
diverged whenever the clamp fired.

The chain reconstructs txInfo.validRange.upperBound from the TX TTL.
Agora's ppermitVote synthesizes the expected Voted lock with
`posix_time = upperBound` and ponlyLocksUpdated compares it to our
output's lock. With a slot mismatch the lock posix_time differs by
(default - voting_end) seconds — for a 1799-slot default and a small
voting window remainder, this is hundreds-to-thousands of seconds.
The mismatch surfaces as a silent UPLC error in the stake validator
with no preceding ptrace, exactly matching the
'validator crashed / exited prematurely' chain rejections we've been
chasing for two days.

Verified hypothesis against working Clarity vote
4f2fac985a08db2349ef2a650bb66ca6cd42fab1ecc5976bb673687666922503:
TTL slot 130276129 → posix_ms 1721842420000, Voted.posix_time
1721842420000 — exact match (diff 0 ms). Our failing prop #5 vote
4f2fac98... had TTL.posix_ms = 1778296041000 vs Voted.posix_time
1778294743000 = 1298s mismatch.

Fix: introduce explicit `validity_upper_slot` field on
ProposalVoteArgs alongside `validity_upper_ms`. Caller sets BOTH
from the same source (MCP layer already had this slot in scope at
the clamp site). The builder's TTL now uses validity_upper_slot
(so the chain computes the same upperBound as our datum embedded).

Other builders (cosign / advance / retract_votes) don't write a
datum field that depends on slot↔ms conversion, so they're not
affected by this bug.

Test fixture updated to derive validity_upper_slot from
validity_upper_ms via the mainnet shelley-zero constants.
2026-05-09 06:09:48 -07:00
Sulkta
f2ae5367a7 feat(mcp): add policy_cbor_path arg to wallet_plutus_mint_unsigned
Mirrors the reference_script_path workaround already on wallet_send.
Lets the caller hand aldabra a path to a hex-CBOR file inside the
container instead of pasting the hex inline as a JSON-RPC arg, which
lets us bypass the 2026-05-07 MCP large-string transport bug (>~ 4500
char hex strings get a 1-byte truncation between Claude Code and
aldabra's stdio reader, surfacing as 'odd length' decode errors and
blocking debug-build minting policies).

policy_cbor_hex becomes Option<String>; new policy_cbor_path:
Option<String> sits next to it. New resolver helper
resolve_policy_cbor_bytes mirrors resolve_ref_script_bytes — at most
one of the two may be set, exactly one must be set. Whitespace is
stripped from file contents so the file may have trailing newlines.

Unblocks Track #33 / preprod_test3: debug-build validators that
overshoot the transport ceiling can now be minted via path arg.
2026-05-08 18:50:26 -07:00
Sulkta
cbf69161ab feat(dao,mcp): proposal_retract_votes builder + dao_stake_retract_votes_unsigned
Closes the destroy-with-locks gap: a stake that voted/created/cosigned a
proposal can now drop those locks once the proposal resolves (or once
voter cooldown elapses), letting it become destroyable via
dao_stake_destroy_unsigned.

Tx shape mirrors proposal_vote: stake input (RetractVotes redeemer) +
proposal input (UnlockStake redeemer) + wallet funding, two reference
scripts, two outputs. Mode auto-derived from proposal status:
  - Finished       → RemoveAllLocks  (drop Created/Cosigned/Voted for id)
  - any other      → RemoveVoterLockOnly (drop only past-cooldown Voted)

When proposal is VotingReady AND tx-validity sits inside the voting
window, also subtracts stake.staked_amount from proposal.votes[voted_tag]
(matches the proposal validator's `shouldUpdateVotes` path at
Agora/Proposal/Scripts.hs:606).

Pre-flights every validator check: ownership/delegation, at least one
lock for proposal_id (proposal validator's pisIrrelevant), Voted-lock
cooldown (Stake/Redeemers.hs:354), and rejects the VotingReady-no-Voted
edge case where the validator's "Votes changed" assertion would fail.

Eight unit tests covering: Finished-drops-all, VotingReady-window-
subtracts, no-locks-for-proposal rejection, cooldown-not-elapsed
rejection, no-Voted-in-window rejection, voter-not-owner-or-delegate
rejection, Locked-after-window-drops-Voted-only.

Validator is from existing `registry.example.com:5000/aldabra/mcp@425c981` with
StakeRedeemer::RetractVotes and ProposalRedeemer::UnlockStake already
landed; this just wires the builder + MCP tool.
2026-05-08 13:24:24 -07:00
Sulkta
425c981682 fix(mcp): clamp vote validity_upper to voting_end_slot when range overshoots
Same H-2-class issue as the advance Draft→VotingReady clamp. The
vote MCP tool computed validity_upper = tip_slot + 1799 then
errored if it overshot voting_end_check. On 30-min Sulkta-shape
DAOs the voting window is 30 min wide, and the moment chain time
crosses into the voting window the default 1799-slot upper bound
lands ~30 min past tip — already past voting_end if voting_start
was a few minutes ago.

Now: when default upper would overshoot, clamp validity_upper_slot
to voting_end_slot. Reject if remaining slots ≤ 5 (chain has no
room to include).

Caught 2026-05-08 trying to vote on preprod_test2 proposal #1
during a clean voting window — tx_upper landed 135s past
voting_end. Clamp lets the vote tx fit.

Cosign builder still has the same raw tip_slot pattern; deferred
since cosign also requires within-Draft semantics and we don't
have a multi-stake test yet to exercise it.
2026-05-08 11:48:36 -07:00
Sulkta
892e4b0ae1 fix(dao,mcp): clamp proposal_advance validity range to fit phase window
The advance builder previously hard-coded the validity range to
[tip_slot, tip_slot + VALIDITY_RANGE_SLOTS=1799]. For early
Draft→VotingReady advance with the wide 1799-slot range, the upper
bound shoots ~30min past starting_time — way past drafting_end on
any DAO with windows narrower than 30 min, including Sulkta-shape
30-min DAOs whose drafting period happens to start partly elapsed.
Validator's getTimingRelation rejects straddles and the MCP layer
then errored 'tx validity range straddles drafting period
boundary'.

Two-part fix:

1. ProposalAdvanceArgs gains optional valid_from_slot_override +
   invalid_from_slot_override fields. None preserves legacy behavior;
   Some(...) lets the MCP layer dictate a clamped range. Builder
   defends against degenerate ranges (invalid_from <= valid_from).

2. MCP-side dao_proposal_advance_unsigned, when transition is
   DraftToVotingReady or VotingReadyToLocked and the natural [tip,
   tip+1799] would overshoot the period end, clamps invalid_from
   to the period_end slot. Refuses if remaining slots < 5 (chain
   has no room to include the tx) and prompts the caller to wait
   for the failed-too-late path instead.

Caught 2026-05-08 trying to drive preprod_test2 proposal #0
through the proper Draft→VotingReady arc — chain time was ~3 min
past starting_time, so 1799-slot range overshot drafting_end by
~27 min. Same code path now clamps to the remaining ~27 min of
drafting period.

Closes audit finding H-2 for the DraftToVotingReady and
VotingReadyToLocked transitions. Cosign + vote builders also have
H-2 (raw tip_slot for validity_from); those are deferred.
2026-05-08 11:09:26 -07:00
Sulkta
48679883ec feat(mcp,chain,dao): support Koios paid-tier bearer via ALDABRA_KOIOS_BEARER env
Adds optional Authorization: Bearer <token> on every Koios request,
sourced from ALDABRA_KOIOS_BEARER env var only — never from the
on-disk config.toml, never from CLI args, never hardcoded. Bearers
are credentials and the on-disk config dir gets routinely backed up;
keeping them env-only guarantees rotations don't leak into snapshots.

Wired through three Koios clients:
- aldabra-chain::KoiosClient — new with_timeout_and_bearer ctor;
  legacy new() / with_timeout() route through it with bearer=None.
- aldabra-dao::KoiosDaoReader — new with_bearer ctor; ditto.
- aldabra-dao::KoiosDiscoveryClient — new with_bearer ctor; ditto.

Bearer is set as a default header on the reqwest client builder so
every request inherits it without per-call boilerplate.
HeaderValue::set_sensitive(true) prevents the value from showing
in reqwest's debug-format output.

Config wiring (aldabra-mcp::config::Config):
- New koios_bearer: Option<String> field. Loaded ONLY from
  ALDABRA_KOIOS_BEARER env var; absent or empty-string means None.
- Startup tracing logs koios_bearer_set: bool — never the value.

WalletInner caches the bearer alongside the koios_base so the
on-demand KoiosDiscoveryClient (constructed inside
dao_discover_scripts) inherits paid-tier auth too.

Motivation: 2026-05-08 preprod_test2 bringup tripped Koios free-tier
daily quota (5240 req/day, 'Exceeded Tier Limit') mid-deploy. Sulkta
provided a paid-tier JWT (Aldabra project, exp 2026-06-26). Wiring
via env var lets the operator (systemd EnvironmentFile, docker run
-e, or k8s Secret) inject it without touching code or config files.
2026-05-08 10:19:06 -07:00
Sulkta
9547bd95e4 fix(core): bump WITNESS_OVERHEAD_BYTES from 256 to 512
256 still underbid by ~16 bytes (721 lovelace) on the same
preprod_test2 governor mint shape that 128 missed by 144 bytes.
The actual CBOR delta between the unsigned tx (def-length witness
set arrays, no vkey witness, no redeemer expansion) and the signed
tx (indef-length flips, vkey witness, finalized redeemer) is
~270 bytes for this shape, not the 144 the first FeeTooSmallUTxO
suggested.

512 gives plenty of headroom — worst-case ~22k lovelace overestimate
which is trivial. For multi-sig flows with N vkey witnesses, this
needs revisiting; plutus_mint's only signer today is the wallet's
own payment key so a single-vkey budget is correct.
2026-05-08 07:42:06 -07:00
Sulkta
f4aa9ae037 fix(core): bump WITNESS_OVERHEAD_BYTES from 128 to 256
128 underbid by ~144 bytes on a 3-input plutus_mint with inline V2
policy CBOR (preprod_test2 governor bootstrap 2026-05-08, hit
FeeTooSmallUTxO 6353 lovelace short). The original constant covered
the vkey witness alone but missed redeemer ex_units final-pass
inflation + CBOR length-prefix shifts between unsigned (def-length)
and signed (often indef-length) witness-set arrays.

256 is plenty for any single-vkey case and still cheap fee-wise
(56-lovelace difference at the Cardano per-byte rate). For multi-sig
flows we'd revisit, but plutus_mint's only signer is the wallet's
own payment key.
2026-05-08 07:12:04 -07:00
Sulkta
adc2154437 fix(dao): center starting_time in proposal_create validity window
For tiny-window test DAOs (preprod_test: 30s), the prior anchor
(valid_from = starting_time, invalid_from = starting_time + 30)
gave zero past-side slack. With koios block_time vs real chain
clock skewing ±60s on the public endpoint, hitting that window
is essentially a coin flip — the tx submits but never confirms
because the next block lands after invalid_from.

Centering keeps the validator-required width unchanged but moves
valid_from to starting_time - 15, so the chain now has 15s of
past-side slack to land the tx in a block. Same width, same
in-script time check (starting_time still ∈ [valid_from, invalid_from)),
better landing odds.

Sulkta-shape DAOs (1800s windows) are unaffected: 900s of slack
each side is plenty either way.
2026-05-07 20:19:27 -07:00
Sulkta
3b445fae89 fix(dao): prepend (cons) Created lock instead of append in proposal_create
Agora's stake validator (ppermitVote) enforces output_locks =
pcons NEW_LOCK old_locks (head-cons, not append). proposal_create
was using Vec::push which appends, so when the stake had any
pre-existing locks the output lock order didn't match what the
validator expected and the chain rejected with CekError on the
stake validator (5178-byte script, hash 57d6b17f...).

The bug went undetected on 2026-05-07 because that day's first
proposal_create ran on a stake with locked_by = [], where push
and prepend produce the same single-element vector. Today's
proposal #1 attempt — stake already holding a Created lock for #0 —
flushed it out.

Mirror the prepend pattern that proposal_cosign and proposal_vote
already use: build new_locks with the new lock at index 0, then
extend with the old locks.
2026-05-07 19:43:10 -07:00
Sulkta
82613bb188 fix(dao): wire V2 cost model into advance/cosign/vote/stake_destroy
Same trap that hit proposal_create yesterday: every spending tx that
witnesses a PlutusV2 script (Agora's proposal_validator, stake_validator,
proposalSt policy on burn) needs language_view in the tx body so the
chain-side script_data_hash matches the off-chain one. Without this the
chain rejects with PPViewHashesDontMatch.

proposal_create got the fix in 6b8d2ccc. The other four builders shipped
without it, so dao_proposal_advance_unsigned (Draft → Finished on
preprod_test #0) hit PPViewHashesDontMatch on submit. Mirror the same
language_view + ScriptKind::PlutusV2 + PLUTUS_V2_COST_MODEL_PREPROD
wiring into the remaining four staging chains.
2026-05-07 18:56:57 -07:00
Sulkta
7b4215423a fix(dao): use PermitVote (not DepositWithdraw) for stake spend on proposal_create
Stake validator's DepositWithdraw branch requires locked_by to stay
EMPTY. proposal_create wants to ADD a Created lock for the new
proposal — that's PermitVote's job, not DepositWithdraw's.

Caught by base64-decoding the CekError's failing-script header on
preprod_test today: 0x59 0x14 0x37 = bytes(5175) ⇒ 5178-byte script
= stake validator (57d6b17f), not the governor we'd been suspecting.

This is the audit C-2b fix that landed late.
2026-05-07 18:09:20 -07:00
Sulkta
a1c6ab3f08 fix(dao): anchor proposal-create validity range on starting_time_slot
Public Koios's tip endpoint can lag the actual chain by 100+ slots.
Under a 29-slot governor window that lag pushes invalid_after into
the past before the tx ever reaches a node — every retry hits
OutsideValidityIntervalUTxO no matter how fast we sign+submit.

Now: caller passes starting_time_slot derived from starting_time_ms
via the network's shelley constants; builder uses it as valid_from.
Caller can shift starting_time_ms slightly into the future to
compensate for MCP roundtrip latency. The on-chain
'pvalidateProposalStartingTime' is still satisfied because
starting_time_slot ∈ validRange by construction.
2026-05-07 17:31:45 -07:00
Sulkta
591f2f0861 fix(dao): clamp proposal-create validity range to per-DAO max_width
VALIDITY_RANGE_SLOTS const was hardcoded to 1799 (Sulkta's 30min budget
minus 1 slot). For tiny test DAOs (preprod_test: 30s) this overshoots
the governor's create_proposal_time_range_max_width and the validator
rejects with CekError on submit. Now: derive max width from
GovernorDatum.create_proposal_time_range_max_width / 1000 - 1, capped
at VALIDITY_RANGE_SLOTS for safety.
2026-05-07 17:12:10 -07:00
Sulkta
6b8d2cccbc fix(dao): wire V2 cost model into proposal_create staging
Without staging.language_view(), pallas does not compute
script_data_hash. Chain rejects the tx with PPViewHashesDontMatch.
Same trap that plutus_mint hit on 2026-05-07 — same fix here.

Caught attempting first dao_proposal_create_unsigned on preprod_test
DAO 2026-05-07 PM after deploying governor + proposal validator ref
UTxOs via the new file-path workaround for the MCP large-string bug.
2026-05-07 16:57:05 -07:00
Sulkta
9521ee79c9 mcp: add reference_script_path arg to bypass MCP large-string transport bug
Caught 2026-05-07: hex strings passed to MCP tool args > ~4500 chars
get a 1-byte truncation + structural rearrangement somewhere between
Claude Code and aldabra's stdio reader. Pallas + aldabra-core's
hex_decode are byte-clean (verified via crates/aldabra-dao/examples/
repro_script_corruption.rs); the corruption is purely in the
JSON-RPC-over-stdio transport layer.

Workaround: accept reference_script_path that points at a file
inside the aldabra container. Caller docker-cp's the hex file in,
passes the path via MCP arg, aldabra reads bytes locally — no
large strings cross the JSON-RPC wire.

Applies to wallet_send + wallet_send_unsigned. wallet_plutus_mint_*
tools still ride the hex-string path (small policies only, < 4500
chars). When we hit a Plutus policy that needs the workaround, port
the same pattern.
2026-05-07 16:45:53 -07:00
Sulkta
c3f9b6ca69 diag: also exercise aldabra-core's build_unsigned_payment_extras 2026-05-07 16:30:20 -07:00
Sulkta
285c6e5a84 diag: also test through aldabra's hex_decode 2026-05-07 16:26:55 -07:00
Sulkta
df61535d0f diag: reproducer also reports script bytes-header consistency 2026-05-07 16:24:34 -07:00
Sulkta
30bee07502 fix repro_script_corruption imports 2026-05-07 16:10:58 -07:00
Sulkta
05c29ac3ab diag: standalone reproducer for large-bytestring ref-script corruption
cargo run --example repro_script_corruption -p aldabra-dao --release

Reads a hex-encoded Plutus V2 script, builds a minimal Conway tx
with that script as inline reference, calls build_conway_raw, then
searches the tx body for the input bytes verbatim. Also tests the
known on-chain block-swap corruption fingerprint (bytes 2390-2398
swapped with bytes 2416-2424) to determine whether pallas
reproduces the corruption locally.

If verbatim found: pallas is byte-clean, bug is downstream
(transport / Koios / chain submit). If swapped variant found:
pallas itself produces the corruption.

No chain query, no MCP, no JSON-RPC — pure local serialization.
2026-05-07 16:09:41 -07:00
Sulkta
6fa93a3c68 plutus_mint: thread additional_signers into tx body's required_signers
On Babbage+, Plutus's TxInfo.signatories is populated only from the
tx body's required_signers field — vkey witnesses alone don't surface
in the script context. Without this, any Agora script that calls
pauthorizedBy/txSignedBy fails. Stake-bootstrap on preprod erred
because the owner pkh wasn't in signatories despite the wallet
signing the tx.

MCP layer always passes [wallet_pkh]; callers can append cosigner
pkhs for multi-sig flows.
2026-05-07 10:33:55 -07:00
Sulkta
1e20b71cc9 feat(examples): dump_stake — emit StakeDatum CBOR hex for bootstrap
Companion to dump_governor (committed earlier this branch). Edit
owner_pkh_hex + staked_amount in the source, then `cargo run` to
print the inline datum CBOR for a wallet_plutus_mint_unsigned call
that mints StakeST + sends to stakes_addr.

No locks at bootstrap (locked_by = []) and no delegation
(delegated_to = None). For a stake that's been used in proposals,
locked_by would carry the ProposalLock entries; reuse this scaffold
when reseeding a stake from a snapshot.
2026-05-07 10:21:27 -07:00
Sulkta
42a0e249b6 feat(plutus_mint): set language_view per Plutus version + add V2 cost model
Without language_view, pallas does not compute script_data_hash on
the tx body. Plutus txs without script_data_hash get rejected with
ConwayUtxowFailure (PPViewHashesDontMatch SNothing (SJust ...)).

Caught 2026-05-07 attempting governor bootstrap on preprod against
Agora's V2 GST policy. Previous code only set language_view when
the policy was V3 — every V2 mint hit the chain rejection.

Three changes:

1. crates/aldabra-core/src/plutus_cost_models.rs — append
   PLUTUS_V2_COST_MODEL_PREPROD constant (175 i64 entries), pulled
   live from preprod Koios epoch_params 2026-05-07. Same protocol-
   version convention as the existing V3 constant: V2 cost model
   is identical mainnet vs preprod (cost models are protocol-version
   parameters, not network), so the _PREPROD suffix is naming
   convention, not a separation point.

2. crates/aldabra-core/src/plutus_mint.rs — replace the V3-only
   language_view block with a per-PlutusVersion match. V2 wires
   the new constant; V3 keeps the existing
   params.plutus_v3_cost_model path; V1 left as TODO with a note
   (no V1 mint use case yet).

3. crates/aldabra-dao/examples/dump_governor.rs — small cargo
   example that encodes a sample GovernorDatum to CBOR hex via
   the existing aldabra_dao::agora::GovernorDatum::to_plutus_data
   path. Used during preprod DAO bringup to construct the inline
   datum for the governor bootstrap tx. Edit values + re-run for
   any DAO bringup. Builds against the existing pallas-codec
   dev-dependency.
2026-05-07 08:52:59 -07:00
Sulkta
616a6eefff feat(mcp): wallet_plutus_mint_unsigned MCP tool
Wires the new aldabra-core::plutus_mint module into MCP. Tool name
mirrors the unsigned-first DAO write convention (wallet_send_unsigned,
dao_proposal_*_unsigned, etc).

Args:
- policy_cbor_hex + policy_version (v1/v2/v3) + redeemer_cbor_hex
- mint_assets: array of {asset_name_hex, quantity}
- dest_address + dest_lovelace + dest_extra_assets + dest_inline_datum_cbor_hex
- required_input_refs: array of 'txhash#index' UTxOs that MUST be spent
- ex_units (mem + steps, optional — defaults to DEFAULT_EX_UNITS)

Returns the standard unsigned-payment shape ({cbor_hex, summary})
ready for wallet_sign_partial → wallet_submit_signed_tx.

Used for governor + stake + proposal bootstrap of any Plutus DAO.
For Agora preprod bringup, the typical call is:
- governor: required_input_refs=[gstOutRef], mint=[(GST,1)],
  dest=governor_addr, datum=GovernorDatum
- stake: mint=[(StakeST,1)], dest_extras=[(tTRP,N)], dest=stakes_addr,
  datum=StakeDatum
- proposal: spends GST input under the existing governor's spend
  redeemer + mints ProposalST under the proposal policy
2026-05-07 06:36:05 -07:00
Sulkta
11848d869b feat(plutus): plutus_mint module — Plutus-policy mint with custom output
Adds aldabra-core::plutus_mint with build_signed_plutus_mint and
build_unsigned_plutus_mint. Designed for Agora-style DAO bringup
where every "mint a single ST token under a Plutus policy and
deposit it at a script address with inline datum" tx shares the
same shape (governor / stake / proposal bootstrap).

PlutusMintArgs takes:
- required_inputs: UTxOs that MUST be spent (e.g. gstOutRef the
  GST policy is parameterized on)
- policy_cbor + policy_version + redeemer + ex_units
- mint_assets: list of (asset_name_hex, qty) under this policy
- dest_address + dest_lovelace + dest_extra_assets to forward
  + optional inline datum

Same collateral/funding pattern as plutus.rs::build_signed_plutus_spend
(smallest ADA-only ≥ 5 ADA for collateral, separate funding picks
to cover dest + fee + min_change). PlutusV3 cost-model wired into
language_view per the existing PLUTUS-4 fix.

3 unit tests cover empty-policy / empty-mint / required-input-not-
in-available rejections + the governor-bootstrap shape produces
valid Conway CBOR.

This is Phase 2 of the Track B-fast preprod DAO bringup. Together
with the wallet-ref-script Phase 1 commits (8b0e090 + a1060b1)
it gives aldabra everything needed to deploy 11 Agora script ref
UTxOs + bootstrap a governor + bootstrap stakes on preprod.
2026-05-07 06:32:59 -07:00
Sulkta
a1060b13fc chore(mcp): drop unused build_signed_payment_with_assets / build_unsigned_payment_with_assets imports
Replaced direct calls in wallet_send + wallet_send_unsigned with the
new _extras variants in the previous commit. The _with_assets re-exports
are still in aldabra-core::lib.rs for any other callers, just not
imported into tools.rs anymore.
2026-05-07 06:21:33 -07:00