use std::path::Path;
use miden_node_db::sqlite::{DbReader, DbWriter, WriteTx};
use miden_node_tracing::miden_instrument;
use miden_protocol::account::AccountId;
use thiserror::Error;
use crate::{COMPONENT, DatabaseError};
mod invitation;
mod migrations;
mod queries;
pub use invitation::{InvalidInvitationCode, InvitationCode};
#[cfg(test)]
mod tests;
#[derive(Clone, Debug)]
pub struct InvitationEntry {
pub invitation_code: InvitationCode,
pub account_id: Option<AccountId>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InvitationImportOutcome {
pub invitation_added: bool,
pub registered_account: Option<AccountId>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InvitationStatus {
Unknown,
Unused,
Registered(AccountId),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InvitationInfo {
pub account_id: Option<AccountId>,
pub allowlisted_at: i64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RegistrationOutcome {
Registered,
AlreadyRegistered,
}
#[derive(Debug, Error)]
pub enum AllowlistError {
#[error("invitation code does not exist")]
InvitationNotFound,
#[error("invitation code is already registered to another account")]
InvitationAlreadyUsed,
#[error("account {0} is already registered")]
AccountAlreadyRegistered(AccountId),
#[error("account registry database operation failed")]
Database(#[source] DatabaseError),
}
#[derive(Clone)]
pub struct AccountAllowlistReader {
db: DbReader,
}
impl AccountAllowlistReader {
#[miden_instrument(
target = COMPONENT,
name = "store.allowlist.allowlisted_at",
fields(account.id = account_id),
err,
)]
pub async fn allowlisted_at(
&self,
account_id: AccountId,
) -> Result<Option<i64>, DatabaseError> {
self.db
.read("allowlist.allowlisted_at", move |tx| queries::allowlisted_at(tx, account_id))
.await
.map_err(DatabaseError::DatabaseError)
}
#[miden_instrument(target = COMPONENT, name = "store.allowlist.invitation_info", err)]
pub async fn invitation_info(
&self,
invitation_code: InvitationCode,
) -> Result<Option<InvitationInfo>, DatabaseError> {
self.db
.read("allowlist.invitation_info", move |tx| {
queries::invitation_info(tx, &invitation_code)
})
.await
.map_err(DatabaseError::DatabaseError)
}
#[miden_instrument(
target = COMPONENT,
name = "store.allowlist.contains_account",
fields(account.id = account_id),
err,
)]
pub async fn contains_account(&self, account_id: AccountId) -> Result<bool, DatabaseError> {
self.db
.read("allowlist.contains_account", move |tx| {
queries::contains_account(tx, account_id)
})
.await
.map_err(DatabaseError::DatabaseError)
}
#[miden_instrument(target = COMPONENT, name = "store.allowlist.invitation_status", err)]
pub async fn invitation_status(
&self,
invitation_code: InvitationCode,
) -> Result<InvitationStatus, DatabaseError> {
self.db
.read("allowlist.invitation_status", move |tx| {
queries::invitation_status(tx, &invitation_code)
})
.await
.map_err(DatabaseError::DatabaseError)
}
}
pub struct AccountAllowlist {
writer: DbWriter,
reader: AccountAllowlistReader,
}
impl std::ops::Deref for AccountAllowlist {
type Target = AccountAllowlistReader;
fn deref(&self) -> &Self::Target {
&self.reader
}
}
impl AccountAllowlist {
pub fn bootstrap(database_filepath: impl AsRef<Path>) -> Result<(), DatabaseError> {
let migrator = migrations::migrator()
.map_err(miden_node_db::DatabaseError::migration)
.map_err(DatabaseError::DatabaseError)?;
migrator
.bootstrap(database_filepath)
.map_err(miden_node_db::DatabaseError::migration)
.map_err(DatabaseError::DatabaseError)
}
pub fn load(database_filepath: impl AsRef<Path>) -> Result<Self, DatabaseError> {
let database_filepath = database_filepath.as_ref();
let migrator = migrations::migrator()
.map_err(miden_node_db::DatabaseError::migration)
.map_err(DatabaseError::DatabaseError)?;
migrator
.verify_latest_schema(database_filepath)
.map_err(miden_node_db::DatabaseError::migration)
.map_err(DatabaseError::DatabaseError)?;
let (writer, reader) =
miden_node_db::sqlite::open(database_filepath).map_err(DatabaseError::DatabaseError)?;
Ok(Self {
writer,
reader: AccountAllowlistReader { db: reader },
})
}
pub fn migrate(database_filepath: impl AsRef<Path>) -> Result<(), DatabaseError> {
let migrator = migrations::migrator()
.map_err(miden_node_db::DatabaseError::migration)
.map_err(DatabaseError::DatabaseError)?;
migrator
.migrate(database_filepath)
.map_err(miden_node_db::DatabaseError::migration)
.map_err(DatabaseError::DatabaseError)
}
pub fn reader(&self) -> AccountAllowlistReader {
self.reader.clone()
}
#[miden_instrument(
target = COMPONENT,
name = "store.allowlist.import_invitation",
fields(account.id = entry.account_id),
err,
)]
pub async fn import_invitation(
&self,
entry: InvitationEntry,
) -> Result<InvitationImportOutcome, AllowlistError> {
self.transact("allowlist.import_invitation", move |tx| {
queries::import_invitation(tx, &entry)
})
.await
}
#[miden_instrument(
target = COMPONENT,
name = "store.allowlist.add_account",
fields(account.id = account_id),
err,
)]
pub async fn add_account(&self, account_id: AccountId) -> Result<bool, DatabaseError> {
self.writer
.write("allowlist.add_account", move |tx| queries::add_account(tx, account_id))
.await
.map_err(DatabaseError::DatabaseError)
}
#[miden_instrument(
target = COMPONENT,
name = "store.allowlist.register_account",
fields(account.id = account_id),
err,
)]
pub async fn register_account(
&self,
invitation_code: InvitationCode,
account_id: AccountId,
) -> Result<RegistrationOutcome, AllowlistError> {
self.transact("allowlist.register_account", move |tx| {
queries::register_account(tx, &invitation_code, account_id)
})
.await
}
async fn transact<T: Send + 'static>(
&self,
name: &'static str,
query: impl FnOnce(&WriteTx<'_>) -> Result<T, AllowlistError> + Send + 'static,
) -> Result<T, AllowlistError> {
let tx = self
.writer
.begin_write()
.await
.map_err(DatabaseError::DatabaseError)
.map_err(AllowlistError::Database)?;
let result = tx
.run(name, move |tx| Ok::<_, miden_node_db::DatabaseError>(query(tx)))
.await
.map_err(DatabaseError::DatabaseError)
.map_err(AllowlistError::Database)
.and_then(std::convert::identity);
match result {
Ok(value) => {
tx.commit()
.await
.map_err(DatabaseError::DatabaseError)
.map_err(AllowlistError::Database)?;
Ok(value)
},
Err(error) => {
tx.rollback()
.await
.map_err(DatabaseError::DatabaseError)
.map_err(AllowlistError::Database)?;
Err(error)
},
}
}
}