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.
This commit is contained in:
Sulkta 2026-05-09 13:36:44 -07:00
parent 54a90a5f8d
commit ef38ff0e57
3 changed files with 606 additions and 12 deletions

View file

@ -264,6 +264,29 @@ impl EscrowDatum {
acc
}
/// Decode an EscrowDatum from a hex-encoded CBOR string. The
/// caller's source is typically the inline_datum field returned by
/// `chain_address_info` / Koios. Whitespace is stripped before
/// parsing.
///
/// Surfaced so MCP layers can stay free of pallas-codec /
/// pallas-primitives direct deps — they pass hex through, this
/// crate owns the decoding.
pub fn from_cbor_hex(hex_str: &str) -> DaoResult<Self> {
use pallas_codec::minicbor;
use pallas_primitives::PlutusData;
let cleaned: String = hex_str.chars().filter(|c| !c.is_whitespace()).collect();
let bytes = hex::decode(&cleaned).map_err(|e| {
DaoError::Datum(format!("EscrowDatum::from_cbor_hex hex decode: {e}"))
})?;
let pd: PlutusData = minicbor::decode(&bytes).map_err(|e| {
DaoError::Datum(format!(
"EscrowDatum::from_cbor_hex parse as PlutusData: {e}"
))
})?;
Self::from_plutus_data(&pd)
}
/// Look up a deposit entry by contributor PKH.
pub fn deposit_for(&self, pkh: &[u8; PKH_LEN]) -> Option<&EscrowDeposit> {
self.deposits.iter().find(|d| &d.contributor == pkh)