refactor(dao): wire KoiosDaoReader::list_proposals + use it from vote tool

The first attempt's vote MCP tool inlined a Koios address_info pull
helper in tools.rs that needed reqwest + pallas_codec + pallas_primitives
as direct deps on aldabra-mcp — which it doesn't have. Compile failed.

Cleaner: move the work into the dao crate where those deps already live.

- ProposalUtxo gains `lovelace` + `proposal_st_asset_name_hex`. The
  vote builder needs both to construct the new proposal output.
- KoiosDaoReader::list_proposals (was stubbed) now reads cfg.proposal_addr,
  decodes every UTxO's inline datum to ProposalDatum, and matches the
  ProposalST asset name against cfg.proposal_st_policy when set, falling
  back to the first asset on the utxo when not (Sulkta convention is one
  ProposalST + nothing else).
- KoiosAsset.asset_name no longer #[allow(dead_code)] — it's read now.
- tools.rs::dao_proposal_vote_unsigned switches to dao_reader.list_proposals
  + drops the inline pull helper. ~150 LOC simpler.
This commit is contained in:
Sulkta 2026-05-06 06:41:52 -07:00
parent d9f852d8bd
commit cc69e0df21
2 changed files with 90 additions and 152 deletions

View file

@ -44,11 +44,15 @@ pub struct StakeUtxo {
pub gov_token_quantity: u64,
}
/// One on-chain proposal at the proposal script address (derived from
/// the gov-token policy).
/// One on-chain proposal at the proposal script address.
#[derive(Debug, Clone)]
pub struct ProposalUtxo {
pub utxo_ref: String,
/// Lovelace at this UTxO. Preserved in vote/cosign/advance outputs.
pub lovelace: u64,
/// Asset name (hex) of the ProposalST token on this UTxO. Sulkta
/// convention is empty bytes; community DAOs may use something else.
pub proposal_st_asset_name_hex: String,
pub datum: ProposalDatum,
}
@ -195,18 +199,72 @@ impl DaoReader for KoiosDaoReader {
Ok(out)
}
async fn list_proposals(&self, _cfg: &DaoConfig) -> DaoResult<Vec<ProposalUtxo>> {
// Proposals live at the proposal script address, which is derived
// from the Agora deployment + gov-token-policy parameters. We
// don't compute that derivation in Phase 1 (it lands in Phase 4
// alongside reference_scripts.rs). For now: return empty + a
// tracked-todo string. Real wiring: decode proposal script
// hash → bech32 → call address_info → filter to inline-datum
// UTxOs → decode ProposalDatum.
Err(DaoError::State(
"list_proposals pending Phase 4 (proposal script address discovery)"
.into(),
))
async fn list_proposals(&self, cfg: &DaoConfig) -> DaoResult<Vec<ProposalUtxo>> {
let proposal_addr = cfg.proposal_addr.as_deref().ok_or_else(|| {
DaoError::Config(
"DaoConfig.proposal_addr missing — register the DAO with proposal_addr \
or run dao_discover_scripts first"
.into(),
)
})?;
let infos = self.address_info(proposal_addr).await?;
let utxos = infos
.into_iter()
.next()
.map(|i| i.utxo_set)
.unwrap_or_default();
let mut out = Vec::new();
for u in utxos {
// Need an inline datum to be a real proposal UTxO. Skip orphans.
let Some(ref d) = u.inline_datum else { continue };
let pd = match decode_datum_cbor_hex(&d.bytes) {
Ok(pd) => pd,
Err(_) => continue,
};
let datum = match ProposalDatum::from_plutus_data(&pd) {
Ok(d) => d,
Err(_) => continue,
};
// Pick the ProposalST asset name. We don't have the policy id
// baked into the trait surface (cfg.proposal_st_policy may or
// may not be populated yet), so:
// - if cfg.proposal_st_policy IS set, match exactly on it;
// - otherwise fall back to "the first asset on the utxo,"
// which is right for Sulkta convention (1 ProposalST + 0
// other assets) but defends against the case where a
// community DAO bundles other tokens in the proposal output.
let proposal_st_asset_name_hex = match cfg.proposal_st_policy.as_deref() {
Some(target_policy) => u
.asset_list
.as_ref()
.into_iter()
.flatten()
.find_map(|a| {
if a.policy_id == target_policy {
Some(a.asset_name.clone().unwrap_or_default())
} else {
None
}
})
.unwrap_or_default(),
None => u
.asset_list
.as_ref()
.and_then(|al| al.first())
.and_then(|a| a.asset_name.clone())
.unwrap_or_default(),
};
out.push(ProposalUtxo {
utxo_ref: format!("{}#{}", u.tx_hash, u.tx_index),
lovelace: u.value.parse().unwrap_or(0),
proposal_st_asset_name_hex,
datum,
});
}
Ok(out)
}
}
@ -235,7 +293,7 @@ struct KoiosUtxo {
#[derive(Debug, Deserialize)]
struct KoiosAsset {
policy_id: String,
#[allow(dead_code)]
#[serde(default)]
asset_name: Option<String>,
quantity: String,
}