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:
parent
3b0e0dd9bf
commit
68e493dd2f
2 changed files with 90 additions and 152 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1832,34 +1832,25 @@ impl WalletService {
|
|||
"DaoConfig.proposal_addr missing — register or discover_scripts first".to_string()
|
||||
})?;
|
||||
|
||||
// Find this proposal among the UTxOs at proposal_addr. Use the
|
||||
// dedicated `pull_proposal_utxos` helper which decodes inline
|
||||
// datums (the regular `chain.get_utxos` doesn't surface inline
|
||||
// datum bytes).
|
||||
let mut found: Option<(
|
||||
String, // tx_hash
|
||||
u32, // output_index
|
||||
u64, // lovelace
|
||||
String, // proposal_st asset name hex
|
||||
aldabra_dao::agora::proposal::ProposalDatum,
|
||||
)> = None;
|
||||
let pulled = pull_proposal_utxos(&self.inner.koios_base, proposal_addr).await?;
|
||||
for p in pulled {
|
||||
if p.datum.proposal_id == proposal_id {
|
||||
found = Some((
|
||||
p.tx_hash,
|
||||
p.output_index,
|
||||
p.lovelace,
|
||||
p.proposal_st_asset_name_hex,
|
||||
p.datum,
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (prop_tx, prop_idx, prop_lovelace, prop_st_name_hex, prop_datum) =
|
||||
found.ok_or_else(|| {
|
||||
// Find this proposal among the UTxOs at proposal_addr. Goes
|
||||
// through the DaoReader which decodes inline datums + filters
|
||||
// out orphan/non-proposal UTxOs.
|
||||
let proposals = self
|
||||
.inner
|
||||
.dao_reader
|
||||
.list_proposals(&cfg)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let target = proposals
|
||||
.into_iter()
|
||||
.find(|p| p.datum.proposal_id == proposal_id)
|
||||
.ok_or_else(|| {
|
||||
format!("no proposal with proposal_id={} found at {}", proposal_id, proposal_addr)
|
||||
})?;
|
||||
let (prop_tx, prop_idx) = parse_utxo_ref(&target.utxo_ref)?;
|
||||
let prop_lovelace = target.lovelace;
|
||||
let prop_st_name_hex = target.proposal_st_asset_name_hex;
|
||||
let prop_datum = target.datum;
|
||||
|
||||
// Find the voter's stake.
|
||||
let voter_pkh = self.wallet_pkh()?;
|
||||
|
|
@ -2215,117 +2206,6 @@ fn parse_utxo_ref(s: &str) -> Result<(String, u32), String> {
|
|||
Ok((h.to_string(), idx))
|
||||
}
|
||||
|
||||
/// One proposal UTxO with decoded datum + ProposalST asset name.
|
||||
///
|
||||
/// Sized so [`dao_proposal_vote_unsigned`] can find a specific proposal_id
|
||||
/// without dragging the full Koios JSON shape through the call site.
|
||||
struct PulledProposal {
|
||||
tx_hash: String,
|
||||
output_index: u32,
|
||||
lovelace: u64,
|
||||
proposal_st_asset_name_hex: String,
|
||||
datum: aldabra_dao::agora::proposal::ProposalDatum,
|
||||
}
|
||||
|
||||
/// Pull every UTxO at the proposal address with a decoded ProposalDatum.
|
||||
///
|
||||
/// Used by `dao_proposal_vote_unsigned` to find a specific proposal by id.
|
||||
/// Phase 1 didn't wire this to `KoiosDaoReader::list_proposals` because the
|
||||
/// reader trait doesn't surface ProposalST asset info — we need both the
|
||||
/// ProposalST asset name (for datum-bearing assets) AND the bech32 utxo ref,
|
||||
/// so a focused helper here is cleaner than adding fields to the trait.
|
||||
async fn pull_proposal_utxos(
|
||||
koios_base: &str,
|
||||
proposal_addr: &str,
|
||||
) -> Result<Vec<PulledProposal>, String> {
|
||||
use aldabra_dao::agora::proposal::ProposalDatum;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AddrInfo {
|
||||
utxo_set: Vec<Utxo>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Utxo {
|
||||
tx_hash: String,
|
||||
tx_index: u32,
|
||||
value: String,
|
||||
#[serde(default)]
|
||||
asset_list: Option<Vec<Asset>>,
|
||||
#[serde(default)]
|
||||
inline_datum: Option<InlineDatum>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct Asset {
|
||||
policy_id: String,
|
||||
asset_name: Option<String>,
|
||||
quantity: String,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct InlineDatum {
|
||||
bytes: String,
|
||||
}
|
||||
|
||||
let url = format!("{}/address_info", koios_base.trim_end_matches('/'));
|
||||
let body = serde_json::json!({ "_addresses": [proposal_addr] });
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|e| format!("reqwest build: {e}"))?;
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("address_info: {e}"))?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("address_info: HTTP {}", resp.status()));
|
||||
}
|
||||
let infos: Vec<AddrInfo> = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("address_info parse: {e}"))?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
for info in infos {
|
||||
for u in info.utxo_set {
|
||||
let Some(d) = u.inline_datum else { continue };
|
||||
let datum_bytes = match hex::decode(&d.bytes) {
|
||||
Ok(b) => b,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let pd: pallas_primitives::PlutusData =
|
||||
match pallas_codec::minicbor::decode(&datum_bytes) {
|
||||
Ok(pd) => pd,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let datum = match ProposalDatum::from_plutus_data(&pd) {
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
};
|
||||
// Find a non-zero qty asset whose policy looks like the
|
||||
// ProposalST policy. We don't have that here directly — caller
|
||||
// can match on it; we just expose the asset_name of the first
|
||||
// singleton asset (Sulkta convention).
|
||||
let proposal_st_asset_name_hex = u
|
||||
.asset_list
|
||||
.as_ref()
|
||||
.and_then(|al| al.first())
|
||||
.and_then(|a| a.asset_name.clone())
|
||||
.unwrap_or_default();
|
||||
let lovelace = u.value.parse().unwrap_or(0);
|
||||
out.push(PulledProposal {
|
||||
tx_hash: u.tx_hash,
|
||||
output_index: u.tx_index,
|
||||
lovelace,
|
||||
proposal_st_asset_name_hex,
|
||||
datum,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Render a [`aldabra_dao::reader::StakeUtxo`] as a JSON object for tool output.
|
||||
///
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue