use wasm_bindgen::prelude::*;
use crate::app::{dom, templates};
const PROPOSAL_LIST_LIMIT: u64 = 50;
const PROPOSAL_DEFAULT_PERIOD_HOURS: u64 = 48;
pub(super) fn load_proposals_pressed() {
let Some(guild_id) = dom::input_by_id("governance-guild")
.map(|i| i.value().trim().to_string())
.and_then(|s| s.parse::<u64>().ok())
else {
return;
};
wasm_bindgen_futures::spawn_local(async move {
refresh_governance_list(guild_id).await;
});
}
pub(super) fn propose_measure_pressed() {
let guild_raw = dom::input_by_id("governance-guild")
.map(|i| i.value().trim().to_string())
.unwrap_or_default();
let to = dom::input_by_id("governance-to")
.map(|i| i.value().trim().to_string())
.unwrap_or_default();
let amount_raw = dom::input_by_id("governance-amount")
.map(|i| i.value())
.unwrap_or_default();
let period_raw = dom::input_by_id("governance-period")
.map(|i| i.value().trim().to_string())
.unwrap_or_default();
let Ok(guild_id) = guild_raw.parse::<u64>() else {
return;
};
if to.is_empty() {
return;
}
let Some(amount_wei) = crate::encoding::parse_token_amount(&amount_raw) else {
return;
};
if amount_wei == 0 {
return;
}
let period_secs = if period_raw.is_empty() {
PROPOSAL_DEFAULT_PERIOD_HOURS * 3600
} else {
match period_raw.parse::<u64>() {
Ok(h) if h > 0 => h * 3600,
_ => return,
}
};
let amount_label: String = amount_raw
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.')
.collect();
dom::swap_inner(
"governance-result",
"<span style=\"color:var(--muted)\">proposing…</span>",
);
wasm_bindgen_futures::spawn_local(async move {
let result = async {
super::sponsor_rate_guard()?;
let to_hex = resolve_governance_recipient(&to).await?;
let (signer, _) = crate::app::chat::credit_signer()
.await
.ok_or_else(|| "no identity".to_string())?;
let fee_payer = crate::app::sponsor::signer()?;
crate::app::registry::propose_sponsored(
&signer,
&fee_payer,
guild_id,
&to_hex,
amount_wei,
&[],
period_secs,
crate::app::registry::ALPHA_USD_ADDRESS,
)
.await
}
.await;
match result {
Ok(_) => {
let new_id = crate::app::registry::proposal_count()
.await
.unwrap_or(0);
dom::swap_inner(
"governance-result",
&templates::governance_result_panel(new_id, &amount_label).into_string(),
);
refresh_governance_list(guild_id).await;
}
Err(e) => {
web_sys::console::warn_1(&JsValue::from_str(&format!("propose measure: {e}")));
dom::swap_inner(
"governance-result",
&dom::msg_span(dom::Msg::Error, "proposal couldn't be opened"),
);
}
}
});
}
pub(super) fn vote_pressed(arg: String) {
let mut parts = arg.splitn(2, ':');
let Some(proposal_id) = parts.next().and_then(|s| s.trim().parse::<u64>().ok()) else {
return;
};
let support = matches!(parts.next().map(|s| s.trim()), Some("for"));
let guild_id = dom::input_by_id("governance-guild")
.map(|i| i.value().trim().to_string())
.and_then(|s| s.parse::<u64>().ok());
dom::swap_inner(
"governance-result",
"<span style=\"color:var(--muted)\">voting…</span>",
);
wasm_bindgen_futures::spawn_local(async move {
let result = async {
super::sponsor_rate_guard()?;
let (signer, _) = crate::app::chat::credit_signer()
.await
.ok_or_else(|| "no identity".to_string())?;
let fee_payer = crate::app::sponsor::signer()?;
crate::app::registry::vote_sponsored(
&signer,
&fee_payer,
proposal_id,
support,
crate::app::registry::ALPHA_USD_ADDRESS,
)
.await
}
.await;
match result {
Ok(_) => {
dom::swap_inner(
"governance-result",
&dom::msg_span(dom::Msg::Muted, "vote recorded"),
);
if let Some(g) = guild_id {
refresh_governance_list(g).await;
}
}
Err(e) => {
web_sys::console::warn_1(&JsValue::from_str(&format!("vote: {e}")));
dom::swap_inner(
"governance-result",
&dom::msg_span(dom::Msg::Error, "couldn't record that vote"),
);
}
}
});
}
pub(super) fn execute_proposal_pressed(proposal_id_raw: String) {
let Ok(proposal_id) = proposal_id_raw.trim().parse::<u64>() else {
return;
};
let guild_id = dom::input_by_id("governance-guild")
.map(|i| i.value().trim().to_string())
.and_then(|s| s.parse::<u64>().ok());
dom::swap_inner(
"governance-result",
"<span style=\"color:var(--muted)\">executing…</span>",
);
wasm_bindgen_futures::spawn_local(async move {
let result = async {
super::sponsor_rate_guard()?;
let (signer, _) = crate::app::chat::credit_signer()
.await
.ok_or_else(|| "no identity".to_string())?;
let fee_payer = crate::app::sponsor::signer()?;
crate::app::registry::execute_proposal_sponsored(
&signer,
&fee_payer,
proposal_id,
crate::app::registry::ALPHA_USD_ADDRESS,
)
.await
}
.await;
match result {
Ok(_) => {
super::refresh_credits_pill().await;
dom::swap_inner(
"governance-result",
&dom::msg_span(dom::Msg::Muted, "executed — treasury spend paid out"),
);
if let Some(g) = guild_id {
refresh_governance_list(g).await;
}
}
Err(e) => {
web_sys::console::warn_1(&JsValue::from_str(&format!("execute proposal: {e}")));
dom::swap_inner(
"governance-result",
&dom::msg_span(
dom::Msg::Error,
"couldn't execute (not passed yet, or before the deadline)",
),
);
}
}
});
}
async fn resolve_governance_recipient(arg: &str) -> Result<String, String> {
use crate::encoding::Recipient;
match crate::encoding::classify_recipient(arg)? {
Recipient::Address(addr) => Ok(addr),
Recipient::Name(name) => crate::app::registry::owner_of_name(&name)
.await?
.ok_or_else(|| format!("no on-chain owner for subdomain \"{name}\"")),
}
}
pub(crate) async fn refresh_governance_list(guild_id: u64) {
if dom::by_id("governance-list").is_none() {
return;
}
let ids = match crate::app::registry::proposals_of(guild_id, 0, PROPOSAL_LIST_LIMIT).await {
Ok(v) => v,
Err(_) => {
dom::swap_inner("governance-list", "");
return;
}
};
if ids.is_empty() {
dom::swap_inner(
"governance-list",
&dom::msg_span(dom::Msg::Muted, "no proposals"),
);
return;
}
let mut rows: Vec<maud::Markup> = 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));
let whole = p.amount / 1_000_000_000_000_000_000u128;
let cents = (p.amount % 1_000_000_000_000_000_000u128) / 10_000_000_000_000_000u128;
let status = p.status_label();
let is_open = p.status == 0;
rows.push(maud::html! {
div style="border-top:1px solid var(--border);padding:6px 0;font-size:11px;color:var(--fg)" {
div style="display:flex;align-items:center;gap:8px" {
code style="color:var(--muted)" { "#" (id) }
span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" { (p.to) }
span style="color:var(--muted)" { (whole) "." (format!("{cents:02}")) " LH" }
span style="color:var(--muted)" { (status) }
}
div style="display:flex;align-items:center;gap:8px;margin-top:4px;color:var(--muted)" {
span { "for " (votes_for) " · against " (votes_against) }
span style="flex:1" {}
@if is_open {
button type="button" data-action="vote" data-arg=(format!("{id}:for"))
.ghost style="padding:0 6px" { "vote for" }
button type="button" data-action="vote" data-arg=(format!("{id}:against"))
.ghost style="padding:0 6px" { "vote against" }
button type="button" data-action="execute-proposal" data-arg=(id.to_string())
.ghost style="padding:0 6px" { "execute" }
}
}
}
});
}
let html = maud::html! {
div style="margin-top:8px" { @for r in &rows { (r) } }
}
.into_string();
dom::swap_inner("governance-list", &html);
}