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.
This commit is contained in:
parent
705acdac0c
commit
dba3d46271
6 changed files with 158 additions and 6 deletions
|
|
@ -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<UnsignedE
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::super::escrow_deposit::ESCROW_SPEND_EX_UNITS;
|
||||
use crate::agora::escrow::{EscrowDeposit, EscrowValue};
|
||||
use crate::config::ScriptRefs;
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ use crate::agora::escrow::{EscrowRedeemer, EscrowState, PKH_LEN};
|
|||
use crate::config::{DaoConfig, DaoNetwork};
|
||||
use crate::error::{DaoError, DaoResult};
|
||||
|
||||
use super::escrow_deposit::{EscrowUtxoIn, ESCROW_SPEND_EX_UNITS};
|
||||
use super::escrow_deposit::EscrowUtxoIn;
|
||||
use super::escrow_veto::enterprise_address_for;
|
||||
use super::proposal_create::{
|
||||
parse_address, parse_script_hash, parse_tx_hash, WalletUtxo, MIN_COLLATERAL_LOVELACE,
|
||||
|
|
@ -320,6 +320,7 @@ pub fn build_unsigned_escrow_refund_timeout(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::super::escrow_deposit::ESCROW_SPEND_EX_UNITS;
|
||||
use crate::agora::escrow::{EscrowDatum, EscrowDeposit, EscrowValue};
|
||||
use crate::config::ScriptRefs;
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ use crate::agora::escrow::{EscrowRedeemer, EscrowState, PKH_LEN};
|
|||
use crate::config::{DaoConfig, DaoNetwork};
|
||||
use crate::error::{DaoError, DaoResult};
|
||||
|
||||
use super::escrow_deposit::{EscrowUtxoIn, ESCROW_SPEND_EX_UNITS};
|
||||
use super::escrow_deposit::EscrowUtxoIn;
|
||||
use super::escrow_veto::enterprise_address_for;
|
||||
use super::proposal_create::{
|
||||
parse_address, parse_script_hash, parse_tx_hash, WalletUtxo, MIN_COLLATERAL_LOVELACE,
|
||||
|
|
@ -293,6 +293,7 @@ pub fn build_unsigned_escrow_settle(args: EscrowSettleArgs) -> DaoResult<Unsigne
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::super::escrow_deposit::ESCROW_SPEND_EX_UNITS;
|
||||
use crate::agora::escrow::{EscrowDatum, EscrowDeposit, EscrowValue};
|
||||
use crate::config::ScriptRefs;
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ use crate::agora::escrow::{EscrowRedeemer, EscrowState, PKH_LEN};
|
|||
use crate::config::{DaoConfig, DaoNetwork};
|
||||
use crate::error::{DaoError, DaoResult};
|
||||
|
||||
use super::escrow_deposit::{EscrowUtxoIn, ESCROW_SPEND_EX_UNITS};
|
||||
use super::escrow_deposit::EscrowUtxoIn;
|
||||
use super::proposal_create::{
|
||||
parse_address, parse_script_hash, parse_tx_hash, WalletUtxo, MIN_COLLATERAL_LOVELACE,
|
||||
};
|
||||
|
|
@ -362,6 +362,7 @@ pub fn build_unsigned_escrow_veto(args: EscrowVetoArgs) -> DaoResult<UnsignedEsc
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::super::escrow_deposit::ESCROW_SPEND_EX_UNITS;
|
||||
use crate::agora::escrow::{EscrowDatum, EscrowDeposit, EscrowValue};
|
||||
use crate::config::ScriptRefs;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,13 @@ authors.workspace = true
|
|||
name = "aldabra"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Enable the escrow_wip feature surface (Aiken V3 escrow validator + 6 builders
|
||||
# + MCP tools). Mirrors aldabra-dao's escrow_wip — propagates the feature so
|
||||
# the MCP binary actually exposes escrow_* tools. WIP / UNAUDITED — preprod only.
|
||||
escrow_wip = ["aldabra-dao/escrow_wip"]
|
||||
|
||||
[dependencies]
|
||||
aldabra-core = { path = "../aldabra-core" }
|
||||
aldabra-chain = { path = "../aldabra-chain" }
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ use aldabra_dao::builder::proposal_vote::{
|
|||
build_unsigned_proposal_vote, ProposalUtxoIn, ProposalVoteArgs,
|
||||
};
|
||||
use aldabra_dao::builder::stake_destroy::{build_unsigned_stake_destroy, StakeDestroyArgs};
|
||||
#[cfg(feature = "escrow_wip")]
|
||||
use aldabra_dao::builder::escrow_open::{
|
||||
build_unsigned_escrow_open, EscrowOpenArgs,
|
||||
};
|
||||
use aldabra_dao::config::{DaoConfig, DaoNetwork, DaoStore, ScriptRefs};
|
||||
use aldabra_dao::discovery::{
|
||||
apply_discovery, discover_scripts, KoiosDiscoveryClient, MAINNET_AGORA_SHARED_DEPLOYER,
|
||||
|
|
@ -3542,6 +3546,120 @@ impl WalletService {
|
|||
})
|
||||
.to_string())
|
||||
}
|
||||
|
||||
// ─── escrow_wip — WIP/UNAUDITED two-party agreement-with-veto escrow ───
|
||||
//
|
||||
// Feature-gated (`--features escrow_wip`) so the default release build
|
||||
// doesn't surface tools whose validator hasn't been externally audited.
|
||||
// Enable for preprod E2E only.
|
||||
|
||||
#[cfg(feature = "escrow_wip")]
|
||||
#[tool(
|
||||
name = "escrow_open_unsigned",
|
||||
description = "WIP — UNAUDITED: Build (but DO NOT submit) an unsigned escrow_open tx. Locks lovelace at the escrow validator script address with an inline EscrowDatum (state=Open, optional initial deposit). Two-party agreement-with-veto: party_a + party_b must both sign Agree to flip the state; either can fire Veto from Agreed; recipient is settled-to after the lock period. ⚠️ v1 WIP feature — the validator has NOT been externally audited. Do not route mainnet value through it. Args: escrow_script_address (bech32), party_a/b/recipient_pkh_hex (28-byte hex), open_deadline_ms (after which Refund-timeout becomes valid), lock_period_ms (veto window after Agree), initial_contributor_pkh_hex (optional — party_a or party_b — funds initial deposit), initial_lovelace (lovelace to lock at the script), fee_lovelace (~2.5 ADA reasonable). Returns CBOR-hex of the unsigned tx body. Caller signs via wallet_sign_partial then submits via wallet_submit_signed_tx."
|
||||
)]
|
||||
async fn escrow_open_unsigned(
|
||||
&self,
|
||||
#[tool(aggr)] EscrowOpenUnsignedArgs {
|
||||
escrow_script_address,
|
||||
party_a_pkh_hex,
|
||||
party_b_pkh_hex,
|
||||
recipient_pkh_hex,
|
||||
open_deadline_ms,
|
||||
lock_period_ms,
|
||||
initial_contributor_pkh_hex,
|
||||
initial_lovelace,
|
||||
fee_lovelace,
|
||||
}: EscrowOpenUnsignedArgs,
|
||||
) -> Result<String, String> {
|
||||
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<InputUtxo> = 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<String>,
|
||||
/// 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue