From d7950beb09575a62091c51251e93ec6376c1303e Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sat, 13 Jun 2026 15:06:30 +0200 Subject: [PATCH] feat(db): add proposal and proposal_vote tables --- .../2026-02-14-171124-0000_init/up.sql | 21 +++++ server/crates/arbiter-server/src/db/models.rs | 86 ++++++++++++++++++- server/crates/arbiter-server/src/db/schema.rs | 28 ++++++ 3 files changed, 133 insertions(+), 2 deletions(-) diff --git a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql index 732c6b6..849025e 100644 --- a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql +++ b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql @@ -216,3 +216,24 @@ create table if not exists integrity_envelope ( ) STRICT; create unique index if not exists uniq_integrity_envelope_entity on integrity_envelope (entity_kind, entity_id); + +create table if not exists proposal ( + id integer not null primary key, + kind text not null, + payload blob not null, + initiator_id integer not null references operator_identity(id) on delete restrict, + created_at integer not null default(unixepoch('now')), + expires_at integer not null, + status text not null default 'pending' + check (status in ('pending', 'approved', 'rejected', 'expired')) +) STRICT; + +create table if not exists proposal_vote ( + id integer not null primary key, + proposal_id integer not null references proposal(id) on delete cascade, + operator_id integer not null references operator_identity(id) on delete restrict, + approve integer not null check (approve in (0, 1)), + signature blob not null, + voted_at integer not null default(unixepoch('now')), + unique (proposal_id, operator_id) +) STRICT; diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index 1f57b1b..a546a62 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -15,10 +15,11 @@ use restructed::Models; pub mod types { use chrono::{DateTime, Utc}; use diesel::{ + backend::Backend, deserialize::{FromSql, FromSqlRow}, expression::AsExpression, serialize::{IsNull, ToSql}, - sql_types::Integer, + sql_types::{Integer, Text}, sqlite::{Sqlite, SqliteType}, }; @@ -61,7 +62,7 @@ pub mod types { impl FromSql for SqliteTimestamp { fn from_sql( - mut bytes: ::RawValue<'_>, + mut bytes: ::RawValue<'_>, ) -> diesel::deserialize::Result { let Some(SqliteType::Long) = bytes.value_type() else { return Err(format!( @@ -141,6 +142,45 @@ pub mod types { declare_id!(TlsHistoryId); declare_id!(EvmWalletId); declare_id!(ClientId); + + #[derive(Debug, Clone, PartialEq, Eq, AsExpression, FromSqlRow)] + #[diesel(sql_type = Text)] + pub enum ProposalStatus { + Pending, + Approved, + Rejected, + Expired, + } + + impl ToSql for ProposalStatus { + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, Sqlite>, + ) -> diesel::serialize::Result { + let s: &str = match self { + Self::Pending => "pending", + Self::Approved => "approved", + Self::Rejected => "rejected", + Self::Expired => "expired", + }; + >::to_sql(s, out) + } + } + + impl FromSql for ProposalStatus { + fn from_sql( + bytes: ::RawValue<'_>, + ) -> diesel::deserialize::Result { + let s = >::from_sql(bytes)?; + match s.as_str() { + "pending" => Ok(Self::Pending), + "approved" => Ok(Self::Approved), + "rejected" => Ok(Self::Rejected), + "expired" => Ok(Self::Expired), + other => Err(format!("Unknown proposal status: {other}").into()), + } + } + } } pub use types::*; @@ -438,3 +478,45 @@ pub struct IntegrityEnvelope { pub signed_at: SqliteTimestamp, pub created_at: SqliteTimestamp, } + +#[derive(Debug, Queryable, Selectable, Identifiable)] +#[diesel(table_name = schema::proposal, check_for_backend(Sqlite))] +pub struct Proposal { + pub id: i32, + pub kind: String, + pub payload: Vec, + pub initiator_id: i32, + pub created_at: SqliteTimestamp, + pub expires_at: SqliteTimestamp, + pub status: ProposalStatus, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = schema::proposal, check_for_backend(Sqlite))] +pub struct NewProposal { + pub kind: String, + pub payload: Vec, + pub initiator_id: i32, + // status defaults to 'pending' at the DB layer + pub expires_at: SqliteTimestamp, +} + +#[derive(Debug, Queryable, Selectable, Identifiable)] +#[diesel(table_name = schema::proposal_vote, check_for_backend(Sqlite))] +pub struct ProposalVote { + pub id: i32, + pub proposal_id: i32, + pub operator_id: i32, + pub approve: bool, + pub signature: Vec, + pub voted_at: SqliteTimestamp, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = schema::proposal_vote, check_for_backend(Sqlite))] +pub struct NewProposalVote { + pub proposal_id: i32, + pub operator_id: i32, + pub approve: bool, + pub signature: Vec, +} diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index 981d885..c6ce19e 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -172,6 +172,29 @@ diesel::table! { } } +diesel::table! { + proposal (id) { + id -> Integer, + kind -> Text, + payload -> Binary, + initiator_id -> Integer, + created_at -> Integer, + expires_at -> Integer, + status -> Text, + } +} + +diesel::table! { + proposal_vote (id) { + id -> Integer, + proposal_id -> Integer, + operator_id -> Integer, + approve -> Bool, + signature -> Binary, + voted_at -> Integer, + } +} + diesel::table! { program_client (id) { id -> Integer, @@ -225,6 +248,9 @@ diesel::joinable!(evm_wallet_access -> evm_wallet (wallet_id)); diesel::joinable!(evm_wallet_access -> program_client (client_id)); diesel::joinable!(operator -> operator_identity (id)); diesel::joinable!(program_client -> client_metadata (metadata_id)); +diesel::joinable!(proposal -> operator_identity (initiator_id)); +diesel::joinable!(proposal_vote -> proposal (proposal_id)); +diesel::joinable!(proposal_vote -> operator_identity (operator_id)); diesel::allow_tables_to_appear_in_same_query!( aead_encrypted, @@ -245,6 +271,8 @@ diesel::allow_tables_to_appear_in_same_query!( operator, operator_identity, program_client, + proposal, + proposal_vote, root_key_history, tls_history, );