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 Kayos'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:
parent
82e8273969
commit
b9124ee5d9
3 changed files with 201 additions and 12 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<String, u64>,
|
||||
inline_datum_cbor: Option<&[u8]>,
|
||||
reference_script: Option<ReferenceScriptSpec<'_>>,
|
||||
) -> Result<Output, WalletError> {
|
||||
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<String, u64>,
|
||||
to_inline_datum_cbor: Option<&[u8]>,
|
||||
to_reference_script: Option<ReferenceScriptSpec<'_>>,
|
||||
change_addr: &PallasAddress,
|
||||
change_lovelace: u64,
|
||||
change_assets: &std::collections::BTreeMap<String, u64>,
|
||||
|
|
@ -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<String, u64> = 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<ReferenceScriptSpec<'_>>,
|
||||
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<Vec<u8>, 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<ReferenceScriptSpec<'_>>,
|
||||
params: &ProtocolParams,
|
||||
) -> Result<Vec<u8>, 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<UnsignedPayment, WalletError> {
|
||||
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<ReferenceScriptSpec<'_>>,
|
||||
params: &ProtocolParams,
|
||||
) -> Result<UnsignedPayment, WalletError> {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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}"))?;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue