v3 : le vote par la chaîne Ğ1, côté moteur et serveur
Le bulletin scellé (ld1: + blake2b-256 du bulletin canonique, sel de 128 bits), l'ordre de la chaîne (bloc, puis rang de l'événement : le dernier fait foi), les adresses SS58 comparées par clé (ld-engine, pur). trustin-access : Monnaie libre vote par la chaîne, les groupes gérés sur le serveur ; le droit de vote se lit à la confirmation. Serveur : bulletins en attente, confirmation sur l'indexeur (virement, auteur, identité, membre), identité idty:<index> créée au besoin, tâche de fond toutes les 10 s, expiration à 24 h, vote groupé en une écriture dans les groupes gérés. Migration additive 0006. Sans LD_VOTE_ACCOUNT, le vote par la chaîne est éteint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,12 +14,14 @@
|
||||
use crate::AppState;
|
||||
use crate::adoption;
|
||||
use crate::auth::{IdentityRow, User};
|
||||
use crate::ballots;
|
||||
use crate::docs::{self, DocState, PROPOSAL_SELECT, ProposalRow, UnitRow};
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::util;
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use ld_engine::assist::{self, Hint};
|
||||
use ld_engine::ballot::Choice;
|
||||
use ld_engine::edit::{self, EditKind};
|
||||
use ld_engine::inertia::InertiaLevel;
|
||||
use serde::Deserialize;
|
||||
@@ -616,12 +618,8 @@ pub async fn detail(
|
||||
}));
|
||||
}
|
||||
|
||||
let my_vote: Option<(String, i64)> =
|
||||
sqlx::query_as("SELECT choice, revision FROM votes WHERE proposal_id = ? AND identity_id = ?")
|
||||
.bind(&id)
|
||||
.bind(&user.id)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
let my_vote = ballots::my_votes(&state, &user.id, &st.doc.id).await?.remove(&id);
|
||||
let my_ballot = ballots::my_ballots(&state, &user.id, &st.doc.id).await?.remove(&id);
|
||||
let diff = match (&in_force, p.kind.as_str()) {
|
||||
(Some(v), "text") => Some(edit::diff(&v.body, &p.body)),
|
||||
_ => None,
|
||||
@@ -640,7 +638,9 @@ pub async fn detail(
|
||||
"seedMinimal": p.status == "in_force" && p.entered_at.is_none() && p.confirmed_at.is_none() && p.origin == "seed",
|
||||
"tally": st.tally(&p.id), "evaluation": eval, "votedAtInertia": voted_at,
|
||||
"blocked": st.blocked.contains(&p.id),
|
||||
"myVote": my_vote.as_ref().map(|v| &v.0), "myVoteStale": my_vote.as_ref().is_some_and(|v| v.1 != p.revision),
|
||||
"myVote": my_vote.as_ref().map(|v| &v.choice),
|
||||
"myVoteStale": my_vote.as_ref().is_some_and(|v| v.revision != p.revision),
|
||||
"myVoteChain": my_vote.as_ref().is_some_and(|v| v.source == "chain"), "myBallot": my_ballot,
|
||||
"translations": tr.get(&p.id).cloned().unwrap_or_default(),
|
||||
},
|
||||
"unit": { "id": unit.id, "code": unit.code, "inertia": docs::inertia_of(unit) },
|
||||
@@ -666,6 +666,8 @@ pub struct VoteBody {
|
||||
choice: Option<String>,
|
||||
}
|
||||
|
||||
/// One vote on the server, in a managed group. Where the vote goes through the
|
||||
/// chain (Monnaie libre), it is a ballot (`ballots::create`), never this.
|
||||
pub async fn vote(
|
||||
State(state): State<AppState>,
|
||||
User(user): User,
|
||||
@@ -673,37 +675,23 @@ pub async fn vote(
|
||||
Json(b): Json<VoteBody>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let (p, st) = guarded(&state, &user, &id).await?;
|
||||
if trustin_access::vote_way(&st.group.to_group()) == trustin_access::VoteWay::Chain {
|
||||
return Err(AppError::Conflict("chain_only"));
|
||||
}
|
||||
if !matches!(p.status.as_str(), "open" | "in_force") {
|
||||
return Err(AppError::Conflict("not_votable"));
|
||||
}
|
||||
// Withdrawing one's vote is always possible; casting one follows trustinWallet.
|
||||
if b.choice.is_some() && !docs::may_vote(&state, &user, &st.group).await? {
|
||||
return Err(AppError::Conflict("wot_members_only"));
|
||||
if !docs::may_vote(&state, &user, &st.group).await? {
|
||||
return Err(AppError::Conflict("members_only"));
|
||||
}
|
||||
match b.choice.as_deref() {
|
||||
Some(choice @ ("for" | "against")) => {
|
||||
sqlx::query(
|
||||
"INSERT INTO votes (proposal_id, identity_id, choice, revision, cast_at) VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(proposal_id, identity_id) DO UPDATE SET choice = excluded.choice,
|
||||
revision = excluded.revision, cast_at = excluded.cast_at",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&user.id)
|
||||
.bind(choice)
|
||||
.bind(p.revision)
|
||||
.bind(util::now())
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
sqlx::query("DELETE FROM votes WHERE proposal_id = ? AND identity_id = ?")
|
||||
.bind(&id)
|
||||
.bind(&user.id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
}
|
||||
let choice = match b.choice.as_deref() {
|
||||
Some("for") => Choice::For,
|
||||
Some("against") => Choice::Against,
|
||||
None => Choice::None,
|
||||
Some(_) => return Err(AppError::Invalid("choice")),
|
||||
}
|
||||
};
|
||||
let cast = ballots::Cast { proposal_id: p.id.clone(), revision: p.revision, choice };
|
||||
ballots::write_server_votes(&state, &user.id, &[cast]).await?;
|
||||
let entered = adoption::run(&state, &st.doc.id).await?;
|
||||
Ok(Json(json!({ "entered": entered })))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user