diff --git a/crates/aldabra-mcp/src/tools.rs b/crates/aldabra-mcp/src/tools.rs index a1a6675..727eba3 100644 --- a/crates/aldabra-mcp/src/tools.rs +++ b/crates/aldabra-mcp/src/tools.rs @@ -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, } +#[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, + /// 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, + /// 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, + /// 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, + /// 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, + #[serde(default)] + pub ex_units_steps: Option, +} + +#[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 { + 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 = 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::>()?; + let core_extras: Vec = 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 = 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 = 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."