feat(wallet): reference-script + extras on payment outputs

Adds Babbage/Conway-era reference-script attachment to wallet_send
and wallet_send_unsigned. The output can now carry any combination
of {native assets, inline datum, reference script}.

Why: deploying Plutus validators / minting policies as on-chain
reference UTxOs is the standard Cardano dApp pattern (Agora, Liqwid,
SundaeSwap all do this). Without it every spend or mint that uses
a script has to inline-witness the full CBOR — kilobytes per tx and
quadratic with tx size. Reference scripts let downstream txs witness
via `read_only_input` for ~32 bytes overhead.

API surface:

aldabra-core:
- new `ReferenceScriptSpec<'a> { kind: ScriptKind, cbor: &'a [u8] }`
- `ScriptKind` re-exported from pallas_txbuilder (PlutusV1/V2/V3/Native)
- new `build_signed_payment_extras(...)` and
  `build_unsigned_payment_extras(...)` — supersets of the existing
  `_with_assets` functions; take both `to_inline_datum_cbor` and
  `to_reference_script` Options
- existing `_with_assets` functions kept as thin wrappers that pass
  None for ref script — backwards compatible
- internal `output_with_assets`, `prepare_payment`, and
  `build_staging_with_fee` thread the new ref-script Option through

aldabra-mcp:
- `SendArgs` and `UnsignedSendArgs` gain
  `reference_script_cbor_hex: Option<String>` and
  `reference_script_kind: Option<String>`
- Both must be set or both omitted; mismatched returns a clean error
- `parse_script_kind` helper — case-insensitive, accepts
  PlutusV1/V2/V3/Native (plus shortcut V1/V2/V3)

Reference scripts are intentionally never attached to change outputs.
The change goes back to the wallet's own address, where a script
attachment would lock value into a publicized script that we'd then
have to spend BACK out — pointless. Ref-script attachment is only
on the recipient (`to`) output.

This unblocks Track B-fast step 3 of the preprod DAO bringup —
deploying the 11 Agora script bytecodes (governor / stakes /
proposal / treasury / mutate / noOp / treasuryWithdrawal validators
+ GST / StakeST / ProposalST / GAT minting policies) as reference
UTxOs against Sulkta's preprod wallet.

No new tests in this commit — Phase 2 (Plutus-policy mint with
custom output) lands in a follow-up that will exercise this path
end-to-end against a real chain submit on preprod.
This commit is contained in:
Sulkta 2026-05-07 06:17:07 -07:00
parent 1500a30e9d
commit 8b0e090668
3 changed files with 201 additions and 12 deletions

View file

@ -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<ScriptKind, String> {
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<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
/// Optional reference-script CBOR (hex). See
/// [`SendArgs::reference_script_cbor_hex`].
#[serde(default)]
pub reference_script_cbor_hex: Option<String>,
/// "PlutusV1" | "PlutusV2" | "PlutusV3" | "Native". See
/// [`SendArgs::reference_script_kind`].
#[serde(default)]
pub reference_script_kind: Option<String>,
}
#[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<String, String> {
@ -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<String, String> {
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}"))?;