feat(dao): dao_discover_scripts MCP tool + Koios discovery client

New `aldabra-dao::discovery` module:
- `DiscoveryClient` trait + `KoiosDiscoveryClient` impl
- `discover_scripts(cfg, client, deployers)` — auto-finds:
  - governor_validator_ref + stake_validator_ref via deployer ref-script search
  - stake_st_policy from any existing stake UTxO (gov-token + non-gov-token asset)
  - stake_st_policy_ref via deployer search
- `apply_discovery(cfg, report)` — merges into DaoConfig (never overwrites)
- `script_hash_from_addr(bech32)` — extract 28-byte script hash from a script address

New MCP tool:
- `dao_discover_scripts { dao?, extra_deployers? }` — runs the audit logic
  against any registered DAO + persists the discovered fields back to the
  DaoConfig. Returns JSON with what was found + a gaps list for things
  v1 can't auto-discover (proposal_addr, proposal_st_policy).

Plus 4 unit tests with stub Koios responses validating the full pipeline:
script-hash extraction, StakeST discovery from stake UTxO assets,
validator ref-utxo matching at deployer, apply_discovery merge semantics.

WalletInner now caches `koios_base` so the discovery client can be
constructed on demand without re-passing the URL through args.
This commit is contained in:
Kayos 2026-05-05 20:14:13 -07:00
parent 5913b9266a
commit edd1948dec
3 changed files with 602 additions and 1 deletions

View file

