feat(dao-mcp): wire dao_proposal_create_unsigned to fetch C-2 inputs from chain

This commit is contained in:
Kayos 2026-05-05 20:57:36 -07:00
parent afd0cfb298
commit 893e3f23da

View file

@ -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<DaoWalletUtxo> = {
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,
})