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
d9f852d8bd
commit
cc69e0df21
2 changed files with 90 additions and 152 deletions
|
|
@ -44,11 +44,15 @@ pub struct StakeUtxo {
|
||||||
pub gov_token_quantity: u64,
|
pub gov_token_quantity: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One on-chain proposal at the proposal script address (derived from
|
/// One on-chain proposal at the proposal script address.
|
||||||
/// the gov-token policy).
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ProposalUtxo {
|
pub struct ProposalUtxo {
|
||||||
pub utxo_ref: String,
|
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,
|
pub datum: ProposalDatum,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,18 +199,72 @@ impl DaoReader for KoiosDaoReader {
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_proposals(&self, _cfg: &DaoConfig) -> DaoResult<Vec<ProposalUtxo>> {
|
async fn list_proposals(&self, cfg: &DaoConfig) -> DaoResult<Vec<ProposalUtxo>> {
|
||||||
// Proposals live at the proposal script address, which is derived
|
let proposal_addr = cfg.proposal_addr.as_deref().ok_or_else(|| {
|
||||||
// from the Agora deployment + gov-token-policy parameters. We
|
DaoError::Config(
|
||||||
// don't compute that derivation in Phase 1 (it lands in Phase 4
|
"DaoConfig.proposal_addr missing — register the DAO with proposal_addr \
|
||||||
// alongside reference_scripts.rs). For now: return empty + a
|
or run dao_discover_scripts first"
|
||||||
// tracked-todo string. Real wiring: decode proposal script
|
.into(),
|
||||||
// hash → bech32 → call address_info → filter to inline-datum
|
)
|
||||||
// UTxOs → decode ProposalDatum.
|
})?;
|
||||||
Err(DaoError::State(
|
let infos = self.address_info(proposal_addr).await?;
|
||||||
"list_proposals pending Phase 4 (proposal script address discovery)"
|
let utxos = infos
|
||||||
.into(),
|
.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)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct KoiosAsset {
|
struct KoiosAsset {
|
||||||
policy_id: String,
|
policy_id: String,
|
||||||
#[allow(dead_code)]
|
#[serde(default)]
|
||||||
asset_name: Option<String>,
|
asset_name: Option<String>,
|
||||||
quantity: String,
|
quantity: String,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1832,34 +1832,25 @@ impl WalletService {
|
||||||
"DaoConfig.proposal_addr missing — register or discover_scripts first".to_string()
|
"DaoConfig.proposal_addr missing — register or discover_scripts first".to_string()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Find this proposal among the UTxOs at proposal_addr. Use the
|
// Find this proposal among the UTxOs at proposal_addr. Goes
|
||||||
// dedicated `pull_proposal_utxos` helper which decodes inline
|
// through the DaoReader which decodes inline datums + filters
|
||||||
// datums (the regular `chain.get_utxos` doesn't surface inline
|
// out orphan/non-proposal UTxOs.
|
||||||
// datum bytes).
|
let proposals = self
|
||||||
let mut found: Option<(
|
.inner
|
||||||
String, // tx_hash
|
.dao_reader
|
||||||
u32, // output_index
|
.list_proposals(&cfg)
|
||||||
u64, // lovelace
|
.await
|
||||||
String, // proposal_st asset name hex
|
.map_err(|e| e.to_string())?;
|
||||||
aldabra_dao::agora::proposal::ProposalDatum,
|
let target = proposals
|
||||||
)> = None;
|
.into_iter()
|
||||||
let pulled = pull_proposal_utxos(&self.inner.koios_base, proposal_addr).await?;
|
.find(|p| p.datum.proposal_id == proposal_id)
|
||||||
for p in pulled {
|
.ok_or_else(|| {
|
||||||
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(|| {
|
|
||||||
format!("no proposal with proposal_id={} found at {}", proposal_id, proposal_addr)
|
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.
|
// Find the voter's stake.
|
||||||
let voter_pkh = self.wallet_pkh()?;
|
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))
|
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.
|
/// Render a [`aldabra_dao::reader::StakeUtxo`] as a JSON object for tool output.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue