diff --git a/crates/aldabra-dao/src/builder/escrow_deposit.rs b/crates/aldabra-dao/src/builder/escrow_deposit.rs new file mode 100644 index 0000000..c9e3aa1 --- /dev/null +++ b/crates/aldabra-dao/src/builder/escrow_deposit.rs @@ -0,0 +1,624 @@ +//! Build an unsigned `escrow_deposit_unsigned` transaction. +//! +//! ⚠️ WIP / UNAUDITED. Feature-gated behind `escrow_wip`. +//! +//! ## What this tx does +//! +//! Plutus V3 spend of an existing escrow UTxO with a continuing-output +//! state transition (only `deposits` field mutated). Validator runs +//! `Deposit { contributor }` redeemer. +//! +//! - **Inputs**: +//! - The escrow UTxO at `escrow_script_address` (Plutus V3 spend, +//! redeemer = `Deposit { contributor }`). +//! - One funding wallet UTxO covering `add_lovelace + fee + change_min`. +//! - **Collateral**: 1 ADA-only ≥5 ADA wallet UTxO (separate from funding). +//! - **Outputs**: +//! - Continuing escrow UTxO at the same script address. Inline datum = +//! old datum with `deposits` mutated; lovelace = old + add_lovelace; +//! pre-existing native assets preserved bit-exact. +//! - Wallet change. +//! - **Disclosed signer**: `contributor_pkh` (validator's `signed_by` check). +//! +//! ## v1 limitation: ADA-only deposits +//! +//! The validator enforces canonicality via: +//! +//! ```text +//! cbor.serialise(expected_deposits_after) == cbor.serialise(new_d.deposits) +//! ``` +//! +//! where `expected_deposits_after` is computed in Aiken via right-fold +//! over `flat_merge`. Right-fold + multi-entry `net_added` produces +//! REVERSED appended-policy ordering. Mirroring that exactly off-chain +//! is doable but a v2 enhancement. +//! +//! For v1 we restrict each Deposit tx to lovelace-only (`net_added` is +//! a single-entry `EscrowValue` — `[(empty_policy, [(empty_name, qty)])]`). +//! Single-entry foldr / forward-iter produce identical results, so +//! Rust's `value_merge` matches Aiken's `flat_merge` byte-for-byte +//! regardless of iteration direction. +//! +//! Multi-asset deposit shapes (token + ADA, multiple distinct tokens, +//! etc.) are deferred until we mirror the foldr ordering exactly AND +//! verify byte-equality via golden CBOR captured from the on-chain +//! validator. +//! +//! ## What the validator enforces (must match) +//! +//! From `aiken-escrow/validators/escrow.ak` Deposit branch: +//! +//! 1. `d.state == Open` (escrow not yet agreed/finalised). +//! 2. `contributor == d.party_a || contributor == d.party_b`. +//! 3. `signed_by(self, contributor)`. +//! 4. Continuing output exists at `script_addr` with parsable inline datum. +//! 5. New datum equals old datum in every field except `deposits` +//! (party_a/b/recipient/open_deadline_ms/lock_period_ms/state preserved). +//! 6. New datum's `deposits` matches `expected_deposits_after(d.deposits, +//! contributor, value_to_flat(new_value - in_value))` byte-for-byte +//! via `cbor.serialise`. +//! +//! All six are preflighted client-side here so a misshaped tx never +//! reaches the chain (and thus never burns collateral). + +use pallas_codec::minicbor; +use pallas_crypto::hash::Hash; +use pallas_txbuilder::{BuildConway, ExUnits, Input, Output, ScriptKind, StagingTransaction}; + +use crate::agora::escrow::{ + value_merge, EscrowDatum, EscrowDeposit, EscrowRedeemer, EscrowState, EscrowValue, PKH_LEN, +}; +use crate::config::{DaoConfig, DaoNetwork}; +use crate::error::{DaoError, DaoResult}; + +use super::proposal_create::{ + parse_address, parse_script_hash, parse_tx_hash, WalletUtxo, MIN_COLLATERAL_LOVELACE, +}; + +/// Generous-but-bounded ExUnits budget for the escrow Deposit redeemer. +/// Refine via Koios `tx_evaluate` once we have a live tx to evaluate. +pub const ESCROW_SPEND_EX_UNITS: ExUnits = ExUnits { + mem: 5_000_000, + steps: 2_000_000_000, +}; + +/// Conway-era min UTxO floor we apply to the continuing escrow output. +/// Real value depends on serialized output size; this is a generous +/// bound that covers the escrow datum + token shapes we expect. +pub const ESCROW_OUTPUT_MIN_LOVELACE: u64 = 2_000_000; + +/// Wallet-change min-UTxO floor. +const WALLET_CHANGE_MIN_LOVELACE: u64 = 1_000_000; + +/// On-chain escrow state being spent. +#[derive(Debug, Clone)] +pub struct EscrowUtxoIn { + pub tx_hash_hex: String, + pub output_index: u32, + pub lovelace: u64, + /// Native assets currently locked alongside the lovelace. Preserved + /// bit-exact onto the continuing output (Deposit doesn't move tokens + /// in v1 — only lovelace). Each tuple: `(policy_hex, name_hex, qty)`. + pub assets: Vec<(String, String, u64)>, + /// Current EscrowDatum decoded from the inline datum. Caller fetches + /// via `escrow_show` / Koios. + pub datum: EscrowDatum, +} + +/// Args bundle for [`build_unsigned_escrow_deposit`]. +#[derive(Debug, Clone)] +pub struct EscrowDepositArgs { + pub cfg: DaoConfig, + /// Bech32 address of the deployed escrow validator script. Continuing + /// output goes here. Must match `escrow_in`'s on-chain address. + pub escrow_script_address: String, + /// Compiled Plutus V3 UPLC bytecode of the escrow validator. Inlined + /// in the tx witness for v1; reference-script optimization is a v2 + /// follow-up. + pub validator_script_cbor: Vec, + pub escrow_in: EscrowUtxoIn, + /// Depositing party's payment-credential hash. Must equal + /// `escrow_in.datum.party_a` or `escrow_in.datum.party_b`. + pub contributor_pkh: [u8; PKH_LEN], + /// Lovelace being added to the escrow on this tx. v1 restriction: + /// ADA-only deposits. Must be > 0. Output min-utxo floor still + /// applies after the merge. + pub add_lovelace: u64, + /// Caller's bech32 base address (for change). + pub change_address: String, + /// Caller's spendable wallet UTxOs (must include 1 ADA-only utxo + /// ≥5 ADA for collateral and 1 ADA-only funding utxo). + pub wallet_utxos: Vec, + /// Chain tip slot (sets `valid_from_slot`). + pub tip_slot: u64, + /// Tx upper-bound slot (sets `invalid_from_slot`). Validator does NOT + /// enforce time bounds on Deposit — caller can set freely (commonly + /// `tip_slot + 1800` for a 30-minute window). + pub validity_upper_slot: u64, + /// Estimated total fee. Caller-supplied for v1; refine via real + /// evaluator + size measurement later. + pub fee_lovelace: u64, + /// ExUnits budget for the spend redeemer. Defaults to + /// [`ESCROW_SPEND_EX_UNITS`]. + pub ex_units: ExUnits, +} + +/// What [`build_unsigned_escrow_deposit`] returns. +#[derive(Debug, Clone)] +pub struct UnsignedEscrowDeposit { + /// Hex-encoded CBOR of the unsigned tx body. + pub tx_cbor_hex: String, + /// Blake2b-256 hash of the tx body. + pub tx_hash_hex: String, + /// The script address where the continuing escrow lives. + pub escrow_script_address: String, + /// Hex-encoded CBOR of the new (post-deposit) escrow datum. + pub new_datum_cbor_hex: String, + /// Lovelace locked at the continuing output (= old + add_lovelace). + pub new_escrow_lovelace: u64, + /// Human-readable summary for MCP wrappers. + pub summary: String, +} + +/// Build the unsigned escrow_deposit tx. +pub fn build_unsigned_escrow_deposit(args: EscrowDepositArgs) -> DaoResult { + let datum_in = &args.escrow_in.datum; + + // ---- preflight ---------------------------------------------------------- + // + // Rules numbered against the validator's Deposit branch (see module docstring). + + // (1) state must be Open. + if datum_in.state != EscrowState::Open { + return Err(DaoError::State(format!( + "escrow state is {:?}, must be Open to accept deposits", + datum_in.state + ))); + } + + // (2) contributor must be a or b. + if args.contributor_pkh != datum_in.party_a && args.contributor_pkh != datum_in.party_b { + return Err(DaoError::State( + "contributor_pkh is neither party_a nor party_b — only escrow parties can deposit" + .into(), + )); + } + + // v1 ADA-only restriction: add_lovelace > 0, no native assets being added. + if args.add_lovelace == 0 { + return Err(DaoError::State( + "add_lovelace must be > 0 — Deposit tx with zero net value would still pay fees" + .into(), + )); + } + + // The continuing output's lovelace must clear the floor. + let new_escrow_lovelace = args + .escrow_in + .lovelace + .checked_add(args.add_lovelace) + .ok_or_else(|| DaoError::State("escrow output lovelace overflow".into()))?; + if new_escrow_lovelace < ESCROW_OUTPUT_MIN_LOVELACE { + return Err(DaoError::State(format!( + "continuing escrow output lovelace {new_escrow_lovelace} < min {ESCROW_OUTPUT_MIN_LOVELACE}" + ))); + } + + // ---- compute new datum -------------------------------------------------- + // + // For v1 ADA-only deposits, single-entry `net_added` makes Rust + // `value_merge` byte-equivalent to Aiken `flat_merge` regardless of + // fold direction (only one b_entry to process). + + let net_added = EscrowValue::ada(args.add_lovelace); + let mut new_deposits = datum_in.deposits.clone(); + let pos = new_deposits + .iter() + .position(|d| d.contributor == args.contributor_pkh); + match pos { + Some(i) => { + new_deposits[i].value = value_merge(&new_deposits[i].value, &net_added); + } + None => { + new_deposits.push(EscrowDeposit { + contributor: args.contributor_pkh, + value: net_added, + }); + } + } + + let new_datum = EscrowDatum { + party_a: datum_in.party_a, + party_b: datum_in.party_b, + recipient: datum_in.recipient, + open_deadline_ms: datum_in.open_deadline_ms, + lock_period_ms: datum_in.lock_period_ms, + state: EscrowState::Open, + deposits: new_deposits, + }; + let new_datum_pd = new_datum.to_plutus_data()?; + let new_datum_cbor = minicbor::to_vec(&new_datum_pd) + .map_err(|e| DaoError::Cbor(format!("new escrow datum encode: {e}")))?; + + // ---- redeemer ----------------------------------------------------------- + + let redeemer_pd = EscrowRedeemer::Deposit { + contributor: args.contributor_pkh, + } + .to_plutus_data()?; + let redeemer_cbor = minicbor::to_vec(&redeemer_pd) + .map_err(|e| DaoError::Cbor(format!("deposit redeemer encode: {e}")))?; + + // ---- pick funding + collateral ----------------------------------------- + + let mut ada_only: Vec = args + .wallet_utxos + .iter() + .filter(|u| u.is_ada_only()) + .cloned() + .collect(); + ada_only.sort_by_key(|u| std::cmp::Reverse(u.lovelace)); + + let collateral = ada_only + .iter() + .rev() + .find(|u| u.lovelace >= MIN_COLLATERAL_LOVELACE) + .ok_or_else(|| { + DaoError::State(format!( + "no ada-only wallet UTxO ≥ {MIN_COLLATERAL_LOVELACE} lovelace for collateral" + )) + })? + .clone(); + + let funding = ada_only + .iter() + .find(|u| { + !(u.tx_hash_hex == collateral.tx_hash_hex && u.output_index == collateral.output_index) + }) + .cloned() + .ok_or_else(|| { + DaoError::State( + "need a SECOND ada-only wallet UTxO to fund the deposit (separate from collateral)" + .into(), + ) + })?; + + // ---- balance + change --------------------------------------------------- + // + // total_in = escrow_in.lovelace + funding.lovelace + // outputs = new_escrow_lovelace + change + fee + // change = total_in - new_escrow_lovelace - fee + + let total_in = args + .escrow_in + .lovelace + .checked_add(funding.lovelace) + .ok_or_else(|| DaoError::State("input lovelace overflow".into()))?; + let total_out = new_escrow_lovelace + .checked_add(args.fee_lovelace) + .ok_or_else(|| DaoError::State("output lovelace overflow".into()))?; + let change_lovelace = total_in.checked_sub(total_out).ok_or_else(|| { + DaoError::State(format!( + "insufficient input: total_in={total_in} need={total_out} \ + (escrow_out={new_escrow_lovelace} + fee={})", + args.fee_lovelace + )) + })?; + if change_lovelace > 0 && change_lovelace < WALLET_CHANGE_MIN_LOVELACE { + return Err(DaoError::State(format!( + "change lovelace {change_lovelace} below min UTxO ({WALLET_CHANGE_MIN_LOVELACE}); top up wallet" + ))); + } + + // ---- assemble pallas StagingTransaction -------------------------------- + + let escrow_addr = parse_address(&args.escrow_script_address)?; + let change_addr = parse_address(&args.change_address)?; + + let escrow_input = Input::new( + parse_tx_hash(&args.escrow_in.tx_hash_hex)?, + args.escrow_in.output_index as u64, + ); + let funding_input = Input::new( + parse_tx_hash(&funding.tx_hash_hex)?, + funding.output_index as u64, + ); + let collateral_input = Input::new( + parse_tx_hash(&collateral.tx_hash_hex)?, + collateral.output_index as u64, + ); + + let network_id = match args.cfg.network { + DaoNetwork::Mainnet => 1u8, + DaoNetwork::Preprod | DaoNetwork::Preview => 0u8, + }; + + // Continuing escrow output: same address, new lovelace, preserve any + // pre-existing native assets bit-exact, new inline datum. + let mut new_escrow_output = Output::new(escrow_addr, new_escrow_lovelace) + .set_inline_datum(new_datum_cbor.clone()); + for (policy_hex, name_hex, qty) in &args.escrow_in.assets { + let policy = parse_script_hash(policy_hex)?; + let name = hex::decode(name_hex) + .map_err(|e| DaoError::Config(format!("escrow_in asset name hex: {e}")))?; + new_escrow_output = new_escrow_output + .add_asset(policy, name, *qty) + .map_err(|e| DaoError::Backend(format!("preserve escrow asset: {e}")))?; + } + + let mut staging = StagingTransaction::new(); + // Two regular inputs: escrow (script) + funding (wallet). + staging = staging.input(escrow_input.clone()); + staging = staging.input(funding_input); + staging = staging.collateral_input(collateral_input); + // Continuing escrow output, then change. + staging = staging.output(new_escrow_output); + if change_lovelace > 0 { + let mut change_output = Output::new(change_addr, change_lovelace); + for (policy_hex, name_hex, qty) in &funding.assets { + let policy = parse_script_hash(policy_hex)?; + let name = hex::decode(name_hex) + .map_err(|e| DaoError::Config(format!("funding asset name hex: {e}")))?; + change_output = change_output + .add_asset(policy, name, *qty) + .map_err(|e| DaoError::Backend(format!("add asset to change: {e}")))?; + } + staging = staging.output(change_output); + } + + // Inline the V3 validator script (v1 — no ref-script). + staging = staging.script(ScriptKind::PlutusV3, args.validator_script_cbor); + staging = staging.add_spend_redeemer(escrow_input, redeemer_cbor, Some(args.ex_units)); + + staging = staging.fee(args.fee_lovelace).network_id(network_id); + + // Validity range — Deposit doesn't enforce time bounds, but we set + // them so the tx isn't valid forever. + staging = staging.valid_from_slot(args.tip_slot); + staging = staging.invalid_from_slot(args.validity_upper_slot); + + // Disclosed signer: contributor pkh (validator's `signed_by` check). + staging = staging.disclosed_signer(Hash::<28>::from(args.contributor_pkh)); + + // V3 cost model — required for script_data_hash. Without it the + // chain rejects with PPViewHashesDontMatch. + staging = staging.language_view( + ScriptKind::PlutusV3, + aldabra_core::plutus_cost_models::PLUTUS_V3_COST_MODEL_PREPROD.to_vec(), + ); + + let built = staging + .build_conway_raw() + .map_err(|e| DaoError::Backend(format!("build_conway_raw: {e}")))?; + + let tx_cbor_hex = hex::encode(&built.tx_bytes.0); + let tx_hash_hex = hex::encode(built.tx_hash.0); + + let summary = format!( + "escrow_deposit_unsigned: contributor={} +{} lovelace → {} lovelace at {} (fee={})", + hex::encode(args.contributor_pkh), + args.add_lovelace, + new_escrow_lovelace, + args.escrow_script_address, + args.fee_lovelace, + ); + + Ok(UnsignedEscrowDeposit { + tx_cbor_hex, + tx_hash_hex, + escrow_script_address: args.escrow_script_address, + new_datum_cbor_hex: hex::encode(&new_datum_cbor), + new_escrow_lovelace, + summary, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::ScriptRefs; + + fn pkh(seed: u8) -> [u8; PKH_LEN] { + [seed; PKH_LEN] + } + + fn sample_cfg() -> DaoConfig { + DaoConfig { + name: "sulkta-escrow".into(), + description: None, + governor_addr: "addr_test1wpyt48l".repeat(3), + stakes_addr: "addr_test1wpyt48l".repeat(3), + treasury_addr: "addr_test1wpyt48l".repeat(3), + gov_token_policy: "00".repeat(28), + gov_token_name_hex: "".into(), + initial_spend: format!("{}#0", "00".repeat(32)), + max_cosigners: 5, + treasury_ref_config: "00".repeat(28), + network: DaoNetwork::Preprod, + proposal_addr: None, + stake_st_policy: None, + proposal_st_policy: None, + script_refs: ScriptRefs::default(), + } + } + + /// Stub script CBOR — V3 always-true (`\46\01\00\00\32\22`). Real builds + /// pass the compiled escrow validator UPLC. + fn stub_v3_script() -> Vec { + vec![0x46, 0x01, 0x00, 0x00, 0x32, 0x22] + } + + fn sample_args( + state: EscrowState, + deposits: Vec, + contributor: [u8; PKH_LEN], + ) -> EscrowDepositArgs { + let escrow_in = EscrowUtxoIn { + tx_hash_hex: "11".repeat(32), + output_index: 0, + lovelace: 5_000_000, + assets: vec![], + datum: EscrowDatum { + party_a: pkh(0xa1), + party_b: pkh(0xb2), + recipient: pkh(0xb2), + open_deadline_ms: 1_700_000_000_000, + lock_period_ms: 30 * 60 * 1000, + state, + deposits, + }, + }; + EscrowDepositArgs { + cfg: sample_cfg(), + escrow_script_address: + "addr_test1wqlzsnytzs4qv0trmhvw5cuyxnxk0qjq68crn85jj4lhv7qn4wym4".into(), + validator_script_cbor: stub_v3_script(), + escrow_in, + contributor_pkh: contributor, + add_lovelace: 5_000_000, + change_address: + "addr_test1qqqt0pru382hy9vjlsxv3ye02z50sfvt8xunscg5pgden77z73dpdfng2ctw2ekqplqgrljelz7h4dneac27nn3qx3rqqpavzj" + .into(), + wallet_utxos: vec![ + WalletUtxo { + tx_hash_hex: "22".repeat(32), + output_index: 0, + lovelace: 20_000_000, + assets: vec![], + }, + WalletUtxo { + tx_hash_hex: "33".repeat(32), + output_index: 0, + lovelace: 8_000_000, + assets: vec![], + }, + ], + tip_slot: 50_000_000, + validity_upper_slot: 50_001_800, + fee_lovelace: 2_500_000, + ex_units: ESCROW_SPEND_EX_UNITS, + } + } + + #[test] + fn rejects_non_open_state() { + let args = sample_args( + EscrowState::Agreed { agreed_at_ms: 0 }, + vec![], + pkh(0xa1), + ); + let err = build_unsigned_escrow_deposit(args).unwrap_err(); + assert!(err.to_string().contains("must be Open")); + } + + #[test] + fn rejects_outsider_contributor() { + let args = sample_args(EscrowState::Open, vec![], pkh(0xff)); + let err = build_unsigned_escrow_deposit(args).unwrap_err(); + assert!(err.to_string().contains("party_a nor party_b")); + } + + #[test] + fn rejects_zero_add_lovelace() { + let mut args = sample_args(EscrowState::Open, vec![], pkh(0xa1)); + args.add_lovelace = 0; + let err = build_unsigned_escrow_deposit(args).unwrap_err(); + assert!(err.to_string().contains("add_lovelace")); + } + + #[test] + fn rejects_when_no_collateral_utxo() { + let mut args = sample_args(EscrowState::Open, vec![], pkh(0xa1)); + args.wallet_utxos = vec![WalletUtxo { + tx_hash_hex: "44".repeat(32), + output_index: 0, + lovelace: 1_000_000, // way under collateral floor + assets: vec![], + }]; + let err = build_unsigned_escrow_deposit(args).unwrap_err(); + assert!(err.to_string().contains("collateral")); + } + + #[test] + fn rejects_when_only_one_ada_utxo() { + let mut args = sample_args(EscrowState::Open, vec![], pkh(0xa1)); + args.wallet_utxos = vec![WalletUtxo { + tx_hash_hex: "55".repeat(32), + output_index: 0, + lovelace: 50_000_000, // qualifies for collateral but no funding + assets: vec![], + }]; + let err = build_unsigned_escrow_deposit(args).unwrap_err(); + assert!(err.to_string().contains("SECOND ada-only")); + } + + #[test] + fn first_deposit_appends_new_entry() { + // Empty deposits → new entry created. + let args = sample_args(EscrowState::Open, vec![], pkh(0xa1)); + let unsigned = build_unsigned_escrow_deposit(args).unwrap(); + assert!(unsigned.summary.contains("contributor=")); + assert_eq!(unsigned.new_escrow_lovelace, 10_000_000); + } + + #[test] + fn second_deposit_merges_into_existing_entry() { + // Existing deposit by party_a — second deposit must merge in place. + let existing = vec![EscrowDeposit { + contributor: pkh(0xa1), + value: EscrowValue::ada(3_000_000), + }]; + let args = sample_args(EscrowState::Open, existing, pkh(0xa1)); + let unsigned = build_unsigned_escrow_deposit(args).unwrap(); + // Decode new datum and check + let pd: pallas_primitives::PlutusData = + minicbor::decode(&hex::decode(&unsigned.new_datum_cbor_hex).unwrap()).unwrap(); + let new_datum = EscrowDatum::from_plutus_data(&pd).unwrap(); + assert_eq!(new_datum.deposits.len(), 1); + // 3 ADA existing + 5 ADA added = 8 ADA + assert_eq!( + new_datum.deposits[0].value.lovelace(), + 8_000_000, + "value_merge should sum lovelace in place" + ); + assert_eq!(new_datum.deposits[0].contributor, pkh(0xa1)); + } + + #[test] + fn second_party_deposit_appends_new_entry() { + // party_a already deposited; party_b deposits next. + let existing = vec![EscrowDeposit { + contributor: pkh(0xa1), + value: EscrowValue::ada(5_000_000), + }]; + let args = sample_args(EscrowState::Open, existing, pkh(0xb2)); + let unsigned = build_unsigned_escrow_deposit(args).unwrap(); + let pd: pallas_primitives::PlutusData = + minicbor::decode(&hex::decode(&unsigned.new_datum_cbor_hex).unwrap()).unwrap(); + let new_datum = EscrowDatum::from_plutus_data(&pd).unwrap(); + assert_eq!(new_datum.deposits.len(), 2); + assert_eq!(new_datum.deposits[0].contributor, pkh(0xa1)); + assert_eq!(new_datum.deposits[0].value.lovelace(), 5_000_000); + assert_eq!(new_datum.deposits[1].contributor, pkh(0xb2)); + assert_eq!(new_datum.deposits[1].value.lovelace(), 5_000_000); + } + + #[test] + fn datum_immutable_fields_preserved() { + // Ensure party_a/b/recipient/deadline/lock_period are bit-exact + // on the new datum. Validator's "datum equal except deposits" + // check fails otherwise. + let args = sample_args(EscrowState::Open, vec![], pkh(0xa1)); + let original = args.escrow_in.datum.clone(); + let unsigned = build_unsigned_escrow_deposit(args).unwrap(); + let pd: pallas_primitives::PlutusData = + minicbor::decode(&hex::decode(&unsigned.new_datum_cbor_hex).unwrap()).unwrap(); + let new_datum = EscrowDatum::from_plutus_data(&pd).unwrap(); + assert_eq!(new_datum.party_a, original.party_a); + assert_eq!(new_datum.party_b, original.party_b); + assert_eq!(new_datum.recipient, original.recipient); + assert_eq!(new_datum.open_deadline_ms, original.open_deadline_ms); + assert_eq!(new_datum.lock_period_ms, original.lock_period_ms); + assert_eq!(new_datum.state, EscrowState::Open); + } +} diff --git a/crates/aldabra-dao/src/builder/mod.rs b/crates/aldabra-dao/src/builder/mod.rs index b7edcdd..c86a5b5 100644 --- a/crates/aldabra-dao/src/builder/mod.rs +++ b/crates/aldabra-dao/src/builder/mod.rs @@ -24,5 +24,7 @@ pub mod proposal_retract_votes; pub mod proposal_vote; pub mod stake_destroy; +#[cfg(feature = "escrow_wip")] +pub mod escrow_deposit; #[cfg(feature = "escrow_wip")] pub mod escrow_open;