diff --git a/crates/aldabra-dao/src/builder/proposal_vote.rs b/crates/aldabra-dao/src/builder/proposal_vote.rs index 739e1c9..497ab2a 100644 --- a/crates/aldabra-dao/src/builder/proposal_vote.rs +++ b/crates/aldabra-dao/src/builder/proposal_vote.rs @@ -110,15 +110,23 @@ pub struct ProposalVoteArgs { pub change_address: String, /// Spendable wallet UTxOs. pub wallet_utxos: Vec, - /// Current chain tip slot. Sets `valid_from_slot(tip_slot)` and - /// `invalid_from_slot(tip_slot + VALIDITY_RANGE_SLOTS)`. + /// Current chain tip slot. Sets `valid_from_slot(tip_slot)`. pub tip_slot: u64, - /// POSIX-ms equivalent of the validity range's UPPER bound (i.e. the - /// slot `tip_slot + VALIDITY_RANGE_SLOTS` converted to ms via the - /// Shelley genesis epoch). Embedded as `Voted.posix_time` on the new - /// stake lock — must match what the validator extracts from - /// `PFullyBoundedTimeRange _ upperBound`. Caller is responsible for - /// the slot↔ms conversion. + /// Tx upper-bound slot. Sets `invalid_from_slot(validity_upper_slot)`. + /// Caller may clamp this to (e.g.) the proposal's voting_end_slot to + /// keep the validity range inside the voting window. MUST be consistent + /// with `validity_upper_ms` — both should encode the SAME slot via the + /// network's slot↔ms conversion. The chain's V2 ScriptContext computes + /// `txInfo.validRange.upperBound` from this slot, and the validator's + /// `ppermitVote` synthesizes the expected `Voted.posix_time` from + /// THAT upper bound. If the slot underlying `validity_upper_ms` differs + /// from this slot, the validator's `passert "Correct outputs"` fails. + pub validity_upper_slot: u64, + /// POSIX-ms equivalent of `validity_upper_slot`. Embedded as + /// `Voted.posix_time` on the new stake lock — must match what the + /// validator extracts from `PFullyBoundedTimeRange _ upperBound`. + /// Caller is responsible for the slot↔ms conversion AND for ensuring + /// `slot_to_posix_ms(validity_upper_slot) == validity_upper_ms`. pub validity_upper_ms: i64, /// Reference UTxO citing the stake validator script. pub stake_validator_ref: ReferenceUtxo, @@ -481,9 +489,13 @@ pub fn build_unsigned_proposal_vote( ); // Validity range — must be inside voting window (already preflighted) - // AND its width must be ≤ votingTimeRangeMaxWidth. + // AND its width must be ≤ votingTimeRangeMaxWidth. The TTL uses + // `validity_upper_slot` (which the caller may have clamped) so the + // chain's reconstructed `txInfo.validRange.upperBound` matches the + // `validity_upper_ms` we embedded in the new `Voted` lock — otherwise + // `ppermitVote`'s `passert "Correct outputs"` would crash silently. staging = staging.valid_from_slot(args.tip_slot); - staging = staging.invalid_from_slot(args.tip_slot + VALIDITY_RANGE_SLOTS); + staging = staging.invalid_from_slot(args.validity_upper_slot); // Disclosed signer: voter pkh. The validator's `pisSignedBy` checks // this against `txInfoSignatories`. @@ -644,6 +656,11 @@ mod tests { }, ], tip_slot: 180_062_536, + // Derive from validity_upper_ms via mainnet shelley_zero. + // shelley_zero = (4_492_800, 1_596_059_091_000). + // slot = 4_492_800 + (validity_upper_ms - 1_596_059_091_000) / 1000. + validity_upper_slot: 4_492_800 + + ((validity_upper_ms - 1_596_059_091_000) / 1000) as u64, validity_upper_ms, stake_validator_ref: ReferenceUtxo { tx_hash_hex: "479b8203c8b84ce20bb0a1c6ee1f527f122d1ce3e655dfc54635061eff622aea".into(), diff --git a/crates/aldabra-mcp/src/tools.rs b/crates/aldabra-mcp/src/tools.rs index 1de9609..11fdd8e 100644 --- a/crates/aldabra-mcp/src/tools.rs +++ b/crates/aldabra-mcp/src/tools.rs @@ -103,6 +103,48 @@ fn resolve_ref_script_bytes( } } +/// Resolve the Plutus minting-policy CBOR from EITHER an inline +/// hex argument OR a file path inside the container. Caller passes +/// both raw options; this fn enforces the "exactly one" rule and +/// reads the file when path is set. +/// +/// Mirrors [`resolve_ref_script_bytes`] — same workaround for the +/// 2026-05-07 MCP transport bug where hex strings >~ 4500 chars +/// get a 1-byte truncation between Claude Code and aldabra's stdio +/// reader, surfacing as "odd length" hex decode errors and blocking +/// debug-build minting policies. Reading from a file inside the +/// container bypasses the JSON-RPC arg path entirely. +fn resolve_policy_cbor_bytes( + cbor_hex: Option<&str>, + path: Option<&str>, +) -> Result, String> { + match (cbor_hex, path) { + (Some(_), Some(_)) => Err( + "set at most one of policy_cbor_hex / policy_cbor_path".into(), + ), + (Some(s), None) => { + let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect(); + hex_decode(&cleaned).map_err(|e| format!("decode policy_cbor_hex: {e}")) + } + (None, Some(p)) => { + let raw = std::fs::read_to_string(p) + .map_err(|e| format!("read policy_cbor_path '{p}': {e}"))?; + let cleaned: String = raw.chars().filter(|c| !c.is_whitespace()).collect(); + if cleaned.is_empty() { + return Err(format!( + "policy_cbor_path '{p}' contained no hex characters" + )); + } + hex_decode(&cleaned).map_err(|e| { + format!("decode policy_cbor_path '{p}' contents: {e}") + }) + } + (None, None) => { + Err("must set exactly one of policy_cbor_hex / policy_cbor_path".into()) + } + } +} + /// Parse a user-supplied script-kind string ("PlutusV1" / "PlutusV2" /// / "PlutusV3" / "Native") into the pallas `ScriptKind` enum used /// by the reference-script attachment helper. Case-insensitive, @@ -398,7 +440,22 @@ pub struct PolicyCreateArgs { pub struct PlutusMintUnsignedArgs { /// Plutus minting policy script CBOR (hex). 28-byte blake2b /// hash with the version tag becomes the policy_id. - pub policy_cbor_hex: String, + /// At most one of `policy_cbor_hex` or `policy_cbor_path` may + /// be set; exactly one must be set. + #[serde(default)] + pub policy_cbor_hex: Option, + /// Path INSIDE THE ALDABRA CONTAINER to a file containing + /// hex-encoded Plutus policy CBOR. Use INSTEAD of + /// `policy_cbor_hex` for scripts >~ 4500 chars to bypass the + /// MCP large-string transport bug (caught 2026-05-07: hex strings + /// > ~4500 chars get a 1-byte truncation + structural rearrangement + /// somewhere between Claude Code and aldabra's stdio reader, + /// surfacing as "odd length" hex decode errors). File contents + /// may include leading/trailing whitespace; only hex chars are + /// decoded. At most one of `policy_cbor_hex` or `policy_cbor_path` + /// may be set; exactly one must be set. + #[serde(default)] + pub policy_cbor_path: Option, /// Plutus version: "v1", "v2", or "v3". pub policy_version: String, /// PlutusData CBOR redeemer (hex) for the mint redeemer entry. @@ -1612,12 +1669,13 @@ impl WalletService { #[tool( name = "wallet_plutus_mint_unsigned", - description = "Build (no sign) a Plutus-policy mint with custom output. Distinct from wallet_mint (native script only). Args: policy_cbor_hex + policy_version (v1/v2/v3) + redeemer_cbor_hex + ex_units (mem+steps), mint_assets (array of {asset_name_hex, quantity}), dest_address + dest_lovelace + dest_extra_assets + dest_inline_datum_cbor_hex (req for script addrs), required_input_refs (array of 'txhash#index' UTxOs that MUST be spent — e.g. gstOutRef for parameterized policies). Returns JSON {cbor_hex, summary}. Pass through wallet_sign_partial → wallet_submit_signed_tx. Designed for Agora-style DAO bringup: governor bootstrap, stake bootstrap, proposal create." + description = "Build (no sign) a Plutus-policy mint with custom output. Distinct from wallet_mint (native script only). Args: policy_cbor_hex OR policy_cbor_path (use path for >~4500-char scripts to bypass the MCP large-string transport bug) + policy_version (v1/v2/v3) + redeemer_cbor_hex + ex_units (mem+steps), mint_assets (array of {asset_name_hex, quantity}), dest_address + dest_lovelace + dest_extra_assets + dest_inline_datum_cbor_hex (req for script addrs), required_input_refs (array of 'txhash#index' UTxOs that MUST be spent — e.g. gstOutRef for parameterized policies). Returns JSON {cbor_hex, summary}. Pass through wallet_sign_partial → wallet_submit_signed_tx. Designed for Agora-style DAO bringup: governor bootstrap, stake bootstrap, proposal create." )] async fn wallet_plutus_mint_unsigned( &self, #[tool(aggr)] PlutusMintUnsignedArgs { policy_cbor_hex, + policy_cbor_path, policy_version, redeemer_cbor_hex, mint_assets, @@ -1639,8 +1697,10 @@ impl WalletService { )); } - let policy_cbor = - hex_decode(&policy_cbor_hex).map_err(|e| format!("decode policy_cbor: {e}"))?; + let policy_cbor = resolve_policy_cbor_bytes( + policy_cbor_hex.as_deref(), + policy_cbor_path.as_deref(), + )?; let redeemer_cbor = hex_decode(&redeemer_cbor_hex).map_err(|e| format!("decode redeemer: {e}"))?; let policy_ver = match policy_version.trim().to_ascii_lowercase().as_str() { @@ -3214,6 +3274,7 @@ impl WalletService { change_address: self.inner.address.clone(), wallet_utxos, tip_slot, + validity_upper_slot, validity_upper_ms, stake_validator_ref, proposal_validator_ref,