feat(mcp): wallet_plutus_mint_unsigned MCP tool

Wires the new aldabra-core::plutus_mint module into MCP. Tool name
mirrors the unsigned-first DAO write convention (wallet_send_unsigned,
dao_proposal_*_unsigned, etc).

Args:
- policy_cbor_hex + policy_version (v1/v2/v3) + redeemer_cbor_hex
- mint_assets: array of {asset_name_hex, quantity}
- dest_address + dest_lovelace + dest_extra_assets + dest_inline_datum_cbor_hex
- required_input_refs: array of 'txhash#index' UTxOs that MUST be spent
- ex_units (mem + steps, optional — defaults to DEFAULT_EX_UNITS)

Returns the standard unsigned-payment shape ({cbor_hex, summary})
ready for wallet_sign_partial → wallet_submit_signed_tx.

Used for governor + stake + proposal bootstrap of any Plutus DAO.
For Agora preprod bringup, the typical call is:
- governor: required_input_refs=[gstOutRef], mint=[(GST,1)],
  dest=governor_addr, datum=GovernorDatum
- stake: mint=[(StakeST,1)], dest_extras=[(tTRP,N)], dest=stakes_addr,
  datum=StakeDatum
- proposal: spends GST input under the existing governor's spend
  redeemer + mints ProposalST under the proposal policy
This commit is contained in:
Kayos 2026-05-07 06:36:05 -07:00
parent 86bc4e45cd
commit b50d45b5de

View file

