use crate::tools::ClosureTool;
use super::bounty::bounty_signers;
use super::guild::{format_lh, resolve_account};
const PROPOSAL_DEFAULT_PERIOD_HOURS: f64 = 48.0;
pub(crate) fn propose_measure_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"guild_id": {
"type": "integer",
"minimum": 0,
"description": "The id of the guild whose treasury the proposal would spend from."
},
"to": {
"type": "string",
"description": "Spend recipient if the proposal passes — a raw 0x… \
address OR a subdomain name (resolved to that name's on-chain owner)."
},
"amount_lh": {
"type": "string",
"description": "Amount of $LH the proposal would pay out from the \
treasury, as a decimal string (\"5\", \"1.5\"). Must be > 0."
},
"memo": {
"type": "string",
"description": "OPTIONAL description of what the spend is for — recorded \
on-chain so voters know what they're approving."
},
"period_hours": {
"type": "string",
"description": "OPTIONAL voting window in hours (decimal). Omit for the \
48h default. Members can vote until the deadline; only then can a \
passing proposal be executed."
}
},
"required": ["guild_id", "to", "amount_lh"]
});
ClosureTool::new(
"propose_measure",
"Open a DAO governance proposal to spend $LH from a guild's pooled treasury: \
members vote for/against, and a passing proposal can be executed after its \
deadline. Use this to run a guild's spending democratically (instead of an \
Admin spending unilaterally). Returns { proposal_id, guild_id, to, resolved_to, \
amount_lh, period_hours, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let guild_id = args
.get("guild_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| crate::error::Error::other("guild_id is required"))?;
let to_arg = args.get("to").and_then(|v| v.as_str()).unwrap_or("").trim().to_string();
if to_arg.is_empty() {
return Err(crate::error::Error::other("to cannot be empty"));
}
let amount_arg = args
.get("amount_lh")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let amount_wei = crate::encoding::parse_token_amount(&amount_arg).ok_or_else(|| {
crate::error::Error::other(format!(
"could not parse amount_lh \"{amount_arg}\" — pass a decimal $LH \
figure like \"5\" or \"1.5\""
))
})?;
if amount_wei == 0 {
return Err(crate::error::Error::other("amount_lh must be greater than 0"));
}
let memo = args.get("memo").and_then(|v| v.as_str()).unwrap_or("").trim();
let period_hours: f64 = match args.get("period_hours").and_then(|v| v.as_str()) {
Some(s) if !s.trim().is_empty() => s
.trim()
.parse::<f64>()
.map_err(|_| crate::error::Error::other("period_hours must be a number"))?,
_ => PROPOSAL_DEFAULT_PERIOD_HOURS,
};
if period_hours <= 0.0 {
return Err(crate::error::Error::other("period_hours must be greater than 0"));
}
let period_secs = (period_hours * 3600.0) as u64;
let to_hex = resolve_account(&to_arg).await?;
let (signer, fee_payer) = bounty_signers().await?;
let tx_hash = crate::app::registry::propose_sponsored(
&signer,
&fee_payer,
guild_id,
&to_hex,
amount_wei,
memo.as_bytes(),
period_secs,
crate::app::registry::ALPHA_USD_ADDRESS,
)
.await
.map_err(|e| crate::error::Error::other(format!("propose_measure failed: {e}")))?;
let proposal_id = crate::app::registry::proposals_of(guild_id, 0, 256)
.await
.ok()
.and_then(|ids| ids.last().copied());
let mut result = serde_json::json!({
"guild_id": guild_id,
"to": to_arg,
"resolved_to": to_hex,
"amount_lh": amount_arg,
"period_hours": period_hours,
"tx_hash": tx_hash,
});
if let Some(id) = proposal_id {
result["proposal_id"] = serde_json::json!(id);
}
Ok(result)
},
)
}
pub(crate) fn cast_vote_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"proposal_id": {
"type": "integer",
"minimum": 0,
"description": "The id of the open proposal to vote on (from list_proposals)."
},
"support": {
"type": "boolean",
"description": "true to vote FOR the proposal, false to vote AGAINST it."
}
},
"required": ["proposal_id", "support"]
});
ClosureTool::new(
"cast_vote",
"Cast a vote on an open guild governance proposal: `support` true is a vote FOR, \
false is AGAINST. One vote per member per proposal. Returns { proposal_id, \
support, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let proposal_id = args
.get("proposal_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| crate::error::Error::other("proposal_id is required"))?;
let support = args
.get("support")
.and_then(|v| v.as_bool())
.ok_or_else(|| crate::error::Error::other("support (true/false) is required"))?;
let (signer, fee_payer) = bounty_signers().await?;
let tx_hash = crate::app::registry::vote_sponsored(
&signer,
&fee_payer,
proposal_id,
support,
crate::app::registry::ALPHA_USD_ADDRESS,
)
.await
.map_err(|e| crate::error::Error::other(format!("cast_vote failed: {e}")))?;
Ok(serde_json::json!({
"proposal_id": proposal_id,
"support": support,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn execute_proposal_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"proposal_id": {
"type": "integer",
"minimum": 0,
"description": "The id of a passed proposal whose voting deadline has \
elapsed (executing it pays out the treasury spend)."
}
},
"required": ["proposal_id"]
});
ClosureTool::new(
"execute_proposal",
"Execute a guild governance proposal that PASSED, after its voting deadline has \
elapsed — this RELEASES the $LH spend from the guild treasury to the proposed \
recipient. The on-chain facet reverts if the proposal didn't pass or the \
deadline hasn't elapsed. Moves value. Returns { proposal_id, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let proposal_id = args
.get("proposal_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| crate::error::Error::other("proposal_id is required"))?;
let (signer, fee_payer) = bounty_signers().await?;
let tx_hash = crate::app::registry::execute_proposal_sponsored(
&signer,
&fee_payer,
proposal_id,
crate::app::registry::ALPHA_USD_ADDRESS,
)
.await
.map_err(|e| crate::error::Error::other(format!("execute_proposal failed: {e}")))?;
Ok(serde_json::json!({
"proposal_id": proposal_id,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn list_proposals_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"guild_id": {
"type": "integer",
"minimum": 0,
"description": "The id of the guild whose proposals to list."
}
},
"required": ["guild_id"]
});
ClosureTool::new(
"list_proposals",
"List a guild's governance proposals — each with its id, spend recipient, $LH \
amount, status (open/executed/defeated/cancelled), voting deadline, and \
for/against tally. Read-only. Use this to see what's up for a vote before \
cast_vote / execute_proposal. Returns { proposals: [ { proposal_id, to, \
amount_lh, status, deadline, votes_for, votes_against } ], count }.",
schema,
|args: serde_json::Value, _ctx| async move {
let guild_id = args
.get("guild_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| crate::error::Error::other("guild_id is required"))?;
let ids = crate::app::registry::proposals_of(guild_id, 0, 256)
.await
.map_err(crate::error::Error::other)?;
let mut proposals = Vec::new();
for id in ids {
let Ok(p) = crate::app::registry::get_proposal(id).await else {
continue;
};
let (votes_for, votes_against) = crate::app::registry::tally_of(id)
.await
.map(|t| (t.for_votes, t.against_votes))
.unwrap_or((0, 0));
proposals.push(serde_json::json!({
"proposal_id": id,
"to": p.to,
"amount_lh": format_lh(p.amount),
"status": p.status_label(),
"deadline": p.deadline,
"votes_for": votes_for,
"votes_against": votes_against,
}));
}
Ok(serde_json::json!({
"count": proposals.len(),
"proposals": proposals,
}))
},
)
}