refactor(proposal): type ProposalSummary::kind as ProposalKindTag
This commit is contained in:
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
self,
|
self,
|
||||||
models::{
|
models::{
|
||||||
NewProposal, NewProposalVote, NewRecoveryProposalVote, NewRecoveryWakeupRequest,
|
NewProposal, NewProposalVote, NewRecoveryProposalVote, NewRecoveryWakeupRequest,
|
||||||
Proposal, ProposalStatus, SqliteTimestamp,
|
Proposal, ProposalKind, ProposalKindTag, ProposalStatus, SqliteTimestamp,
|
||||||
},
|
},
|
||||||
schema,
|
schema,
|
||||||
},
|
},
|
||||||
@@ -17,129 +17,11 @@ use chrono::Utc;
|
|||||||
use diesel::{ExpressionMethods as _, QueryDsl};
|
use diesel::{ExpressionMethods as _, QueryDsl};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use kameo::{Actor, actor::ActorRef, messages};
|
use kameo::{Actor, actor::ActorRef, messages};
|
||||||
use strum::{Display, EnumDiscriminants, EnumString, IntoDiscriminant as _, IntoStaticStr};
|
use strum::IntoDiscriminant as _;
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
pub const DEFAULT_TTL_SECS: i64 = 7 * 24 * 60 * 60; // 7 days
|
pub const DEFAULT_TTL_SECS: i64 = 7 * 24 * 60 * 60; // 7 days
|
||||||
|
|
||||||
/// A governance proposal and the parameters it carries.
|
|
||||||
#[derive(Debug, Clone, EnumDiscriminants)]
|
|
||||||
#[strum_discriminants(
|
|
||||||
name(ProposalKindTag),
|
|
||||||
vis(pub),
|
|
||||||
derive(Display, EnumString, IntoStaticStr),
|
|
||||||
strum(serialize_all = "snake_case")
|
|
||||||
)]
|
|
||||||
pub enum ProposalKind {
|
|
||||||
ApproveSdkClient {
|
|
||||||
client_id: i32,
|
|
||||||
},
|
|
||||||
GrantWalletAccess {
|
|
||||||
wallet_id: i32,
|
|
||||||
client_id: i32,
|
|
||||||
},
|
|
||||||
ReplaceOperator {
|
|
||||||
old_operator_id: i32,
|
|
||||||
new_pubkey: Vec<u8>,
|
|
||||||
},
|
|
||||||
TriggerRekey,
|
|
||||||
ApprovePersistentGrant {
|
|
||||||
payload_bytes: Vec<u8>,
|
|
||||||
},
|
|
||||||
ApproveOneOffTransaction {
|
|
||||||
payload_bytes: Vec<u8>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProposalKind {
|
|
||||||
pub fn encode_payload(&self) -> Vec<u8> {
|
|
||||||
match self {
|
|
||||||
Self::ApproveSdkClient { client_id } => client_id.to_be_bytes().to_vec(),
|
|
||||||
Self::GrantWalletAccess {
|
|
||||||
wallet_id,
|
|
||||||
client_id,
|
|
||||||
} => {
|
|
||||||
let mut buf = Vec::with_capacity(8);
|
|
||||||
buf.extend_from_slice(&wallet_id.to_be_bytes());
|
|
||||||
buf.extend_from_slice(&client_id.to_be_bytes());
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
Self::ReplaceOperator {
|
|
||||||
old_operator_id,
|
|
||||||
new_pubkey,
|
|
||||||
} => {
|
|
||||||
let len = u32::try_from(new_pubkey.len()).expect("pubkey len fits in u32");
|
|
||||||
let mut buf = Vec::with_capacity(4 + 4 + new_pubkey.len());
|
|
||||||
buf.extend_from_slice(&old_operator_id.to_be_bytes());
|
|
||||||
buf.extend_from_slice(&len.to_be_bytes());
|
|
||||||
buf.extend_from_slice(new_pubkey);
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
Self::TriggerRekey => vec![],
|
|
||||||
Self::ApprovePersistentGrant { payload_bytes }
|
|
||||||
| Self::ApproveOneOffTransaction { payload_bytes } => payload_bytes.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Key-rotation proposals require every operator to approve (§3.3).
|
|
||||||
#[must_use]
|
|
||||||
pub fn requires_full_quorum(kind: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
kind.parse::<ProposalKindTag>(),
|
|
||||||
Ok(ProposalKindTag::ReplaceOperator | ProposalKindTag::TriggerRekey)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decode(kind: &str, payload: &[u8]) -> Result<Self, String> {
|
|
||||||
let tag = kind
|
|
||||||
.parse::<ProposalKindTag>()
|
|
||||||
.map_err(|_| format!("unknown proposal kind: {kind}"))?;
|
|
||||||
match tag {
|
|
||||||
ProposalKindTag::ApproveSdkClient => {
|
|
||||||
let bytes = <[u8; 4]>::try_from(payload)
|
|
||||||
.map_err(|_| "invalid payload for approve_sdk_client".to_owned())?;
|
|
||||||
Ok(Self::ApproveSdkClient {
|
|
||||||
client_id: i32::from_be_bytes(bytes),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ProposalKindTag::GrantWalletAccess => {
|
|
||||||
let bytes = <[u8; 8]>::try_from(payload)
|
|
||||||
.map_err(|_| "invalid payload for grant_wallet_access".to_owned())?;
|
|
||||||
Ok(Self::GrantWalletAccess {
|
|
||||||
wallet_id: i32::from_be_bytes(bytes[..4].try_into().unwrap()),
|
|
||||||
client_id: i32::from_be_bytes(bytes[4..].try_into().unwrap()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ProposalKindTag::ReplaceOperator => {
|
|
||||||
let (id_bytes, rest) = payload
|
|
||||||
.split_first_chunk::<4>()
|
|
||||||
.ok_or_else(|| "replace_operator payload too short".to_owned())?;
|
|
||||||
let old_operator_id = i32::from_be_bytes(*id_bytes);
|
|
||||||
let (len_bytes, rest) = rest
|
|
||||||
.split_first_chunk::<4>()
|
|
||||||
.ok_or_else(|| "replace_operator payload too short".to_owned())?;
|
|
||||||
let len = u32::from_be_bytes(*len_bytes);
|
|
||||||
let len = usize::try_from(len).unwrap_or(usize::MAX);
|
|
||||||
let new_pubkey = rest
|
|
||||||
.get(..len)
|
|
||||||
.ok_or_else(|| "replace_operator payload truncated".to_owned())?
|
|
||||||
.to_vec();
|
|
||||||
Ok(Self::ReplaceOperator {
|
|
||||||
old_operator_id,
|
|
||||||
new_pubkey,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ProposalKindTag::TriggerRekey => Ok(Self::TriggerRekey),
|
|
||||||
ProposalKindTag::ApprovePersistentGrant => Ok(Self::ApprovePersistentGrant {
|
|
||||||
payload_bytes: payload.to_vec(),
|
|
||||||
}),
|
|
||||||
ProposalKindTag::ApproveOneOffTransaction => Ok(Self::ApproveOneOffTransaction {
|
|
||||||
payload_bytes: payload.to_vec(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum VoteOutcome {
|
pub enum VoteOutcome {
|
||||||
Pending,
|
Pending,
|
||||||
@@ -180,7 +62,7 @@ pub enum Error {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ProposalSummary {
|
pub struct ProposalSummary {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub kind: String,
|
pub kind: ProposalKindTag,
|
||||||
pub initiator_id: i32,
|
pub initiator_id: i32,
|
||||||
pub expires_at: SqliteTimestamp,
|
pub expires_at: SqliteTimestamp,
|
||||||
pub approve_count: i64,
|
pub approve_count: i64,
|
||||||
@@ -224,7 +106,7 @@ impl ProposalManager {
|
|||||||
let expires_at = SqliteTimestamp::from(Utc::now() + chrono::Duration::seconds(ttl));
|
let expires_at = SqliteTimestamp::from(Utc::now() + chrono::Duration::seconds(ttl));
|
||||||
|
|
||||||
let new_proposal = NewProposal {
|
let new_proposal = NewProposal {
|
||||||
kind: <&'static str>::from(kind.discriminant()).to_owned(),
|
kind: kind.discriminant(),
|
||||||
payload: kind.encode_payload(),
|
payload: kind.encode_payload(),
|
||||||
initiator_id,
|
initiator_id,
|
||||||
expires_at,
|
expires_at,
|
||||||
@@ -395,7 +277,7 @@ impl ProposalManager {
|
|||||||
clippy::as_conversions,
|
clippy::as_conversions,
|
||||||
reason = "operator count is always a small positive integer"
|
reason = "operator count is always a small positive integer"
|
||||||
)]
|
)]
|
||||||
let threshold = if ProposalKind::requires_full_quorum(&proposal.kind) {
|
let threshold = if proposal.kind.requires_full_quorum() {
|
||||||
// §3.3: key-rotation proposals require every eligible voter to approve
|
// §3.3: key-rotation proposals require every eligible voter to approve
|
||||||
// §3.5: when recovery is active, recovery operators also vote on replace_operator
|
// §3.5: when recovery is active, recovery operators also vote on replace_operator
|
||||||
(total_operators + total_recovery) as usize
|
(total_operators + total_recovery) as usize
|
||||||
@@ -519,7 +401,7 @@ impl ProposalManager {
|
|||||||
other => Error::DatabaseQuery(other),
|
other => Error::DatabaseQuery(other),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if proposal.kind.parse::<ProposalKindTag>() != Ok(ProposalKindTag::ReplaceOperator) {
|
if proposal.kind != ProposalKindTag::ReplaceOperator {
|
||||||
return Err(Error::NotAllowedForRecoveryOperator);
|
return Err(Error::NotAllowedForRecoveryOperator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +554,7 @@ impl ProposalManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_proposal(&self, proposal: &Proposal) -> Result<(), Error> {
|
async fn execute_proposal(&self, proposal: &Proposal) -> Result<(), Error> {
|
||||||
let kind = ProposalKind::decode(&proposal.kind, &proposal.payload)
|
let kind = ProposalKind::decode(proposal.kind, &proposal.payload)
|
||||||
.map_err(Error::ExecutionFailed)?;
|
.map_err(Error::ExecutionFailed)?;
|
||||||
match kind {
|
match kind {
|
||||||
ProposalKind::ApproveSdkClient { client_id } => {
|
ProposalKind::ApproveSdkClient { client_id } => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub mod types {
|
|||||||
sql_types::{Integer, Text},
|
sql_types::{Integer, Text},
|
||||||
sqlite::{Sqlite, SqliteType},
|
sqlite::{Sqlite, SqliteType},
|
||||||
};
|
};
|
||||||
|
use strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr};
|
||||||
|
|
||||||
#[derive(Debug, FromSqlRow, AsExpression, Clone)]
|
#[derive(Debug, FromSqlRow, AsExpression, Clone)]
|
||||||
#[diesel(sql_type = Integer)]
|
#[diesel(sql_type = Integer)]
|
||||||
@@ -166,9 +167,7 @@ pub mod types {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromSql<Text, Sqlite> for ProposalStatus {
|
impl FromSql<Text, Sqlite> for ProposalStatus {
|
||||||
fn from_sql(
|
fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
|
||||||
bytes: <Sqlite as Backend>::RawValue<'_>,
|
|
||||||
) -> diesel::deserialize::Result<Self> {
|
|
||||||
let s = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
|
let s = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
|
||||||
match s.as_str() {
|
match s.as_str() {
|
||||||
"pending" => Ok(Self::Pending),
|
"pending" => Ok(Self::Pending),
|
||||||
@@ -178,6 +177,141 @@ pub mod types {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A governance proposal and the parameters it carries.
|
||||||
|
#[derive(Debug, Clone, EnumDiscriminants)]
|
||||||
|
#[strum_discriminants(
|
||||||
|
name(ProposalKindTag),
|
||||||
|
vis(pub),
|
||||||
|
derive(Display, EnumString, IntoStaticStr, AsExpression, FromSqlRow),
|
||||||
|
diesel(sql_type = Text),
|
||||||
|
strum(serialize_all = "snake_case")
|
||||||
|
)]
|
||||||
|
pub enum ProposalKind {
|
||||||
|
ApproveSdkClient {
|
||||||
|
client_id: i32,
|
||||||
|
},
|
||||||
|
GrantWalletAccess {
|
||||||
|
wallet_id: i32,
|
||||||
|
client_id: i32,
|
||||||
|
},
|
||||||
|
ReplaceOperator {
|
||||||
|
old_operator_id: i32,
|
||||||
|
new_pubkey: Vec<u8>,
|
||||||
|
},
|
||||||
|
TriggerRekey,
|
||||||
|
ApprovePersistentGrant {
|
||||||
|
payload_bytes: Vec<u8>,
|
||||||
|
},
|
||||||
|
ApproveOneOffTransaction {
|
||||||
|
payload_bytes: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProposalKind {
|
||||||
|
pub fn encode_payload(&self) -> Vec<u8> {
|
||||||
|
match self {
|
||||||
|
Self::ApproveSdkClient { client_id } => client_id.to_be_bytes().to_vec(),
|
||||||
|
Self::GrantWalletAccess {
|
||||||
|
wallet_id,
|
||||||
|
client_id,
|
||||||
|
} => {
|
||||||
|
let mut buf = Vec::with_capacity(8);
|
||||||
|
buf.extend_from_slice(&wallet_id.to_be_bytes());
|
||||||
|
buf.extend_from_slice(&client_id.to_be_bytes());
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
Self::ReplaceOperator {
|
||||||
|
old_operator_id,
|
||||||
|
new_pubkey,
|
||||||
|
} => {
|
||||||
|
let len = u32::try_from(new_pubkey.len()).expect("pubkey len fits in u32");
|
||||||
|
let mut buf = Vec::with_capacity(4 + 4 + new_pubkey.len());
|
||||||
|
buf.extend_from_slice(&old_operator_id.to_be_bytes());
|
||||||
|
buf.extend_from_slice(&len.to_be_bytes());
|
||||||
|
buf.extend_from_slice(new_pubkey);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
Self::TriggerRekey => vec![],
|
||||||
|
Self::ApprovePersistentGrant { payload_bytes }
|
||||||
|
| Self::ApproveOneOffTransaction { payload_bytes } => payload_bytes.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Key-rotation proposals require every operator to approve (§3.3).
|
||||||
|
pub fn decode(tag: ProposalKindTag, payload: &[u8]) -> Result<Self, String> {
|
||||||
|
match tag {
|
||||||
|
ProposalKindTag::ApproveSdkClient => {
|
||||||
|
let bytes = <[u8; 4]>::try_from(payload)
|
||||||
|
.map_err(|_| "invalid payload for approve_sdk_client".to_owned())?;
|
||||||
|
Ok(Self::ApproveSdkClient {
|
||||||
|
client_id: i32::from_be_bytes(bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ProposalKindTag::GrantWalletAccess => {
|
||||||
|
let bytes = <[u8; 8]>::try_from(payload)
|
||||||
|
.map_err(|_| "invalid payload for grant_wallet_access".to_owned())?;
|
||||||
|
Ok(Self::GrantWalletAccess {
|
||||||
|
wallet_id: i32::from_be_bytes(bytes[..4].try_into().unwrap()),
|
||||||
|
client_id: i32::from_be_bytes(bytes[4..].try_into().unwrap()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ProposalKindTag::ReplaceOperator => {
|
||||||
|
let (id_bytes, rest) = payload
|
||||||
|
.split_first_chunk::<4>()
|
||||||
|
.ok_or_else(|| "replace_operator payload too short".to_owned())?;
|
||||||
|
let old_operator_id = i32::from_be_bytes(*id_bytes);
|
||||||
|
let (len_bytes, rest) = rest
|
||||||
|
.split_first_chunk::<4>()
|
||||||
|
.ok_or_else(|| "replace_operator payload too short".to_owned())?;
|
||||||
|
let len = u32::from_be_bytes(*len_bytes);
|
||||||
|
let len = usize::try_from(len).unwrap_or(usize::MAX);
|
||||||
|
let new_pubkey = rest
|
||||||
|
.get(..len)
|
||||||
|
.ok_or_else(|| "replace_operator payload truncated".to_owned())?
|
||||||
|
.to_vec();
|
||||||
|
Ok(Self::ReplaceOperator {
|
||||||
|
old_operator_id,
|
||||||
|
new_pubkey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ProposalKindTag::TriggerRekey => Ok(Self::TriggerRekey),
|
||||||
|
ProposalKindTag::ApprovePersistentGrant => Ok(Self::ApprovePersistentGrant {
|
||||||
|
payload_bytes: payload.to_vec(),
|
||||||
|
}),
|
||||||
|
ProposalKindTag::ApproveOneOffTransaction => Ok(Self::ApproveOneOffTransaction {
|
||||||
|
payload_bytes: payload.to_vec(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProposalKindTag {
|
||||||
|
/// Key-rotation proposals require every operator to approve (§3.3).
|
||||||
|
#[must_use]
|
||||||
|
pub const fn requires_full_quorum(self) -> bool {
|
||||||
|
matches!(self, Self::ReplaceOperator | Self::TriggerRekey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSql<Text, Sqlite> for ProposalKindTag {
|
||||||
|
fn to_sql<'b>(
|
||||||
|
&'b self,
|
||||||
|
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
|
||||||
|
) -> diesel::serialize::Result {
|
||||||
|
<str as ToSql<Text, Sqlite>>::to_sql(<&'static str>::from(*self), out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromSql<Text, Sqlite> for ProposalKindTag {
|
||||||
|
fn from_sql(
|
||||||
|
bytes: <Sqlite as Backend>::RawValue<'_>,
|
||||||
|
) -> diesel::deserialize::Result<Self> {
|
||||||
|
let s = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
|
||||||
|
s.parse()
|
||||||
|
.map_err(|_| format!("Unknown proposal kind: {s}").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|
||||||
@@ -480,7 +614,7 @@ pub struct IntegrityEnvelope {
|
|||||||
#[diesel(table_name = schema::proposal, check_for_backend(Sqlite))]
|
#[diesel(table_name = schema::proposal, check_for_backend(Sqlite))]
|
||||||
pub struct Proposal {
|
pub struct Proposal {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub kind: String,
|
pub kind: ProposalKindTag,
|
||||||
pub payload: Vec<u8>,
|
pub payload: Vec<u8>,
|
||||||
pub initiator_id: i32,
|
pub initiator_id: i32,
|
||||||
pub created_at: SqliteTimestamp,
|
pub created_at: SqliteTimestamp,
|
||||||
@@ -491,7 +625,7 @@ pub struct Proposal {
|
|||||||
#[derive(Debug, Insertable)]
|
#[derive(Debug, Insertable)]
|
||||||
#[diesel(table_name = schema::proposal, check_for_backend(Sqlite))]
|
#[diesel(table_name = schema::proposal, check_for_backend(Sqlite))]
|
||||||
pub struct NewProposal {
|
pub struct NewProposal {
|
||||||
pub kind: String,
|
pub kind: ProposalKindTag,
|
||||||
pub payload: Vec<u8>,
|
pub payload: Vec<u8>,
|
||||||
pub initiator_id: i32,
|
pub initiator_id: i32,
|
||||||
// status defaults to 'pending' at the DB layer
|
// status defaults to 'pending' at the DB layer
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
actors::proposal_manager::{Error as ProposalError, ProposalKind, VoteOutcome},
|
actors::proposal_manager::{Error as ProposalError, VoteOutcome},
|
||||||
|
db::models::ProposalKind,
|
||||||
peers::operator::{
|
peers::operator::{
|
||||||
OperatorSession,
|
OperatorSession,
|
||||||
session::handlers::{HandleCastVote, HandleCreateProposal, HandleQueryPending},
|
session::handlers::{HandleCastVote, HandleCreateProposal, HandleQueryPending},
|
||||||
@@ -133,7 +134,7 @@ async fn handle_query(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| proto_gov::ProposalSummary {
|
.map(|s| proto_gov::ProposalSummary {
|
||||||
id: s.id,
|
id: s.id,
|
||||||
kind: s.kind,
|
kind: <&'static str>::from(s.kind).to_owned(),
|
||||||
initiator_id: s.initiator_id,
|
initiator_id: s.initiator_id,
|
||||||
expires_at: s.expires_at.0.timestamp(),
|
expires_at: s.expires_at.0.timestamp(),
|
||||||
approve_count: s.approve_count,
|
approve_count: s.approve_count,
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ impl OperatorSession {
|
|||||||
#[message]
|
#[message]
|
||||||
pub(crate) async fn handle_create_proposal(
|
pub(crate) async fn handle_create_proposal(
|
||||||
&mut self,
|
&mut self,
|
||||||
kind: crate::actors::proposal_manager::ProposalKind,
|
kind: crate::db::models::ProposalKind,
|
||||||
ttl_secs: Option<i64>,
|
ttl_secs: Option<i64>,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<i32, Error> {
|
||||||
use crate::actors::proposal_manager::CreateProposal;
|
use crate::actors::proposal_manager::CreateProposal;
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ use arbiter_server::{
|
|||||||
GlobalActors,
|
GlobalActors,
|
||||||
proposal_manager::{
|
proposal_manager::{
|
||||||
CancelRecoveryWakeup, CastRecoveryVote, CastVote, CreateProposal,
|
CancelRecoveryWakeup, CastRecoveryVote, CastVote, CreateProposal,
|
||||||
Error as ProposalError, ProposalKind, QueryPending,
|
Error as ProposalError, QueryPending, RequestRecoveryWakeup, VoteOutcome,
|
||||||
RequestRecoveryWakeup, VoteOutcome,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
crypto::KeyCell,
|
crypto::KeyCell,
|
||||||
db,
|
db::{self, models::ProposalKind},
|
||||||
};
|
};
|
||||||
use arbiter_server::actors::vault::Bootstrap;
|
use arbiter_server::actors::vault::Bootstrap;
|
||||||
use arbiter_server::db::schema::{
|
use arbiter_server::db::schema::{
|
||||||
|
|||||||
Reference in New Issue
Block a user