diff --git a/crates/aldabra-core/src/lib.rs b/crates/aldabra-core/src/lib.rs index 866504a..2069963 100644 --- a/crates/aldabra-core/src/lib.rs +++ b/crates/aldabra-core/src/lib.rs @@ -69,10 +69,12 @@ pub use governance::{ DRepTarget, VoteChoice, DREP_REGISTRATION_DEPOSIT_LOVELACE, }; pub use tx::{ - build_signed_payment, build_signed_payment_with_assets, build_unsigned_payment, - build_unsigned_payment_with_assets, hex_decode, AssetSpec, InputUtxo, PaymentSummary, - ProtocolParams, UnsignedPayment, + build_signed_payment, build_signed_payment_extras, build_signed_payment_with_assets, + build_unsigned_payment, build_unsigned_payment_extras, build_unsigned_payment_with_assets, + hex_decode, AssetSpec, InputUtxo, PaymentSummary, ProtocolParams, ReferenceScriptSpec, + UnsignedPayment, }; +pub use pallas_txbuilder::ScriptKind; #[derive(Debug, Error)] pub enum WalletError { diff --git a/crates/aldabra-core/src/tx.rs b/crates/aldabra-core/src/tx.rs index a078ef8..e06a3c1 100644 --- a/crates/aldabra-core/src/tx.rs +++ b/crates/aldabra-core/src/tx.rs @@ -45,7 +45,19 @@ use ed25519_bip32::XPrv; use pallas_addresses::Address as PallasAddress; use pallas_crypto::key::ed25519::SecretKeyExtended; -use pallas_txbuilder::{BuildConway, BuiltTransaction, Input, Output, StagingTransaction}; +use pallas_txbuilder::{BuildConway, BuiltTransaction, Input, Output, ScriptKind, StagingTransaction}; + +/// Reference-script attached to a tx output. Used to deploy Plutus +/// validators / minting policies as reusable on-chain references so +/// downstream txs can spend from / mint under those scripts via +/// `--tx-in-script-file ref` semantics instead of inline-witnessing +/// the entire CBOR every time. Each ref-script carries its language +/// (PlutusV1/V2/V3 — Native is also valid but rare). +#[derive(Debug, Clone, Copy)] +pub struct ReferenceScriptSpec<'a> { + pub kind: ScriptKind, + pub cbor: &'a [u8], +} use pallas_wallet::PrivateKey; use serde::{Deserialize, Serialize}; use zeroize::Zeroize; @@ -355,6 +367,7 @@ fn output_with_assets( lovelace: u64, assets: &std::collections::BTreeMap, inline_datum_cbor: Option<&[u8]>, + reference_script: Option>, ) -> Result { let mut out = Output::new(addr.clone(), lovelace); for (key, qty) in assets { @@ -376,6 +389,15 @@ fn output_with_assets( if let Some(datum) = inline_datum_cbor { out = out.set_inline_datum(datum.to_vec()); } + // 2026-05-07: optional reference-script attached to the output. + // This is the on-chain equivalent of `cardano-cli ... --tx-out + // --tx-out-reference-script-file ...`. Once deployed, downstream + // txs can witness the script via `read_only_input` instead of + // inline-witnessing the full CBOR. Required for any DAO/dApp that + // wants to keep witness sizes manageable when validators are large. + if let Some(rs) = reference_script { + out = out.set_inline_script(rs.kind, rs.cbor.to_vec()); + } Ok(out) } @@ -386,6 +408,7 @@ fn build_staging_with_fee( to_lovelace: u64, to_assets: &std::collections::BTreeMap, to_inline_datum_cbor: Option<&[u8]>, + to_reference_script: Option>, change_addr: &PallasAddress, change_lovelace: u64, change_assets: &std::collections::BTreeMap, @@ -402,6 +425,7 @@ fn build_staging_with_fee( to_lovelace, to_assets, to_inline_datum_cbor, + to_reference_script, )?); let nonzero_change_assets: std::collections::BTreeMap = change_assets .iter() @@ -409,13 +433,15 @@ fn build_staging_with_fee( .map(|(k, v)| (k.clone(), *v)) .collect(); if change_lovelace > 0 || !nonzero_change_assets.is_empty() { - // Change output never carries an inline datum — it goes back to - // the wallet, which has no validator to satisfy. + // Change output never carries an inline datum or reference + // script — it goes back to the wallet, which has no validator + // to satisfy and no reason to publish a script there. staging = staging.output(output_with_assets( change_addr, change_lovelace, &nonzero_change_assets, None, + None, )?); } staging = staging.fee(fee).network_id(network_id); @@ -482,6 +508,13 @@ fn build_unsigned_bytes( /// script address with a datum the validator can read (AUDIT4-3 /// fix). Change output never gets a datum — it goes back to the /// wallet which has no validator to satisfy. +/// +/// `to_reference_script`, when `Some`, attaches the script bytes as +/// a reference-script on the recipient output (Babbage/Conway era +/// `--tx-out-reference-script-file`). Used to deploy a Plutus +/// validator/policy as a reusable on-chain reference. Pairs naturally +/// with sending to the wallet's own address — the wallet then "owns" +/// (spends from) the ref-script UTxO any time it wants to retire it. #[allow(clippy::too_many_arguments)] fn prepare_payment( network: Network, @@ -491,6 +524,7 @@ fn prepare_payment( lovelace: u64, assets_to_send: &[AssetSpec], to_inline_datum_cbor: Option<&[u8]>, + to_reference_script: Option>, params: &ProtocolParams, ) -> Result<(BuiltTransaction, PaymentSummary), WalletError> { let to_addr = parse_address(to_address_bech32)?; @@ -579,6 +613,7 @@ fn prepare_payment( lovelace, &target_assets, to_inline_datum_cbor, + to_reference_script, &change_addr, change_pass1, &change_assets, @@ -630,6 +665,7 @@ fn prepare_payment( lovelace, &target_assets, to_inline_datum_cbor, + to_reference_script, &change_addr, final_change, &change_assets, @@ -751,6 +787,42 @@ pub fn build_signed_payment_with_assets( assets_to_send: &[AssetSpec], to_inline_datum_cbor: Option<&[u8]>, params: &ProtocolParams, +) -> Result, WalletError> { + build_signed_payment_extras( + payment_key, + network, + available_utxos, + change_address_bech32, + to_address_bech32, + lovelace, + assets_to_send, + to_inline_datum_cbor, + None, + params, + ) +} + +/// Build + sign a Conway-era payment with the full output extras — +/// ADA + native assets + optional inline datum + optional reference +/// script. Public superset of `build_signed_payment_with_assets`. +/// +/// `to_reference_script`: when `Some`, attaches the script CBOR as a +/// reference-script on the recipient output (Babbage/Conway era). +/// Used to deploy a Plutus validator/policy as a reusable on-chain +/// reference. Pair with sending to the wallet's own address so the +/// wallet retains the ability to retire the deployment later. +#[allow(clippy::too_many_arguments)] +pub fn build_signed_payment_extras( + payment_key: &PaymentKey, + network: Network, + available_utxos: &[InputUtxo], + change_address_bech32: &str, + to_address_bech32: &str, + lovelace: u64, + assets_to_send: &[AssetSpec], + to_inline_datum_cbor: Option<&[u8]>, + to_reference_script: Option>, + params: &ProtocolParams, ) -> Result, WalletError> { let private = payment_key_to_private(payment_key)?; let (built, _summary) = prepare_payment( @@ -761,6 +833,7 @@ pub fn build_signed_payment_with_assets( lovelace, assets_to_send, to_inline_datum_cbor, + to_reference_script, params, )?; let signed = built @@ -807,6 +880,37 @@ pub fn build_unsigned_payment_with_assets( assets_to_send: &[AssetSpec], to_inline_datum_cbor: Option<&[u8]>, params: &ProtocolParams, +) -> Result { + build_unsigned_payment_extras( + network, + available_utxos, + change_address_bech32, + to_address_bech32, + lovelace, + assets_to_send, + to_inline_datum_cbor, + None, + params, + ) +} + +/// Build a Conway-era payment with the full output extras (ADA + +/// native assets + optional inline datum + optional reference script) +/// without signing. Returns unsigned CBOR + `PaymentSummary`. Caller +/// signs + submits via the cold-sign path. +/// +/// See [`build_signed_payment_extras`] for the signed variant. +#[allow(clippy::too_many_arguments)] +pub fn build_unsigned_payment_extras( + network: Network, + available_utxos: &[InputUtxo], + change_address_bech32: &str, + to_address_bech32: &str, + lovelace: u64, + assets_to_send: &[AssetSpec], + to_inline_datum_cbor: Option<&[u8]>, + to_reference_script: Option>, + params: &ProtocolParams, ) -> Result { let (built, summary) = prepare_payment( network, @@ -816,6 +920,7 @@ pub fn build_unsigned_payment_with_assets( lovelace, assets_to_send, to_inline_datum_cbor, + to_reference_script, params, )?; Ok(UnsignedPayment { diff --git a/crates/aldabra-mcp/src/tools.rs b/crates/aldabra-mcp/src/tools.rs index e5a470f..d8cc914 100644 --- a/crates/aldabra-mcp/src/tools.rs +++ b/crates/aldabra-mcp/src/tools.rs @@ -52,11 +52,29 @@ use aldabra_dao::reader::{DaoReader, KoiosDaoReader}; use aldabra_core::plutus_cost_models::PLUTUS_V3_COST_MODEL_PREPROD; use aldabra_core::{ add_witness, build_signed_cip68_nft_mint, build_signed_mint_with_metadata, - build_signed_payment_with_assets, build_signed_plutus_spend, build_signed_stake_delegation, - build_unsigned_mint, build_unsigned_payment_with_assets, hex_decode, summarize_tx, AssetSpec, - InputUtxo, Network, PaymentKey, PlutusExUnits, PlutusInput, PlutusVersion, PolicySpec, - ProtocolParams, StakeKey, DEFAULT_EX_UNITS, + build_signed_payment_extras, build_signed_payment_with_assets, build_signed_plutus_spend, + build_signed_stake_delegation, build_unsigned_mint, build_unsigned_payment_extras, + build_unsigned_payment_with_assets, hex_decode, summarize_tx, AssetSpec, InputUtxo, Network, + PaymentKey, PlutusExUnits, PlutusInput, PlutusVersion, PolicySpec, ProtocolParams, + ReferenceScriptSpec, ScriptKind, StakeKey, DEFAULT_EX_UNITS, }; + +/// 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, +/// trims whitespace; returns a clean error message on miss. +fn parse_script_kind(s: &str) -> Result { + match s.trim().to_ascii_lowercase().as_str() { + "plutusv1" | "v1" => Ok(ScriptKind::PlutusV1), + "plutusv2" | "v2" => Ok(ScriptKind::PlutusV2), + "plutusv3" | "v3" => Ok(ScriptKind::PlutusV3), + "native" => Ok(ScriptKind::Native), + other => Err(format!( + "invalid reference_script_kind '{other}'; expected one of: \ + PlutusV1, PlutusV2, PlutusV3, Native" + )), + } +} use rmcp::{ model::{ServerCapabilities, ServerInfo}, schemars, tool, ServerHandler, @@ -186,6 +204,22 @@ pub struct SendArgs { /// a datum are un-spendable). Omit for normal sends. #[serde(default)] pub datum_inline_cbor_hex: Option, + /// Optional reference-script CBOR (hex). When set, the recipient + /// output carries the script as a reference-script (Babbage/Conway + /// era `--tx-out-reference-script-file` equivalent). Used to + /// deploy a Plutus validator/policy as a reusable on-chain + /// reference so downstream txs can witness it via `--tx-in-script- + /// file ref` instead of inline-witnessing the full CBOR. Pair with + /// `to_address` = wallet's own address so the wallet retains the + /// ability to retire the deployment later. + /// Requires `reference_script_kind` to also be set. + #[serde(default)] + pub reference_script_cbor_hex: Option, + /// Plutus version of the reference-script: "PlutusV1", "PlutusV2", + /// "PlutusV3", or "Native". Required when reference_script_cbor_hex + /// is set; ignored otherwise. + #[serde(default)] + pub reference_script_kind: Option, /// Bypass the configured `max_send_lovelace` hard cap. Only /// pass `true` for an intentional, user-confirmed large send. #[serde(default)] @@ -260,6 +294,14 @@ pub struct UnsignedSendArgs { /// Optional inline-datum CBOR (hex). See [`SendArgs::datum_inline_cbor_hex`]. #[serde(default)] pub datum_inline_cbor_hex: Option, + /// Optional reference-script CBOR (hex). See + /// [`SendArgs::reference_script_cbor_hex`]. + #[serde(default)] + pub reference_script_cbor_hex: Option, + /// "PlutusV1" | "PlutusV2" | "PlutusV3" | "Native". See + /// [`SendArgs::reference_script_kind`]. + #[serde(default)] + pub reference_script_kind: Option, } #[derive(Debug, Deserialize, schemars::JsonSchema)] @@ -574,6 +616,8 @@ impl WalletService { lovelace, assets, datum_inline_cbor_hex, + reference_script_cbor_hex, + reference_script_kind, force, }: SendArgs, ) -> Result { @@ -623,8 +667,25 @@ impl WalletService { Some(s) => Some(hex_decode(s).map_err(|e| format!("decode datum: {e}"))?), None => None, }; + let ref_script_bytes = match reference_script_cbor_hex.as_deref() { + Some(s) => Some(hex_decode(s).map_err(|e| format!("decode reference_script: {e}"))?), + None => None, + }; + let ref_script = match (ref_script_bytes.as_ref(), reference_script_kind.as_deref()) { + (Some(bytes), Some(kind)) => Some(ReferenceScriptSpec { + kind: parse_script_kind(kind)?, + cbor: bytes.as_slice(), + }), + (Some(_), None) => { + return Err("reference_script_cbor_hex set without reference_script_kind".into()) + } + (None, Some(_)) => { + return Err("reference_script_kind set without reference_script_cbor_hex".into()) + } + (None, None) => None, + }; - let cbor = build_signed_payment_with_assets( + let cbor = build_signed_payment_extras( &self.inner.payment_key, self.inner.network, &inputs, @@ -633,6 +694,7 @@ impl WalletService { lovelace, &asset_specs, datum_bytes.as_deref(), + ref_script, &ProtocolParams::default(), ) .map_err(|e| format!("build/sign: {e}"))?; @@ -674,6 +736,8 @@ impl WalletService { lovelace, assets, datum_inline_cbor_hex, + reference_script_cbor_hex, + reference_script_kind, }: UnsignedSendArgs, ) -> Result { if lovelace == 0 { @@ -706,8 +770,25 @@ impl WalletService { Some(s) => Some(hex_decode(s).map_err(|e| format!("decode datum: {e}"))?), None => None, }; + let ref_script_bytes = match reference_script_cbor_hex.as_deref() { + Some(s) => Some(hex_decode(s).map_err(|e| format!("decode reference_script: {e}"))?), + None => None, + }; + let ref_script = match (ref_script_bytes.as_ref(), reference_script_kind.as_deref()) { + (Some(bytes), Some(kind)) => Some(ReferenceScriptSpec { + kind: parse_script_kind(kind)?, + cbor: bytes.as_slice(), + }), + (Some(_), None) => { + return Err("reference_script_cbor_hex set without reference_script_kind".into()) + } + (None, Some(_)) => { + return Err("reference_script_kind set without reference_script_cbor_hex".into()) + } + (None, None) => None, + }; - let unsigned = build_unsigned_payment_with_assets( + let unsigned = build_unsigned_payment_extras( self.inner.network, &inputs, &self.inner.address, @@ -715,6 +796,7 @@ impl WalletService { lovelace, &asset_specs, datum_bytes.as_deref(), + ref_script, &ProtocolParams::default(), ) .map_err(|e| format!("build: {e}"))?;