diff --git a/crates/aldabra-dao/src/builder/escrow_agree.rs b/crates/aldabra-dao/src/builder/escrow_agree.rs index 5b638b1..5d0c8c7 100644 --- a/crates/aldabra-dao/src/builder/escrow_agree.rs +++ b/crates/aldabra-dao/src/builder/escrow_agree.rs @@ -58,9 +58,7 @@ use crate::agora::escrow::{EscrowDatum, EscrowRedeemer, EscrowState, PKH_LEN}; use crate::config::{DaoConfig, DaoNetwork}; use crate::error::{DaoError, DaoResult}; -use super::escrow_deposit::{ - EscrowUtxoIn, ESCROW_OUTPUT_MIN_LOVELACE, ESCROW_SPEND_EX_UNITS, -}; +use super::escrow_deposit::{EscrowUtxoIn, ESCROW_OUTPUT_MIN_LOVELACE}; use super::proposal_create::{ parse_address, parse_script_hash, parse_tx_hash, WalletUtxo, MIN_COLLATERAL_LOVELACE, }; @@ -356,6 +354,7 @@ pub fn build_unsigned_escrow_agree(args: EscrowAgreeArgs) -> DaoResult DaoResult DaoResult Result { + let _ = fee_lovelace; // accepted for forward compat; payment builder is fee-estimating + + let party_a = decode_pkh28(&party_a_pkh_hex, "party_a_pkh_hex")?; + let party_b = decode_pkh28(&party_b_pkh_hex, "party_b_pkh_hex")?; + let recipient = decode_pkh28(&recipient_pkh_hex, "recipient_pkh_hex")?; + let initial_contributor = match initial_contributor_pkh_hex { + Some(s) => Some(decode_pkh28(&s, "initial_contributor_pkh_hex")?), + None => None, + }; + + // Pull wallet UTxOs as core::InputUtxo (escrow_open builder takes core's shape). + let raw = self + .inner + .chain + .get_utxos(&self.inner.address) + .await + .map_err(|e| format!("fetch utxos: {e}"))?; + if raw.is_empty() { + return Err(format!( + "no utxos at wallet address {} — fund the wallet first", + self.inner.address + )); + } + let wallet_utxos: Vec = raw + .into_iter() + .map(|u| InputUtxo { + tx_hash_hex: u.tx_hash, + output_index: u.output_index, + lovelace: u.lovelace, + assets: u.assets, + }) + .collect(); + + // V3 cost model + populate ProtocolParams. The escrow validator + // is V3, so the ProtocolParams must carry the V3 cost model + // for the chain to verify script_data_hash on subsequent + // Plutus-spend txs against this escrow. open itself doesn't + // run a script, but downstream tools assume the same params. + let params = ProtocolParams { + plutus_v3_cost_model: Some(PLUTUS_V3_COST_MODEL_PREPROD.to_vec()), + ..ProtocolParams::default() + }; + + let unsigned = build_unsigned_escrow_open(EscrowOpenArgs { + network: self.inner.network, + escrow_script_address: escrow_script_address.clone(), + party_a_pkh: party_a, + party_b_pkh: party_b, + recipient_pkh: recipient, + open_deadline_ms, + lock_period_ms, + initial_contributor, + initial_lovelace, + initial_assets: vec![], // ADA-only deposits in v1 + change_address: self.inner.address.clone(), + wallet_utxos, + params, + }) + .map_err(|e| format!("build_unsigned_escrow_open: {e}"))?; + + Ok(serde_json::json!({ + "tx_cbor_hex": unsigned.tx_cbor_hex, + "tx_hash_hex": unsigned.tx_hash_hex, + "escrow_script_address": unsigned.escrow_script_address, + "escrow_datum_cbor_hex": unsigned.escrow_datum_cbor_hex, + "summary": unsigned.summary, + "next_step": "review tx_cbor_hex (decode + audit), sign via wallet_sign_partial, submit via wallet_submit_signed_tx. After submission, query the script address to discover the new escrow UTxO + its tx_hash#index for follow-up deposit/agree/veto/settle/refund tools.", + "wip_warning": "⚠️ UNAUDITED escrow validator. Use preprod testnet only.", + }) + .to_string()) + } +} + +/// Decode a 28-byte payment-key-hash from hex. Shared helper for +/// escrow MCP tools. +#[cfg(feature = "escrow_wip")] +fn decode_pkh28(s: &str, field: &str) -> Result<[u8; 28], String> { + let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect(); + let bytes = hex_decode(&cleaned).map_err(|e| format!("{field} hex decode: {e}"))?; + if bytes.len() != 28 { + return Err(format!( + "{field} expects 28 bytes (56 hex chars), got {} bytes", + bytes.len() + )); + } + let mut out = [0u8; 28]; + out.copy_from_slice(&bytes); + Ok(out) } // ─── DAO arg structs ──────────────────────────────────────────────────────── @@ -3697,6 +3815,31 @@ pub struct DaoProposalVoteArgs { pub fee_lovelace: u64, } +#[cfg(feature = "escrow_wip")] +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct EscrowOpenUnsignedArgs { + /// Bech32 address of the deployed escrow validator script. + pub escrow_script_address: String, + /// party_a payment-key-hash (28 bytes hex / 56 chars). + pub party_a_pkh_hex: String, + /// party_b payment-key-hash (28 bytes hex / 56 chars). + pub party_b_pkh_hex: String, + /// recipient payment-key-hash (28 bytes hex / 56 chars). Often equals party_b. + pub recipient_pkh_hex: String, + /// POSIX-ms after which Refund-timeout becomes valid. + pub open_deadline_ms: i64, + /// Veto-window length after Agree, in ms. + pub lock_period_ms: i64, + /// Optional pkh of the initial contributor (must equal party_a or party_b). + /// If unset, opens with empty deposits — both parties top up later. + #[serde(default)] + pub initial_contributor_pkh_hex: Option, + /// Lovelace to lock at the escrow output. Must clear min-utxo floor (~1 ADA). + pub initial_lovelace: u64, + /// Estimated total fee in lovelace. ~2_500_000 reasonable for v1. + pub fee_lovelace: u64, +} + /// Mainnet Shelley genesis constants for slot↔POSIX-ms conversion. /// /// Per-network Shelley genesis constants for slot↔POSIX-ms conversion.