@ -0,0 +1,531 @@
//! Auto-discover Agora script hashes + reference UTxO refs from on-chain state.
//!
//! Closes the "user has to research and hand-populate ScriptRefs" gap by
//! running the same Koios queries the human audit at
//! `memory/audit-sulkta-agora-2026-05-05.md` performed.
//!
//! ## What we discover from the existing config
//!
//! Given a `DaoConfig` with at minimum {governor_addr, stakes_addr,
//! treasury_addr, gov_token_policy, gov_token_name_hex} we can find:
//!
//! - **governor_validator_ref** — search known deployer addresses for a UTxO
//! whose `reference_script.hash` matches the script hash extracted from
//! `governor_addr`'s bech32.
//! - **stake_validator_ref** — same pattern against `stakes_addr`'s hash.
//! - **stake_st_policy + stake_st_policy_ref** — look at any existing stake
//! UTxO at `stakes_addr` (filtered to those holding the gov token); the
//! other asset on the UTxO is the StakeST. Then locate its ref-utxo at
//! the deployer.
//!
//! ## What we DON'T cover in v1
//!
//! - **proposal_addr / ProposalST policy** — for v1 the user provides these
//! explicitly. Discovery would require walking governor txs (CreateProposal
//! spend → output at proposal_addr), which is a heavier lift. Phase 4b.
//! - **treasury_validator_ref** — Sulkta's treasury validator wasn't found at
//! the shared deployer per the audit. Possibly deployed elsewhere or not
//! yet on chain. Phase 4c when treasury-spend ships.
//!
//! ## Deployer addresses to search
//!
//! MLabs's shared Agora deployer at `addr1w9gexmeunzsykesf42d4eqet5yvzeap6trjnflxqtkcf66g5740fw`
//! (mainnet) is the standard for Clarity-deployed DAOs. We probe the
//! addresses provided by the caller as additional deployer candidates;
//! by default we just probe the MLabs shared one.
use serde::Deserialize;
use crate::config::{DaoConfig, ScriptRefs};
use crate::error::{DaoError, DaoResult};
/// Standard MLabs shared Agora deployer on mainnet. Hosts the parameterized
/// validators + minting policies for many Clarity DAOs (Indigo, SundaeSwap,
/// Sulkta, etc).
pub const MAINNET_AGORA_SHARED_DEPLOYER: &str =
"addr1w9gexmeunzsykesf42d4eqet5yvzeap6trjnflxqtkcf66g5740fw";
/// Trait for the chain reads we need. Lets tests stub Koios responses.
#[async_trait::async_trait]
pub trait DiscoveryClient: Send + Sync {
async fn address_info(&self, address: &str) -> DaoResult<Vec<AddressInfo>>;
}
/// Koios-backed [`DiscoveryClient`] for production use.
///
/// Mirrors the `KoiosDaoReader` shape — separate client because the trait
/// surface is different and we don't want to entangle Phase 1 reads with
/// Phase 4-prep discovery.
pub struct KoiosDiscoveryClient {
base_url: String,
http: reqwest::Client,
}
impl KoiosDiscoveryClient {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
base_url: base_url.into(),
http: reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("reqwest client"),
}
}
}
#[async_trait::async_trait]
impl DiscoveryClient for KoiosDiscoveryClient {
async fn address_info(&self, address: &str) -> DaoResult<Vec<AddressInfo>> {
let url = format!("{}/address_info", self.base_url);
let body = serde_json::json!({ "_addresses": [address] });
let resp = self
.http
.post(url)
.json(&body)
.send()
.await
.map_err(|e| DaoError::Backend(format!("address_info {address}: {e}")))?;
if !resp.status().is_success() {
return Err(DaoError::Backend(format!(
"address_info {address}: HTTP {}",
resp.status()
)));
}
resp.json::<Vec<AddressInfo>>()
.await
.map_err(|e| DaoError::Backend(format!("address_info {address} parse: {e}")))
}
}
/// Subset of Koios `address_info` JSON we need.
#[derive(Debug, Deserialize, Clone)]
pub struct AddressInfo {
#[allow(dead_code)]
pub address: String,
pub utxo_set: Vec<AddressUtxo>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct AddressUtxo {
pub tx_hash: String,
pub tx_index: u32,
#[serde(default)]
pub asset_list: Option<Vec<UtxoAsset>>,
#[serde(default)]
pub reference_script: Option<RefScript>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct UtxoAsset {
pub policy_id: String,
pub asset_name: String,
pub quantity: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct RefScript {
pub hash: String,
#[allow(dead_code)]
#[serde(rename = "type")]
pub kind: Option<String>,
#[allow(dead_code)]
pub size: Option<u32>,
}
/// What `discover_scripts` filled in vs. left blank.
#[derive(Debug, Clone, Default)]
pub struct DiscoveryReport {
pub governor_validator_ref: Option<String>,
pub stake_validator_ref: Option<String>,
pub stake_st_policy: Option<String>,
pub stake_st_policy_ref: Option<String>,
/// Things we couldn't auto-find. Show these to the user so they know
/// what to provide manually before running write tools.
pub gaps: Vec<String>,
}
/// Decode a bech32 script address → 28-byte script-hash hex.
pub fn script_hash_from_addr(bech32: &str) -> DaoResult<String> {
use bech32::FromBase32;
let (_hrp, data, _variant) =
bech32::decode(bech32).map_err(|e| DaoError::Address(format!("bech32 decode: {e}")))?;
let bytes = Vec::<u8>::from_base32(&data)
.map_err(|e| DaoError::Address(format!("bech32 base32: {e}")))?;
if bytes.len() < 29 {
return Err(DaoError::Address(format!(
"address too short ({} bytes)",
bytes.len()
)));
}
// First byte = network/type header; next 28 = script hash.
Ok(hex::encode(&bytes[1..29]))
}
/// Auto-discover the script-ref UTxOs + StakeST policy for a DAO.
///
/// Caller supplies a [`DiscoveryClient`] (typically a Koios wrapper) and the
/// config to inspect. Returns a [`DiscoveryReport`] with whatever was found
/// + a list of `gaps` (things the user must still provide manually).
pub async fn discover_scripts(
cfg: &DaoConfig,
client: &dyn DiscoveryClient,
deployer_addresses: &[&str],
) -> DaoResult<DiscoveryReport> {
let mut report = DiscoveryReport::default();
// 1. Validator script hashes from address bech32.
let governor_hash = script_hash_from_addr(&cfg.governor_addr)?;
let stakes_hash = script_hash_from_addr(&cfg.stakes_addr)?;
// 2. StakeST policy from any stake UTxO at stakes_addr.
//
// A stake UTxO carries (gov_token, qty) + (stake_st_token, 1). Filter to
// ones that have BOTH our gov-token AND a non-gov-token asset; the
// non-gov-token's policy_id is the StakeST policy.
match client.address_info(&cfg.stakes_addr).await {
Ok(infos) => {
let utxos = infos.into_iter().next().map(|i| i.utxo_set).unwrap_or_default();
let mut found_stake_st = None;
for u in &utxos {
let assets = match &u.asset_list {
Some(a) => a,
None => continue,
};
let has_gov = assets
.iter()
.any(|a| a.policy_id == cfg.gov_token_policy);
if !has_gov {
continue;
}
if let Some(other) = assets.iter().find(|a| a.policy_id != cfg.gov_token_policy) {
found_stake_st = Some(other.policy_id.clone());
break;
}
}
if let Some(p) = found_stake_st {
report.stake_st_policy = Some(p);
} else {
report.gaps.push(
"stake_st_policy: no stakes-addr UTxO carries (gov_token + StakeST) — \
either no stakes exist yet OR stakes_addr is wrong"
.into(),
);
}
}
Err(e) => report
.gaps
.push(format!("stake_st_policy: address_info failed for stakes_addr: {e}")),
}
// 3. Reference-script UTxOs at the deployers.
//
// For each deployer address, fetch its UTxO set. Iterate UTxOs, match
// each `reference_script.hash` against our targets:
// - governor validator → governor_validator_ref
// - stake validator → stake_validator_ref
// - stake_st policy → stake_st_policy_ref (if we found the policy)
let stake_st_target = report.stake_st_policy.clone();
for &deployer in deployer_addresses.iter() {
let infos = match client.address_info(deployer).await {
Ok(v) => v,
Err(e) => {
report.gaps.push(format!(
"deployer {deployer} probe failed: {e}"
));
continue;
}
};
let utxos = infos.into_iter().next().map(|i| i.utxo_set).unwrap_or_default();
for u in &utxos {
let rs = match &u.reference_script {
Some(r) => r,
None => continue,
};
let utxo_ref = format!("{}#{}", u.tx_hash, u.tx_index);
if rs.hash == governor_hash && report.governor_validator_ref.is_none() {
report.governor_validator_ref = Some(utxo_ref.clone());
}
if rs.hash == stakes_hash && report.stake_validator_ref.is_none() {
report.stake_validator_ref = Some(utxo_ref.clone());
}
if let Some(ref target) = stake_st_target {
if &rs.hash == target && report.stake_st_policy_ref.is_none() {
report.stake_st_policy_ref = Some(utxo_ref.clone());
}
}
}
}
if report.governor_validator_ref.is_none() {
report.gaps.push(format!(
"governor_validator_ref: hash {} not found at any provided deployer address",
&governor_hash[..16]
));
}
if report.stake_validator_ref.is_none() {
report.gaps.push(format!(
"stake_validator_ref: hash {} not found at any provided deployer address",
&stakes_hash[..16]
));
}
if report.stake_st_policy.is_some() && report.stake_st_policy_ref.is_none() {
report.gaps.push(
"stake_st_policy_ref: policy id discovered but no ref-utxo found at deployers"
.into(),
);
}
// Always add gaps for things v1 doesn't auto-discover.
if cfg.proposal_addr.is_none() {
report
.gaps
.push("proposal_addr: not auto-discovered in v1; provide via dao_register".into());
}
if cfg.proposal_st_policy.is_none() {
report.gaps.push(
"proposal_st_policy: not auto-discovered in v1; provide via dao_register".into(),
);
}
Ok(report)
}
/// Merge a [`DiscoveryReport`] into a [`DaoConfig`], filling in any field
/// the report discovered. Returns the merged config — caller persists.
pub fn apply_discovery(cfg: &mut DaoConfig, report: &DiscoveryReport) {
if let Some(p) = &report.stake_st_policy {
if cfg.stake_st_policy.is_none() {
cfg.stake_st_policy = Some(p.clone());
}
}
if let Some(r) = &report.governor_validator_ref {
if cfg.script_refs.governor_validator.is_none() {
cfg.script_refs.governor_validator = Some(r.clone());
}
}
if let Some(r) = &report.stake_validator_ref {
if cfg.script_refs.stake_validator.is_none() {
cfg.script_refs.stake_validator = Some(r.clone());
}
}
if let Some(r) = &report.stake_st_policy_ref {
if cfg.script_refs.stake_st_policy.is_none() {
cfg.script_refs.stake_st_policy = Some(r.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_script_hash_from_governor_addr() {
let h = script_hash_from_addr("addr1w8v73wfrru7smn738k6c5xafqvl2tgsvct7dtztc4jwlf4c35jnmy")
.unwrap();
assert_eq!(h, "d9e8b9231f3d0dcfd13db58a1ba9033ea5a20cc2fcd58978ac9df4d7");
}
#[test]
fn extracts_script_hash_from_real_stakes_addr() {
let h =
script_hash_from_addr("addr1w8msu7psehjlcu5glzjgjtq53m4mk75c9d2p8hfjwyknmfqskkah8")
.unwrap();
assert_eq!(h, "f70e7830cde5fc7288f8a4892c148eebbb7a982b5413dd32712d3da4");
}
#[test]
fn extracts_script_hash_from_treasury_addr() {
let h = script_hash_from_addr("addr1wx2xrpft9f97ggz4u5yrkev4u340fzfsrqama95kp8l2v6qll696y")
.unwrap();
assert_eq!(h, "9461852b2a4be42055e5083b6595e46af48930183bbe969609fea668");
}
/// Stub client returning canned address_info for testing the discovery
/// pipeline without hitting Koios.
struct StubClient {
responses: std::collections::HashMap<String, Vec<AddressInfo>>,
}
#[async_trait::async_trait]
impl DiscoveryClient for StubClient {
async fn address_info(&self, address: &str) -> DaoResult<Vec<AddressInfo>> {
Ok(self
.responses
.get(address)
.cloned()
.unwrap_or_default())
}
}
fn sulkta_cfg() -> DaoConfig {
DaoConfig {
name: "sulkta".into(),
description: None,
governor_addr: "addr1w8v73wfrru7smn738k6c5xafqvl2tgsvct7dtztc4jwlf4c35jnmy".into(),
stakes_addr: "addr1w8msu7psehjlcu5glzjgjtq53m4mk75c9d2p8hfjwyknmfqskkah8".into(),
treasury_addr: "addr1wx2xrpft9f97ggz4u5yrkev4u340fzfsrqama95kp8l2v6qll696y".into(),
gov_token_policy: "9c4bd4a90cdb73d9ff681215ecf7dea9fb183d916d30487d17098e05".into(),
gov_token_name_hex: "546572726170696e".into(),
initial_spend: "5a7e33f6c399a09b74d607397a20b105e6e1462dd67c6569fa7549eafcfe8cc5#0"
.into(),
max_cosigners: 5,
treasury_ref_config: "d9a1cdac8d196ad9303e0faedba992ff31f5bc2186740c362686cfad".into(),
network: crate::config::DaoNetwork::Mainnet,
proposal_addr: None,
stake_st_policy: None,
proposal_st_policy: None,
script_refs: ScriptRefs::default(),
}
}
#[tokio::test]
async fn discovers_stake_st_from_existing_stake() {
let cfg = sulkta_cfg();
let mut responses = std::collections::HashMap::new();
// A fake stake UTxO at stakes_addr carrying gov-token + StakeST.
responses.insert(
cfg.stakes_addr.clone(),
vec![AddressInfo {
address: cfg.stakes_addr.clone(),
utxo_set: vec![AddressUtxo {
tx_hash: "deadbeef".repeat(8),
tx_index: 0,
asset_list: Some(vec![
UtxoAsset {
policy_id: cfg.gov_token_policy.clone(),
asset_name: cfg.gov_token_name_hex.clone(),
quantity: "50".into(),
},
UtxoAsset {
policy_id: "732ff23ade752d46c903c16866b0cb3e2e977216db594bb47c434696".into(),
asset_name: "f70e7830cde5fc7288f8a4892c148eebbb7a982b5413dd32712d3da4".into(),
quantity: "1".into(),
},
]),
reference_script: None,
}],
}],
);
// Empty deployer for this test — we just want StakeST policy id.
responses.insert(
MAINNET_AGORA_SHARED_DEPLOYER.into(),
vec![AddressInfo {
address: MAINNET_AGORA_SHARED_DEPLOYER.into(),
utxo_set: vec![],
}],
);
let client = StubClient { responses };
let report = discover_scripts(&cfg, &client, &[MAINNET_AGORA_SHARED_DEPLOYER])
.await
.unwrap();
assert_eq!(
report.stake_st_policy.as_deref(),
Some("732ff23ade752d46c903c16866b0cb3e2e977216db594bb47c434696")
);
}
#[tokio::test]
async fn finds_validator_refs_at_deployer() {
let cfg = sulkta_cfg();
let governor_hash = "d9e8b9231f3d0dcfd13db58a1ba9033ea5a20cc2fcd58978ac9df4d7";
let stake_hash = "f70e7830cde5fc7288f8a4892c148eebbb7a982b5413dd32712d3da4";
let mut responses = std::collections::HashMap::new();
responses.insert(cfg.stakes_addr.clone(), vec![AddressInfo {
address: cfg.stakes_addr.clone(),
utxo_set: vec![],
}]);
responses.insert(
MAINNET_AGORA_SHARED_DEPLOYER.into(),
vec![AddressInfo {
address: MAINNET_AGORA_SHARED_DEPLOYER.into(),
utxo_set: vec![
// Governor validator ref
AddressUtxo {
tx_hash: "479b8203c8b84ce20bb0a1c6ee1f527f122d1ce3e655dfc54635061eff622aea"
.into(),
tx_index: 3,
asset_list: None,
reference_script: Some(RefScript {
hash: governor_hash.into(),
kind: Some("plutusV2".into()),
size: Some(7213),
}),
},
// Stake validator ref
AddressUtxo {
tx_hash: "479b8203c8b84ce20bb0a1c6ee1f527f122d1ce3e655dfc54635061eff622aea"
.into(),
tx_index: 2,
asset_list: None,
reference_script: Some(RefScript {
hash: stake_hash.into(),
kind: Some("plutusV2".into()),
size: Some(5182),
}),
},
// Random other ref-utxo (different DAO's script — should be ignored)
AddressUtxo {
tx_hash: "0000000000000000000000000000000000000000000000000000000000000000"
.into(),
tx_index: 0,
asset_list: None,
reference_script: Some(RefScript {
hash: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffff".into(),
kind: Some("plutusV2".into()),
size: Some(1024),
}),
},
],
}],
);
let client = StubClient { responses };
let report = discover_scripts(&cfg, &client, &[MAINNET_AGORA_SHARED_DEPLOYER])
.await
.unwrap();
assert_eq!(
report.governor_validator_ref.as_deref(),
Some("479b8203c8b84ce20bb0a1c6ee1f527f122d1ce3e655dfc54635061eff622aea#3")
);
assert_eq!(
report.stake_validator_ref.as_deref(),
Some("479b8203c8b84ce20bb0a1c6ee1f527f122d1ce3e655dfc54635061eff622aea#2")
);
}
#[test]
fn apply_discovery_merges_into_config() {
let mut cfg = sulkta_cfg();
let report = DiscoveryReport {
governor_validator_ref: Some("aa#1".into()),
stake_validator_ref: Some("bb#2".into()),
stake_st_policy: Some("ccdd".into()),
stake_st_policy_ref: Some("ee#0".into()),
gaps: vec![],
};
apply_discovery(&mut cfg, &report);
assert_eq!(cfg.script_refs.governor_validator.as_deref(), Some("aa#1"));
assert_eq!(cfg.script_refs.stake_validator.as_deref(), Some("bb#2"));
assert_eq!(cfg.stake_st_policy.as_deref(), Some("ccdd"));
assert_eq!(cfg.script_refs.stake_st_policy.as_deref(), Some("ee#0"));
}
#[test]
fn apply_discovery_doesnt_overwrite_existing() {
let mut cfg = sulkta_cfg();
cfg.stake_st_policy = Some("preexisting".into());
let report = DiscoveryReport {
stake_st_policy: Some("would_overwrite".into()),
..Default::default()
};
apply_discovery(&mut cfg, &report);
assert_eq!(cfg.stake_st_policy.as_deref(), Some("preexisting"));
}
}

View file

@ -28,6 +28,7 @@
pub mod agora;
pub mod builder;
pub mod config;
pub mod discovery;
pub mod error;
pub mod reader;

View file

@ -35,6 +35,9 @@ use aldabra_dao::builder::proposal_create::{
WalletUtxo as DaoWalletUtxo,
};
use aldabra_dao::config::{DaoConfig, DaoNetwork, DaoStore, ScriptRefs};
use aldabra_dao::discovery::{
apply_discovery, discover_scripts, KoiosDiscoveryClient, MAINNET_AGORA_SHARED_DEPLOYER,
};
use aldabra_dao::reader::{DaoReader, KoiosDaoReader};
use aldabra_core::plutus_cost_models::PLUTUS_V3_COST_MODEL_PREPROD;
use aldabra_core::{
@ -90,6 +93,9 @@ struct WalletInner {
/// Reader-only Koios client for DAO-shape queries. Reuses the
/// koios_base; separate from `chain` so the trait surface stays clean.
dao_reader: KoiosDaoReader,
/// Cached Koios base url so `dao_discover_scripts` can spin up a
/// `KoiosDiscoveryClient` on demand without a re-construction call.
koios_base: String,
}
impl WalletService {
@ -111,7 +117,8 @@ impl WalletService {
stake_key,
max_send_lovelace,
dao_store: DaoStore::new(&data_dir),
dao_reader: KoiosDaoReader::new(koios_base),
dao_reader: KoiosDaoReader::new(koios_base.clone()),
koios_base,
}),
}
}
@ -1503,6 +1510,56 @@ impl WalletService {
Ok(serde_json::json!({ "dao": cfg.name, "stakes": arr }).to_string())
}
#[tool(
name = "dao_discover_scripts",
description = "Auto-populate a DAO's ScriptRefs + StakeST policy by inspecting on-chain state. v1 fills in: governor_validator_ref, stake_validator_ref, stake_st_policy, stake_st_policy_ref. proposal_addr + proposal_st_policy still require manual entry (v1 limitation). Searches the MLabs shared Agora deployer (`addr1w9gexmeunzsy...`) by default; pass extra deployer addresses if your DAO's scripts live elsewhere. Args: dao (optional), extra_deployers (optional list of bech32). Returns JSON {discovered, gaps, updated_config}."
)]
async fn dao_discover_scripts(
&self,
#[tool(aggr)] DaoDiscoverArgs {
dao,
extra_deployers,
}: DaoDiscoverArgs,
) -> Result<String, String> {
let mut cfg = self
.inner
.dao_store
.resolve(dao.as_deref())
.map_err(|e| e.to_string())?;
// Build the deployer search list: MLabs's shared one + any caller-supplied.
let extra: Vec<String> = extra_deployers.unwrap_or_default();
let mut deployers: Vec<&str> = vec![MAINNET_AGORA_SHARED_DEPLOYER];
deployers.extend(extra.iter().map(|s| s.as_str()));
// Use the same Koios base URL as the wallet's chain backend.
let client = KoiosDiscoveryClient::new(self.inner.koios_base.clone());
let report = discover_scripts(&cfg, &client, &deployers)
.await
.map_err(|e| e.to_string())?;
apply_discovery(&mut cfg, &report);
// Persist.
self.inner
.dao_store
.register(&cfg)
.map_err(|e| e.to_string())?;
Ok(serde_json::json!({
"dao": cfg.name,
"discovered": {
"stake_st_policy": report.stake_st_policy,
"governor_validator_ref": report.governor_validator_ref,
"stake_validator_ref": report.stake_validator_ref,
"stake_st_policy_ref": report.stake_st_policy_ref,
},
"gaps": report.gaps,
"config_after": cfg,
})
.to_string())
}
#[tool(
name = "dao_proposal_create_unsigned",
description = "Build (but DO NOT submit) an unsigned proposal-creation tx for the given DAO. Returns the CBOR-hex of the unsigned tx body + the new proposal_id. Currently supports InfoOnly proposals only — TreasuryWithdrawal effect path lands in Phase 4c. Caller signs via wallet_sign_partial then submits via wallet_submit_signed_tx. Args: dao (optional — defaults to active), fee_lovelace (suggested ~3_000_000 for v1; refine via koios tx_evaluate), starting_time_ms (POSIX millis to embed in ProposalDatum.starting_time; pass current chain tip's slot * 1000 + epoch start)."
@ -1739,6 +1796,18 @@ pub struct DaoShowArgs {
pub dao: Option<String>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct DaoDiscoverArgs {
/// Named DAO. Falls through to active if omitted.
#[serde(default)]
pub dao: Option<String>,
/// Extra deployer addresses to search (bech32) on top of the
/// default MLabs shared deployer. Useful for DAOs whose scripts
/// were deployed to a private address.
#[serde(default)]
pub extra_deployers: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct DaoProposalCreateArgs {
/// Named DAO. Falls through to active if omitted.