diff --git a/crates/aldabra-mcp/src/tools.rs b/crates/aldabra-mcp/src/tools.rs index 7de0d5c..7dae6c7 100644 --- a/crates/aldabra-mcp/src/tools.rs +++ b/crates/aldabra-mcp/src/tools.rs @@ -30,8 +30,9 @@ use std::sync::Arc; use aldabra_chain::{ChainBackend, KoiosClient}; use aldabra_dao::agora::stake::Credential as DaoCredential; +use aldabra_dao::agora::stake::StakeDatum; use aldabra_dao::builder::proposal_create::{ - build_unsigned_proposal_create, GovernorUtxoIn, ProposalCreateArgs, ReferenceUtxo, + build_unsigned_proposal_create, GovernorUtxoIn, ProposalCreateArgs, ReferenceUtxo, StakeUtxoIn, WalletUtxo as DaoWalletUtxo, }; use aldabra_dao::config::{DaoConfig, DaoNetwork, DaoStore, ScriptRefs}; @@ -1588,10 +1589,8 @@ impl WalletService { .map_err(|e| e.to_string())?; let (gov_tx_hash, gov_idx) = parse_utxo_ref(&governor_utxo_ref)?; - // Pull governor lovelace + every wallet UTxO. We need both: - // (a) governor lovelace for tx-balance calculation, - // (b) wallet utxos for funding + collateral selection. - let gov_lovelace = self + // Pull governor lovelace + GST asset id from the same utxo. + let governor_utxo = self .inner .chain .get_utxos(&cfg.governor_addr) @@ -1599,8 +1598,93 @@ impl WalletService { .map_err(|e| format!("koios get governor utxos: {e}"))? .into_iter() .find(|u| u.tx_hash == gov_tx_hash && u.output_index == gov_idx) - .ok_or_else(|| format!("governor utxo {governor_utxo_ref} no longer present on chain"))? - .lovelace; + .ok_or_else(|| format!("governor utxo {governor_utxo_ref} no longer present on chain"))?; + let gov_lovelace = governor_utxo.lovelace; + // Extract GST policy + name from the governor utxo's asset_list. + // Sulkta's GST has empty asset name; one asset on the utxo (qty=1) IS the GST. + let (gst_policy_hex, gst_asset_name_hex) = governor_utxo + .assets + .iter() + .next() + .map(|(k, _)| { + if k.len() < 56 { + return ("".to_string(), "".to_string()); + } + let (p, n) = k.split_at(56); + (p.to_string(), n.to_string()) + }) + .ok_or_else(|| { + "governor UTxO has no GST asset — chain state inconsistent".to_string() + })?; + if gst_policy_hex.is_empty() { + return Err("governor UTxO asset key malformed (< 56 chars)".into()); + } + + // Find the proposer's stake at stakes_addr via dao_reader.list_stakes. + // Match on owner pkh. + let proposer_pkh = self.wallet_pkh()?; + let stakes = self + .inner + .dao_reader + .list_stakes(&cfg) + .await + .map_err(|e| e.to_string())?; + let my_stake = stakes + .into_iter() + .find(|s| match &s.datum.owner { + aldabra_dao::agora::stake::Credential::PubKey(h) => h == &proposer_pkh, + _ => false, + }) + .ok_or_else(|| { + format!( + "no stake at stakes_addr owned by this wallet's pkh {} — \ + proposer must have a registered stake first", + hex::encode(&proposer_pkh) + ) + })?; + let (stake_tx, stake_idx) = parse_utxo_ref(&my_stake.utxo_ref)?; + // Pull StakeST asset name from the stake utxo. + let stake_utxos_raw = self + .inner + .chain + .get_utxos(&cfg.stakes_addr) + .await + .map_err(|e| format!("koios get stake utxos: {e}"))?; + let stake_utxo_raw = stake_utxos_raw + .into_iter() + .find(|u| u.tx_hash == stake_tx && u.output_index == stake_idx) + .ok_or_else(|| format!("stake utxo {} no longer on chain", my_stake.utxo_ref))?; + let stake_st_asset_name_hex = stake_utxo_raw + .assets + .iter() + .find_map(|(k, _)| { + if k.len() < 56 { + return None; + } + let (p, n) = k.split_at(56); + if p == cfg.stake_st_policy.as_deref().unwrap_or("") { + Some(n.to_string()) + } else { + None + } + }) + .ok_or_else(|| "stake UTxO is missing StakeST token".to_string())?; + + // Chain tip slot — for tx validity range. + let tip_resp = self + .inner + .chain + .get_raw_json("tip", &[]) + .await + .map_err(|e| format!("koios tip: {e}"))?; + let tip: serde_json::Value = + serde_json::from_str(&tip_resp).map_err(|e| format!("tip parse: {e}"))?; + let tip_slot = tip + .as_array() + .and_then(|a| a.first()) + .and_then(|t| t.get("abs_slot")) + .and_then(|s| s.as_u64()) + .ok_or_else(|| format!("tip response missing abs_slot: {tip_resp}"))?; let wallet_utxos: Vec = { let raw = self @@ -1642,17 +1726,23 @@ impl WalletService { }; // ScriptRefs must be populated before this tool can build a tx. - // For Sulkta the values are known from the audit; user must pass - // them in via dao_register or hand-edit the json (until - // dao_discover_scripts ships). let governor_validator_ref = ReferenceUtxo::from_str( cfg.script_refs .governor_validator .as_deref() .ok_or_else(|| { - "DaoConfig.script_refs.governor_validator missing — populate before \ - calling dao_proposal_create_unsigned" - .to_string() + "DaoConfig.script_refs.governor_validator missing — \ + run dao_discover_scripts first".to_string() + })?, + ) + .map_err(|e| e.to_string())?; + let stake_validator_ref = ReferenceUtxo::from_str( + cfg.script_refs + .stake_validator + .as_deref() + .ok_or_else(|| { + "DaoConfig.script_refs.stake_validator missing — \ + run dao_discover_scripts first".to_string() })?, ) .map_err(|e| e.to_string())?; @@ -1661,14 +1751,11 @@ impl WalletService { .proposal_st_policy .as_deref() .ok_or_else(|| { - "DaoConfig.script_refs.proposal_st_policy missing — populate first" - .to_string() + "DaoConfig.script_refs.proposal_st_policy missing".to_string() })?, ) .map_err(|e| e.to_string())?; - let proposer_pkh = self.wallet_pkh()?; - let unsigned = build_unsigned_proposal_create(ProposalCreateArgs { cfg: cfg.clone(), governor: GovernorUtxoIn { @@ -1676,12 +1763,24 @@ impl WalletService { output_index: gov_idx, lovelace: gov_lovelace, datum: governor_datum, + gst_policy_hex, + gst_asset_name_hex, + }, + stake_in: StakeUtxoIn { + tx_hash_hex: stake_tx, + output_index: stake_idx, + lovelace: my_stake.lovelace, + gov_token_qty: my_stake.gov_token_quantity, + stake_st_asset_name_hex, + datum: my_stake.datum, }, proposer_pkh, change_address: self.inner.address.clone(), wallet_utxos, starting_time_ms, + tip_slot, governor_validator_ref, + stake_validator_ref, proposal_st_policy_ref, fee_lovelace, })