@ -53,8 +53,9 @@ 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_extras, build_signed_plutus_spend, build_signed_stake_delegation,
build_unsigned_mint, build_unsigned_payment_extras, hex_decode, summarize_tx, AssetSpec,
InputUtxo, Network, PaymentKey, PlutusExUnits, PlutusInput, PlutusVersion, PolicySpec,
build_unsigned_mint, build_unsigned_payment_extras, build_unsigned_plutus_mint, hex_decode,
summarize_tx, AssetSpec, ExtraDestAsset, InputUtxo, Network, PaymentKey, PlutusExUnits,
PlutusInput, PlutusMintArgs as CorePlutusMintArgs, PlutusMintAsset, PlutusVersion, PolicySpec,
ProtocolParams, ReferenceScriptSpec, ScriptKind, StakeKey, DEFAULT_EX_UNITS,
};
@ -320,6 +321,62 @@ pub struct PolicyCreateArgs {
pub invalid_after_slot: Option<u64>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
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,
/// Plutus version: "v1", "v2", or "v3".
pub policy_version: String,
/// PlutusData CBOR redeemer (hex) for the mint redeemer entry.
pub redeemer_cbor_hex: String,
/// Assets to mint under this policy. Each entry needs an
/// `asset_name_hex` (hex of raw bytes, 0-64 chars) and a
/// `quantity` (i64). Quantity > 0 mints, < 0 burns. Burning
/// requires the wallet to already hold the asset.
pub mint_assets: Vec<PlutusMintAssetArg>,
/// Recipient address — typically a Plutus script address (e.g.
/// governor / stakes / proposal address from the Agora linker).
pub dest_address: String,
/// ADA on the recipient output. Must be ≥ min-utxo for the
/// shape (asset count + name length).
pub dest_lovelace: u64,
/// Non-mint native assets to forward from wallet inputs onto
/// the recipient output. Used e.g. on stake bootstrap to send
/// gov tokens (tTRP) into the stakes_addr alongside the freshly
/// minted StakeST.
#[serde(default)]
pub dest_extra_assets: Vec<McpAssetSpec>,
/// PlutusData CBOR (hex) for the recipient output's inline
/// datum. REQUIRED when sending to a script address — the
/// validator needs a datum to read on subsequent spends.
#[serde(default)]
pub dest_inline_datum_cbor_hex: Option<String>,
/// UTxOs that MUST appear as regular tx inputs. Each is
/// `txhash#index` referencing a UTxO at this wallet's address.
/// Use this to spend the UTxO a parameterized minting policy
/// is bound to (Agora's `gstOutRef` is the canonical case).
#[serde(default)]
pub required_input_refs: Vec<String>,
/// ExUnits budget for the mint redeemer. Defaults to the
/// generous DEFAULT_EX_UNITS if omitted. Tune for known
/// validators to keep the fee tight.
#[serde(default)]
pub ex_units_mem: Option<u64>,
#[serde(default)]
pub ex_units_steps: Option<u64>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct PlutusMintAssetArg {
/// Hex of raw asset name bytes (0-64 chars). Empty string for
/// policy-only / no-asset-name native assets.
pub asset_name_hex: String,
/// Positive = mint, negative = burn (caller must hold the
/// assets to burn).
pub quantity: i64,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MintUnsignedArgs {
pub dest_address: String,
@ -1478,6 +1535,149 @@ impl WalletService {
serde_json::to_string(&unsigned).map_err(|e| e.to_string())
}
#[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."
)]
async fn wallet_plutus_mint_unsigned(
&self,
#[tool(aggr)] PlutusMintUnsignedArgs {
policy_cbor_hex,
policy_version,
redeemer_cbor_hex,
mint_assets,
dest_address,
dest_lovelace,
dest_extra_assets,
dest_inline_datum_cbor_hex,
required_input_refs,
ex_units_mem,
ex_units_steps,
}: PlutusMintUnsignedArgs,
) -> Result<String, String> {
if mint_assets.is_empty() {
return Err("mint_assets must contain at least one entry".into());
}
if dest_lovelace < 1_000_000 {
return Err(format!(
"dest_lovelace {dest_lovelace} below 1 ADA min for asset-bearing UTXO"
));
}
let policy_cbor =
hex_decode(&policy_cbor_hex).map_err(|e| format!("decode policy_cbor: {e}"))?;
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() {
"v1" | "plutusv1" => PlutusVersion::V1,
"v2" | "plutusv2" => PlutusVersion::V2,
"v3" | "plutusv3" => PlutusVersion::V3,
other => {
return Err(format!(
"invalid policy_version '{other}'; expected v1/v2/v3"
))
}
};
let datum_bytes = match dest_inline_datum_cbor_hex.as_deref() {
Some(s) => Some(hex_decode(s).map_err(|e| format!("decode datum: {e}"))?),
None => None,
};
let core_mints: Vec<PlutusMintAsset> = mint_assets
.into_iter()
.map(|m| {
if m.quantity == 0 {
return Err("mint_asset quantity must be nonzero".to_string());
}
Ok(PlutusMintAsset {
asset_name_hex: m.asset_name_hex,
quantity: m.quantity,
})
})
.collect::<Result<_, _>>()?;
let core_extras: Vec<ExtraDestAsset> = dest_extra_assets
.into_iter()
.map(|a| ExtraDestAsset {
policy_id_hex: a.policy_id_hex,
asset_name_hex: a.asset_name_hex,
quantity: a.quantity,
})
.collect();
// Pull current UTxO set; resolve required_input_refs against it.
let utxos = self
.inner
.chain
.get_utxos(&self.inner.address)
.await
.map_err(|e| format!("fetch utxos: {e}"))?;
if utxos.is_empty() {
return Err(format!(
"no utxos at wallet address {}",
self.inner.address
));
}
let inputs: Vec<InputUtxo> = utxos
.into_iter()
.map(|u| InputUtxo {
tx_hash_hex: u.tx_hash,
output_index: u.output_index,
lovelace: u.lovelace,
assets: u.assets,
})
.collect();
let mut required: Vec<InputUtxo> = Vec::with_capacity(required_input_refs.len());
for r in &required_input_refs {
let (h, ix) = r
.split_once('#')
.ok_or_else(|| format!("required_input_ref '{r}' must be 'txhash#index'"))?;
let ix: u32 = ix.parse().map_err(|e| format!("required_input_ref idx: {e}"))?;
let found = inputs
.iter()
.find(|u| u.tx_hash_hex == h && u.output_index == ix)
.ok_or_else(|| {
format!(
"required_input {r} not found in this wallet's UTxOs — \
either fund it first, or pass an existing UTxO ref"
)
})?;
required.push(found.clone());
}
let ex_units = match (ex_units_mem, ex_units_steps) {
(Some(m), Some(s)) => PlutusExUnits { mem: m, steps: s },
(None, None) => DEFAULT_EX_UNITS,
_ => {
return Err(
"ex_units_mem and ex_units_steps must both be set or both omitted".into(),
)
}
};
let core_args = CorePlutusMintArgs {
required_inputs: &required,
policy_cbor: &policy_cbor,
policy_version: policy_ver,
redeemer_cbor: &redeemer_cbor,
ex_units,
mint_assets: &core_mints,
dest_address_bech32: &dest_address,
dest_lovelace,
dest_extra_assets: &core_extras,
dest_inline_datum_cbor: datum_bytes.as_deref(),
};
let unsigned = build_unsigned_plutus_mint(
self.inner.network,
&inputs,
&self.inner.address,
&core_args,
&ProtocolParams::default(),
)
.map_err(|e| format!("build unsigned plutus mint: {e}"))?;
serde_json::to_string(&unsigned).map_err(|e| e.to_string())
}
#[tool(
name = "wallet_tx_summary",
description = "Decode a Conway-era tx CBOR (unsigned, partial, or signed) into a human-reviewable JSON summary: tx_hash, inputs count, outputs (address+lovelace+assets+inline_datum flag), fee, certificates, mint, witness count, aux-data presence. **Read-only — does not sign or submit.** Run this before `wallet_sign_partial` on any CBOR you didn't build yourself."