use crate::{collect_flags, fmt_interval, fmt_lh, fmt_ttl, load_signer, parse_guild_id, parse_proposal_id, parse_ttl, registry, resolve_member_address, take_tba_flag, tba_execute_diamond_call, truncate_words, INVITE_DEFAULT_TTL_SECS};
pub(crate) const VOTE_USAGE: &str = "\
usage: localharness vote <propose|cast|execute|list|show|shares|weighted> ...
vote propose [--as <me>] <guildId> <to> <amount> [--period <dur>] [memo...]
a member proposes a treasury spend (opens a vote)
vote cast [--as <me>] [--tba <subguild>] <proposalId> <for|against>
cast a one-member-one-vote ballot;
--tba: a sub-guild's TBA votes in a parent guild's DAO
vote execute [--as <me>] <proposalId> resolve a closed proposal (spends if passed)
vote list <guildId> list a guild's proposals + tally
vote show <proposalId> full proposal detail + tally + passing
to: a subdomain name (resolved to its owner) or a raw 0x address amount: $LH (e.g. 5 or 0.5)
dur: 1h / 7d / 30d (1h … 30d, default 7d)
SHARE-WEIGHTED board (a cap table — runs in PARALLEL to the 1m1v vote above):
vote shares set [--as <me>] <guildId> <member> <count>
admin sets a member's share weight (cap table)
vote shares show <guildId> [member] show the cap table (a member's shares, or the total)
vote weighted propose [--as <me>] <guildId> <to> <amount> [--period <dur>] [memo...]
a member opens a SHARE-WEIGHTED treasury-spend proposal
vote weighted cast [--as <me>] [--tba <subguild>] <proposalId> <for|against>
cast a ballot weighted by YOUR shares
vote weighted execute [--as <me>] <proposalId> resolve a closed weighted proposal
vote weighted list <guildId> list a guild's weighted proposals + share tally
vote weighted show <proposalId> full weighted-proposal detail + share tally
count: a whole number of shares (unitless, e.g. 60 — NOT $LH)";
pub(crate) const VOTE_MAX_PERIOD_SECS: u64 = 30 * 24 * 3600;
pub(crate) const VOTE_LIST_SCAN: u64 = 100;
pub(crate) fn parse_vote_support(raw: &str) -> Result<bool, String> {
match raw.trim().to_ascii_lowercase().as_str() {
"for" | "yes" | "y" | "aye" | "support" => Ok(true),
"against" | "no" | "n" | "nay" | "oppose" => Ok(false),
other => Err(format!("ballot must be 'for' or 'against', got '{other}'")),
}
}
pub(crate) fn parse_share_count(raw: &str) -> Result<u128, String> {
raw.trim()
.parse::<u128>()
.map_err(|_| format!("share count must be a whole number, got '{raw}'"))
}
pub(crate) fn parse_voting_period(raw: &str) -> Result<u64, String> {
let secs = parse_ttl(raw)?;
if secs > VOTE_MAX_PERIOD_SECS {
return Err(format!("voting period '{raw}' exceeds the 30d maximum"));
}
Ok(secs)
}
pub(crate) struct ParsedVotePropose {
guild_id: u64,
to: String,
amount_wei: u128,
period_secs: u64,
memo: String,
}
pub(crate) fn parse_vote_propose_args(rest: &[String]) -> Result<ParsedVotePropose, String> {
let ([period], positional) = collect_flags(rest, ["--period"], VOTE_USAGE)?;
if positional.len() < 3 {
return Err(format!("vote propose needs <guildId> <to> <amount>\n{VOTE_USAGE}"));
}
let guild_id = parse_guild_id(&positional[0])?;
let to = positional[1].clone();
let amount_label = &positional[2];
let amount_wei = match localharness::encoding::parse_token_amount(amount_label) {
Some(w) if w > 0 => w,
_ => return Err(format!("amount must be a positive $LH amount, got '{amount_label}'")),
};
let period_secs = match period {
None => INVITE_DEFAULT_TTL_SECS, Some(raw) => parse_voting_period(&raw)?,
};
let memo = positional[3..].join(" ");
Ok(ParsedVotePropose { guild_id, to, amount_wei, period_secs, memo })
}
pub(crate) async fn vote(caller: Option<&str>, rest: &[String]) -> i32 {
match rest.first().map(String::as_str) {
Some("propose") => vote_propose(caller, &rest[1..]).await,
Some("cast") => {
let (tba, positional) = match take_tba_flag(&rest[1..]) {
Ok(v) => v,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
match (positional.first(), positional.get(1)) {
(Some(id), Some(ballot)) => vote_cast(caller, id, ballot, tba.as_deref()).await,
_ => {
eprintln!("usage: localharness vote cast [--as <me>] [--tba <subguild>] <proposalId> <for|against>");
2
}
}
}
Some("execute") => match rest.get(1) {
Some(id) => vote_execute(caller, id).await,
None => {
eprintln!("usage: localharness vote execute [--as <me>] <proposalId>");
2
}
},
Some("list") => match rest.get(1) {
Some(id) => vote_list(id).await,
None => {
eprintln!("usage: localharness vote list <guildId>");
2
}
},
Some("show") => match rest.get(1) {
Some(id) => vote_show(id).await,
None => {
eprintln!("usage: localharness vote show <proposalId>");
2
}
},
Some("shares") => vote_shares(caller, &rest[1..]).await,
Some("weighted") => vote_weighted(caller, &rest[1..]).await,
_ => {
eprintln!("{VOTE_USAGE}");
2
}
}
}
pub(crate) async fn vote_shares(caller: Option<&str>, rest: &[String]) -> i32 {
match rest.first().map(String::as_str) {
Some("set") => match (rest.get(1), rest.get(2), rest.get(3)) {
(Some(g), Some(m), Some(c)) => vote_shares_set(caller, g, m, c).await,
_ => {
eprintln!("usage: localharness vote shares set [--as <me>] <guildId> <member> <count>");
2
}
},
Some("show") => match rest.get(1) {
Some(g) => vote_shares_show(g, rest.get(2).map(String::as_str)).await,
None => {
eprintln!("usage: localharness vote shares show <guildId> [member]");
2
}
},
_ => {
eprintln!("usage: localharness vote shares <set|show> …");
2
}
}
}
pub(crate) async fn vote_weighted(caller: Option<&str>, rest: &[String]) -> i32 {
match rest.first().map(String::as_str) {
Some("propose") => vote_weighted_propose(caller, &rest[1..]).await,
Some("cast") => {
let (tba, positional) = match take_tba_flag(&rest[1..]) {
Ok(v) => v,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
match (positional.first(), positional.get(1)) {
(Some(id), Some(ballot)) => vote_weighted_cast(caller, id, ballot, tba.as_deref()).await,
_ => {
eprintln!("usage: localharness vote weighted cast [--as <me>] [--tba <subguild>] <proposalId> <for|against>");
2
}
}
}
Some("execute") => match rest.get(1) {
Some(id) => vote_weighted_execute(caller, id).await,
None => {
eprintln!("usage: localharness vote weighted execute [--as <me>] <proposalId>");
2
}
},
Some("list") => match rest.get(1) {
Some(id) => vote_weighted_list(id).await,
None => {
eprintln!("usage: localharness vote weighted list <guildId>");
2
}
},
Some("show") => match rest.get(1) {
Some(id) => vote_weighted_show(id).await,
None => {
eprintln!("usage: localharness vote weighted show <proposalId>");
2
}
},
_ => {
eprintln!("usage: localharness vote weighted <propose|cast|execute|list|show> …");
2
}
}
}
pub(crate) async fn vote_propose(caller: Option<&str>, rest: &[String]) -> i32 {
let ParsedVotePropose { guild_id, to, amount_wei, period_secs, memo } =
match parse_vote_propose_args(rest) {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let to_hex = match resolve_member_address(&to).await {
Ok(a) => a,
Err(e) => {
eprintln!("vote propose: {e}");
return 1;
}
};
let signer = match load_signer(caller) {
Ok(pair) => pair,
Err(code) => return code,
};
println!(
"proposing to spend {} from guild #{guild_id} to {to_hex} (votes for {}) …",
fmt_lh(amount_wei),
fmt_ttl(period_secs)
);
match registry::propose_sponsored(&signer, guild_id, &to_hex, amount_wei, memo.as_bytes(), period_secs)
.await
{
Ok(tx) => {
let id_note = match registry::proposals_of(guild_id, 0, VOTE_LIST_SCAN).await {
Ok(ids) if !ids.is_empty() => Some(ids[ids.len() - 1]),
_ => None,
};
match id_note {
Some(id) => {
println!("✓ proposal #{id} opened — voting closes in {}", fmt_ttl(period_secs));
println!(" members vote: vote cast {id} <for|against>");
println!(" after it closes, anyone runs: vote execute {id}");
}
None => {
println!("✓ proposal opened — see it with `vote list {guild_id}`");
}
}
println!(" tx: {tx}");
0
}
Err(e) => {
eprintln!("vote propose failed: {e}");
1
}
}
}
pub(crate) async fn vote_cast(caller: Option<&str>, id_arg: &str, ballot: &str, tba: Option<&str>) -> i32 {
let proposal_id = match parse_proposal_id(id_arg) {
Ok(id) => id,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let support = match parse_vote_support(ballot) {
Ok(s) => s,
Err(e) => {
eprintln!("vote cast: {e}");
return 2;
}
};
if let Some(subguild) = tba {
let side = if support { "for" } else { "against" };
return tba_execute_diamond_call(
caller,
subguild,
registry::encode_vote_calldata(proposal_id, support),
&format!("'{subguild}' voting {side} on proposal #{proposal_id}"),
)
.await;
}
let signer = match load_signer(caller) {
Ok(pair) => pair,
Err(code) => return code,
};
let side = if support { "for" } else { "against" };
println!("casting a '{side}' vote on proposal #{proposal_id} …");
match registry::vote_sponsored(&signer, proposal_id, support)
.await
{
Ok(tx) => {
println!("✓ voted {side} on proposal #{proposal_id} tx: {tx}");
0
}
Err(e) => {
eprintln!("vote cast failed: {e}");
1
}
}
}
pub(crate) async fn vote_execute(caller: Option<&str>, id_arg: &str) -> i32 {
let proposal_id = match parse_proposal_id(id_arg) {
Ok(id) => id,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let signer = match load_signer(caller) {
Ok(pair) => pair,
Err(code) => return code,
};
println!("executing proposal #{proposal_id} …");
match registry::execute_proposal_sponsored(&signer, proposal_id)
.await
{
Ok(tx) => {
let outcome = match registry::get_proposal(proposal_id).await {
Ok(p) => match p.status {
3 => " — PASSED, treasury spent".to_string(),
2 => " — FAILED, no spend".to_string(),
_ => String::new(),
},
Err(_) => String::new(),
};
println!("✓ proposal #{proposal_id} resolved{outcome} tx: {tx}");
0
}
Err(e) => {
eprintln!("vote execute failed: {e}");
1
}
}
}
pub(crate) fn format_proposal_row(id: u64, p: ®istry::Proposal, t: ®istry::Tally, memo: &str, now: u64) -> String {
let when = if p.deadline == 0 {
"—".to_string()
} else if p.deadline <= now {
"CLOSED".to_string()
} else {
format!("in {}", fmt_interval(p.deadline - now))
};
let snippet = truncate_words(memo, 60);
format!(
" #{id} [{status}] for {f} / against {a} quorum {q} closes {when} {passing}\n {snippet}",
status = p.status_label(),
f = t.for_votes,
a = t.against_votes,
q = t.quorum,
passing = if t.passing { "(passing)" } else { "(not passing)" },
)
}
pub(crate) async fn vote_list(id_arg: &str) -> i32 {
let guild_id = match parse_guild_id(id_arg) {
Ok(id) => id,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let ids = match registry::proposals_of(guild_id, 0, VOTE_LIST_SCAN).await {
Ok(ids) => ids,
Err(e) => {
eprintln!("vote list failed: {e}");
return 1;
}
};
let name = registry::guild_name(guild_id).await.unwrap_or_default();
let label = if name.is_empty() {
format!("guild #{guild_id}")
} else {
format!("guild #{guild_id} '{name}'")
};
if ids.is_empty() {
println!("{label} has no proposals — open one with `vote propose {guild_id} <to> <amount>`");
return 0;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
println!("{label} — {} proposal(s):", ids.len());
for id in ids {
let p = match registry::get_proposal(id).await {
Ok(p) => p,
Err(e) => {
println!(" #{id} (could not read: {e})");
continue;
}
};
let t = registry::tally_of(id).await.unwrap_or(registry::Tally {
for_votes: 0,
against_votes: 0,
quorum: 0,
votes_cast: 0,
passing: false,
});
let memo = registry::proposal_memo_of(id).await.unwrap_or_default();
println!("{}", format_proposal_row(id, &p, &t, &memo, now));
}
0
}
pub(crate) async fn vote_show(id_arg: &str) -> i32 {
let proposal_id = match parse_proposal_id(id_arg) {
Ok(id) => id,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let p = match registry::get_proposal(proposal_id).await {
Ok(p) => p,
Err(e) => {
eprintln!("vote show: {e}");
return 1;
}
};
let t = registry::tally_of(proposal_id).await.ok();
let memo = registry::proposal_memo_of(proposal_id).await.unwrap_or_default();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let when = if p.deadline == 0 {
"—".to_string()
} else if p.deadline <= now {
"CLOSED (ready to execute)".to_string()
} else {
format!("in {}", fmt_interval(p.deadline - now))
};
println!("proposal #{proposal_id} [{}]", p.status_label());
println!(" guild #{}", p.guild_id);
println!(" proposer {}", p.proposer);
println!(" spend {} -> {}", fmt_lh(p.amount), p.to);
println!(" closes {when}");
match t {
Some(t) => {
println!(
" tally for {} / against {} quorum {} cast {} {}",
t.for_votes,
t.against_votes,
t.quorum,
t.votes_cast,
if t.passing { "(passing)" } else { "(not passing)" }
);
}
None => println!(" tally for {} / against {}", p.for_votes, p.against_votes),
}
if !memo.is_empty() {
println!(" memo {memo}");
}
0
}
pub(crate) async fn vote_shares_set(caller: Option<&str>, guild_arg: &str, member_arg: &str, count_arg: &str) -> i32 {
let guild_id = match parse_guild_id(guild_arg) {
Ok(id) => id,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let shares = match parse_share_count(count_arg) {
Ok(s) => s,
Err(e) => {
eprintln!("vote shares set: {e}");
return 2;
}
};
let member_hex = match resolve_member_address(member_arg).await {
Ok(a) => a,
Err(e) => {
eprintln!("vote shares set: {e}");
return 1;
}
};
let signer = match load_signer(caller) {
Ok(pair) => pair,
Err(code) => return code,
};
println!("setting {member_hex} to {shares} share(s) in guild #{guild_id} …");
match registry::set_shares_sponsored(&signer, guild_id, &member_hex, shares).await {
Ok(tx) => {
let total = registry::total_shares_of(guild_id).await.unwrap_or(0);
println!("✓ {member_hex} now holds {shares} of {total} share(s) in guild #{guild_id} tx: {tx}");
0
}
Err(e) => {
eprintln!("vote shares set failed: {e}");
1
}
}
}
pub(crate) async fn vote_shares_show(guild_arg: &str, member_arg: Option<&str>) -> i32 {
let guild_id = match parse_guild_id(guild_arg) {
Ok(id) => id,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let total = match registry::total_shares_of(guild_id).await {
Ok(t) => t,
Err(e) => {
eprintln!("vote shares show failed: {e}");
return 1;
}
};
println!("guild #{guild_id} — {total} total share(s)");
if let Some(member) = member_arg {
let member_hex = match resolve_member_address(member).await {
Ok(a) => a,
Err(e) => {
eprintln!("vote shares show: {e}");
return 1;
}
};
match registry::shares_of(guild_id, &member_hex).await {
Ok(s) => println!(" {member_hex}: {s} share(s)"),
Err(e) => {
eprintln!("vote shares show: {e}");
return 1;
}
}
}
0
}
pub(crate) async fn vote_weighted_propose(caller: Option<&str>, rest: &[String]) -> i32 {
let ParsedVotePropose { guild_id, to, amount_wei, period_secs, memo } =
match parse_vote_propose_args(rest) {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let to_hex = match resolve_member_address(&to).await {
Ok(a) => a,
Err(e) => {
eprintln!("vote weighted propose: {e}");
return 1;
}
};
let signer = match load_signer(caller) {
Ok(pair) => pair,
Err(code) => return code,
};
println!(
"proposing (share-weighted) to spend {} from guild #{guild_id} to {to_hex} (votes for {}) …",
fmt_lh(amount_wei),
fmt_ttl(period_secs)
);
match registry::propose_weighted_sponsored(
&signer,
guild_id,
&to_hex,
amount_wei,
period_secs,
memo.as_bytes(),
)
.await
{
Ok(tx) => {
let id_note = match registry::weighted_proposals_of(guild_id, 0, VOTE_LIST_SCAN).await {
Ok(ids) if !ids.is_empty() => Some(ids[ids.len() - 1]),
_ => None,
};
match id_note {
Some(id) => {
println!("✓ weighted proposal #{id} opened — voting closes in {}", fmt_ttl(period_secs));
println!(" share-holders vote: vote weighted cast {id} <for|against>");
println!(" after it closes, anyone runs: vote weighted execute {id}");
}
None => {
println!("✓ weighted proposal opened — see it with `vote weighted list {guild_id}`");
}
}
println!(" tx: {tx}");
0
}
Err(e) => {
eprintln!("vote weighted propose failed: {e}");
1
}
}
}
pub(crate) async fn vote_weighted_cast(caller: Option<&str>, id_arg: &str, ballot: &str, tba: Option<&str>) -> i32 {
let proposal_id = match parse_proposal_id(id_arg) {
Ok(id) => id,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let support = match parse_vote_support(ballot) {
Ok(s) => s,
Err(e) => {
eprintln!("vote weighted cast: {e}");
return 2;
}
};
if let Some(subguild) = tba {
let side = if support { "for" } else { "against" };
return tba_execute_diamond_call(
caller,
subguild,
registry::encode_vote_weighted_calldata(proposal_id, support),
&format!("'{subguild}' weighted-voting {side} on proposal #{proposal_id}"),
)
.await;
}
let signer = match load_signer(caller) {
Ok(pair) => pair,
Err(code) => return code,
};
let side = if support { "for" } else { "against" };
println!("casting a share-weighted '{side}' vote on proposal #{proposal_id} …");
match registry::vote_weighted_sponsored(&signer, proposal_id, support).await {
Ok(tx) => {
println!("✓ voted {side} (weighted) on proposal #{proposal_id} tx: {tx}");
0
}
Err(e) => {
eprintln!("vote weighted cast failed: {e}");
1
}
}
}
pub(crate) async fn vote_weighted_execute(caller: Option<&str>, id_arg: &str) -> i32 {
let proposal_id = match parse_proposal_id(id_arg) {
Ok(id) => id,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let signer = match load_signer(caller) {
Ok(pair) => pair,
Err(code) => return code,
};
println!("executing weighted proposal #{proposal_id} …");
match registry::execute_weighted_proposal_sponsored(&signer, proposal_id).await {
Ok(tx) => {
let outcome = match registry::get_weighted_proposal(proposal_id).await {
Ok(p) => match p.status {
3 => " — PASSED, treasury spent".to_string(),
2 => " — FAILED, no spend".to_string(),
_ => String::new(),
},
Err(_) => String::new(),
};
println!("✓ weighted proposal #{proposal_id} resolved{outcome} tx: {tx}");
0
}
Err(e) => {
eprintln!("vote weighted execute failed: {e}");
1
}
}
}
pub(crate) fn format_weighted_proposal_row(
id: u64,
p: ®istry::WeightedProposal,
t: ®istry::WeightedTally,
memo: &str,
now: u64,
) -> String {
let when = if p.deadline == 0 {
"—".to_string()
} else if p.deadline <= now {
"CLOSED".to_string()
} else {
format!("in {}", fmt_interval(p.deadline - now))
};
let snippet = truncate_words(memo, 60);
format!(
" #{id} [{status}] for {f} / against {a} shares quorum {q} shares closes {when} {passing}\n {snippet}",
status = p.status_label(),
f = t.for_shares,
a = t.against_shares,
q = t.quorum_shares,
passing = if t.passing { "(passing)" } else { "(not passing)" },
)
}
pub(crate) async fn vote_weighted_list(id_arg: &str) -> i32 {
let guild_id = match parse_guild_id(id_arg) {
Ok(id) => id,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let ids = match registry::weighted_proposals_of(guild_id, 0, VOTE_LIST_SCAN).await {
Ok(ids) => ids,
Err(e) => {
eprintln!("vote weighted list failed: {e}");
return 1;
}
};
let name = registry::guild_name(guild_id).await.unwrap_or_default();
let label = if name.is_empty() {
format!("guild #{guild_id}")
} else {
format!("guild #{guild_id} '{name}'")
};
if ids.is_empty() {
println!("{label} has no weighted proposals — open one with `vote weighted propose {guild_id} <to> <amount>`");
return 0;
}
let total = registry::total_shares_of(guild_id).await.unwrap_or(0);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
println!("{label} — {} weighted proposal(s) ({total} total shares):", ids.len());
for id in ids {
let p = match registry::get_weighted_proposal(id).await {
Ok(p) => p,
Err(e) => {
println!(" #{id} (could not read: {e})");
continue;
}
};
let t = registry::weighted_tally_of(id).await.unwrap_or(registry::WeightedTally {
for_shares: 0,
against_shares: 0,
quorum_shares: 0,
cast_shares: 0,
passing: false,
});
let memo = registry::weighted_proposal_memo_of(id).await.unwrap_or_default();
println!("{}", format_weighted_proposal_row(id, &p, &t, &memo, now));
}
0
}
pub(crate) async fn vote_weighted_show(id_arg: &str) -> i32 {
let proposal_id = match parse_proposal_id(id_arg) {
Ok(id) => id,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let p = match registry::get_weighted_proposal(proposal_id).await {
Ok(p) => p,
Err(e) => {
eprintln!("vote weighted show: {e}");
return 1;
}
};
let t = registry::weighted_tally_of(proposal_id).await.ok();
let memo = registry::weighted_proposal_memo_of(proposal_id).await.unwrap_or_default();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let when = if p.deadline == 0 {
"—".to_string()
} else if p.deadline <= now {
"CLOSED (ready to execute)".to_string()
} else {
format!("in {}", fmt_interval(p.deadline - now))
};
println!("weighted proposal #{proposal_id} [{}]", p.status_label());
println!(" guild #{}", p.guild_id);
println!(" proposer {}", p.proposer);
println!(" spend {} -> {}", fmt_lh(p.amount), p.to);
println!(" closes {when}");
println!(" snapshot {} total share(s) at propose", p.snapshot_total_shares);
match t {
Some(t) => {
println!(
" tally for {} / against {} shares quorum {} shares cast {} {}",
t.for_shares,
t.against_shares,
t.quorum_shares,
t.cast_shares,
if t.passing { "(passing)" } else { "(not passing)" }
);
}
None => println!(" tally for {} / against {} shares", p.for_shares, p.against_shares),
}
if !memo.is_empty() {
println!(" memo {memo}");
}
0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::args;
#[test]
fn parse_vote_support_maps_for_against() {
for raw in ["for", "FOR", "yes", " Y ", "aye", "support"] {
assert_eq!(parse_vote_support(raw), Ok(true), "{raw}");
}
for raw in ["against", "AGAINST", "no", " N ", "nay", "oppose"] {
assert_eq!(parse_vote_support(raw), Ok(false), "{raw}");
}
assert!(parse_vote_support("maybe").is_err());
assert!(parse_vote_support("").is_err());
}
#[test]
fn parse_voting_period_bounds_to_30d() {
assert_eq!(parse_voting_period("1h"), Ok(3600));
assert_eq!(parse_voting_period("7d"), Ok(7 * 86_400));
assert_eq!(parse_voting_period("30d"), Ok(30 * 86_400));
assert!(parse_voting_period("31d").is_err()); assert!(parse_voting_period("90d").is_err()); assert!(parse_voting_period("30m").is_err()); }
#[test]
fn parse_vote_propose_args_positionals_and_period() {
let p = parse_vote_propose_args(&args(&["5", "alice", "2.5", "q3", "grant"])).unwrap();
assert_eq!(p.guild_id, 5);
assert_eq!(p.to, "alice");
assert_eq!(p.amount_wei, 2_500_000_000_000_000_000); assert_eq!(p.period_secs, INVITE_DEFAULT_TTL_SECS);
assert_eq!(p.memo, "q3 grant");
let p = parse_vote_propose_args(&args(&["3", "--period", "1h", "0x1111111111111111111111111111111111111111", "1"])).unwrap();
assert_eq!(p.guild_id, 3);
assert_eq!(p.to, "0x1111111111111111111111111111111111111111");
assert_eq!(p.amount_wei, 1_000_000_000_000_000_000);
assert_eq!(p.period_secs, 3600);
assert_eq!(p.memo, "");
assert!(parse_vote_propose_args(&args(&["5", "alice"])).is_err()); assert!(parse_vote_propose_args(&args(&["5", "alice", "0"])).is_err()); assert!(parse_vote_propose_args(&args(&["5", "alice", "1", "--period", "90d"])).is_err());
}
#[test]
fn format_proposal_row_contains_key_fields() {
let p = registry::Proposal {
guild_id: 5,
proposer: "0xproposer".into(),
to: "0xrecipient".into(),
amount: 2_000_000_000_000_000_000,
deadline: 1_000 + 3600, status: 0, for_votes: 2,
against_votes: 1,
};
let t = registry::Tally { for_votes: 2, against_votes: 1, quorum: 2, votes_cast: 3, passing: true };
let row = format_proposal_row(9, &p, &t, "fund\nthe audit", 1_000);
assert!(row.contains("#9"));
assert!(row.contains("[active]"));
assert!(row.contains("for 2 / against 1"));
assert!(row.contains("quorum 2"));
assert!(row.contains("closes in 1h"));
assert!(row.contains("(passing)"));
assert!(row.contains("fund the audit")); }
#[test]
fn format_proposal_row_closed_and_not_passing() {
let p = registry::Proposal {
guild_id: 1,
proposer: "0x0".into(),
to: "0x0".into(),
amount: 0,
deadline: 100, status: 2, for_votes: 0,
against_votes: 0,
};
let t = registry::Tally { for_votes: 0, against_votes: 0, quorum: 1, votes_cast: 0, passing: false };
let row = format_proposal_row(2, &p, &t, "", 5_000);
assert!(row.contains("[failed]"));
assert!(row.contains("closes CLOSED"));
assert!(row.contains("(not passing)"));
}
#[test]
fn parse_share_count_whole_numbers_only() {
assert_eq!(parse_share_count("60"), Ok(60));
assert_eq!(parse_share_count(" 0 "), Ok(0)); assert_eq!(parse_share_count("1000000"), Ok(1_000_000));
assert!(parse_share_count("2.5").is_err()); assert!(parse_share_count("-1").is_err());
assert!(parse_share_count("abc").is_err());
assert!(parse_share_count("").is_err());
}
#[test]
fn format_weighted_proposal_row_contains_share_fields() {
let p = registry::WeightedProposal {
guild_id: 5,
proposer: "0xproposer".into(),
to: "0xrecipient".into(),
amount: 2_000_000_000_000_000_000,
deadline: 1_000 + 3600, status: 0, for_shares: 60,
against_shares: 10,
snapshot_total_shares: 100,
};
let t = registry::WeightedTally {
for_shares: 60,
against_shares: 10,
quorum_shares: 51,
cast_shares: 70,
passing: true,
};
let row = format_weighted_proposal_row(9, &p, &t, "fund\nthe audit", 1_000);
assert!(row.contains("#9"));
assert!(row.contains("[active]"));
assert!(row.contains("for 60 / against 10 shares"));
assert!(row.contains("quorum 51 shares"));
assert!(row.contains("closes in 1h"));
assert!(row.contains("(passing)"));
assert!(row.contains("fund the audit")); }
}