miden_node_store/allowlist/
mod.rs1use std::path::Path;
7
8use miden_node_db::sqlite::{DbReader, DbWriter, WriteTx};
9use miden_node_tracing::miden_instrument;
10use miden_protocol::account::AccountId;
11use thiserror::Error;
12
13use crate::{COMPONENT, DatabaseError};
14
15mod invitation;
16mod migrations;
17mod queries;
18
19pub use invitation::{InvalidInvitationCode, InvitationCode};
20
21#[cfg(test)]
22mod tests;
23
24#[derive(Clone, Debug)]
26pub struct InvitationEntry {
27 pub invitation_code: InvitationCode,
28 pub account_id: Option<AccountId>,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct InvitationImportOutcome {
34 pub invitation_added: bool,
35 pub registered_account: Option<AccountId>,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum InvitationStatus {
42 Unknown,
43 Unused,
44 Registered(AccountId),
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct InvitationInfo {
50 pub account_id: Option<AccountId>,
51 pub allowlisted_at: i64,
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum RegistrationOutcome {
57 Registered,
58 AlreadyRegistered,
60}
61
62#[derive(Debug, Error)]
64pub enum AllowlistError {
65 #[error("invitation code does not exist")]
66 InvitationNotFound,
67 #[error("invitation code is already registered to another account")]
68 InvitationAlreadyUsed,
69 #[error("account {0} is already registered")]
70 AccountAlreadyRegistered(AccountId),
71 #[error("account registry database operation failed")]
72 Database(#[source] DatabaseError),
73}
74
75#[derive(Clone)]
77pub struct AccountAllowlistReader {
78 db: DbReader,
79}
80
81impl AccountAllowlistReader {
82 #[miden_instrument(
84 target = COMPONENT,
85 name = "store.allowlist.allowlisted_at",
86 fields(account.id = account_id),
87 err,
88 )]
89 pub async fn allowlisted_at(
90 &self,
91 account_id: AccountId,
92 ) -> Result<Option<i64>, DatabaseError> {
93 self.db
94 .read("allowlist.allowlisted_at", move |tx| queries::allowlisted_at(tx, account_id))
95 .await
96 .map_err(DatabaseError::DatabaseError)
97 }
98
99 #[miden_instrument(target = COMPONENT, name = "store.allowlist.invitation_info", err)]
101 pub async fn invitation_info(
102 &self,
103 invitation_code: InvitationCode,
104 ) -> Result<Option<InvitationInfo>, DatabaseError> {
105 self.db
106 .read("allowlist.invitation_info", move |tx| {
107 queries::invitation_info(tx, &invitation_code)
108 })
109 .await
110 .map_err(DatabaseError::DatabaseError)
111 }
112
113 #[miden_instrument(
115 target = COMPONENT,
116 name = "store.allowlist.contains_account",
117 fields(account.id = account_id),
118 err,
119 )]
120 pub async fn contains_account(&self, account_id: AccountId) -> Result<bool, DatabaseError> {
121 self.db
122 .read("allowlist.contains_account", move |tx| {
123 queries::contains_account(tx, account_id)
124 })
125 .await
126 .map_err(DatabaseError::DatabaseError)
127 }
128
129 #[miden_instrument(target = COMPONENT, name = "store.allowlist.invitation_status", err)]
131 pub async fn invitation_status(
132 &self,
133 invitation_code: InvitationCode,
134 ) -> Result<InvitationStatus, DatabaseError> {
135 self.db
136 .read("allowlist.invitation_status", move |tx| {
137 queries::invitation_status(tx, &invitation_code)
138 })
139 .await
140 .map_err(DatabaseError::DatabaseError)
141 }
142}
143
144pub struct AccountAllowlist {
151 writer: DbWriter,
152 reader: AccountAllowlistReader,
153}
154
155impl std::ops::Deref for AccountAllowlist {
156 type Target = AccountAllowlistReader;
157
158 fn deref(&self) -> &Self::Target {
159 &self.reader
160 }
161}
162
163impl AccountAllowlist {
164 pub fn bootstrap(database_filepath: impl AsRef<Path>) -> Result<(), DatabaseError> {
168 let migrator = migrations::migrator()
169 .map_err(miden_node_db::DatabaseError::migration)
170 .map_err(DatabaseError::DatabaseError)?;
171 migrator
172 .bootstrap(database_filepath)
173 .map_err(miden_node_db::DatabaseError::migration)
174 .map_err(DatabaseError::DatabaseError)
175 }
176
177 pub fn load(database_filepath: impl AsRef<Path>) -> Result<Self, DatabaseError> {
181 let database_filepath = database_filepath.as_ref();
182 let migrator = migrations::migrator()
183 .map_err(miden_node_db::DatabaseError::migration)
184 .map_err(DatabaseError::DatabaseError)?;
185 migrator
186 .verify_latest_schema(database_filepath)
187 .map_err(miden_node_db::DatabaseError::migration)
188 .map_err(DatabaseError::DatabaseError)?;
189 let (writer, reader) =
190 miden_node_db::sqlite::open(database_filepath).map_err(DatabaseError::DatabaseError)?;
191 Ok(Self {
192 writer,
193 reader: AccountAllowlistReader { db: reader },
194 })
195 }
196
197 pub fn migrate(database_filepath: impl AsRef<Path>) -> Result<(), DatabaseError> {
199 let migrator = migrations::migrator()
200 .map_err(miden_node_db::DatabaseError::migration)
201 .map_err(DatabaseError::DatabaseError)?;
202 migrator
203 .migrate(database_filepath)
204 .map_err(miden_node_db::DatabaseError::migration)
205 .map_err(DatabaseError::DatabaseError)
206 }
207
208 pub fn reader(&self) -> AccountAllowlistReader {
210 self.reader.clone()
211 }
212
213 #[miden_instrument(
220 target = COMPONENT,
221 name = "store.allowlist.import_invitation",
222 fields(account.id = entry.account_id),
223 err,
224 )]
225 pub async fn import_invitation(
226 &self,
227 entry: InvitationEntry,
228 ) -> Result<InvitationImportOutcome, AllowlistError> {
229 self.transact("allowlist.import_invitation", move |tx| {
230 queries::import_invitation(tx, &entry)
231 })
232 .await
233 }
234
235 #[miden_instrument(
239 target = COMPONENT,
240 name = "store.allowlist.add_account",
241 fields(account.id = account_id),
242 err,
243 )]
244 pub async fn add_account(&self, account_id: AccountId) -> Result<bool, DatabaseError> {
245 self.writer
246 .write("allowlist.add_account", move |tx| queries::add_account(tx, account_id))
247 .await
248 .map_err(DatabaseError::DatabaseError)
249 }
250
251 #[miden_instrument(
256 target = COMPONENT,
257 name = "store.allowlist.register_account",
258 fields(account.id = account_id),
259 err,
260 )]
261 pub async fn register_account(
262 &self,
263 invitation_code: InvitationCode,
264 account_id: AccountId,
265 ) -> Result<RegistrationOutcome, AllowlistError> {
266 self.transact("allowlist.register_account", move |tx| {
267 queries::register_account(tx, &invitation_code, account_id)
268 })
269 .await
270 }
271
272 async fn transact<T: Send + 'static>(
273 &self,
274 name: &'static str,
275 query: impl FnOnce(&WriteTx<'_>) -> Result<T, AllowlistError> + Send + 'static,
276 ) -> Result<T, AllowlistError> {
277 let tx = self
278 .writer
279 .begin_write()
280 .await
281 .map_err(DatabaseError::DatabaseError)
282 .map_err(AllowlistError::Database)?;
283 let result = tx
284 .run(name, move |tx| Ok::<_, miden_node_db::DatabaseError>(query(tx)))
285 .await
286 .map_err(DatabaseError::DatabaseError)
287 .map_err(AllowlistError::Database)
288 .and_then(std::convert::identity);
289
290 match result {
291 Ok(value) => {
292 tx.commit()
293 .await
294 .map_err(DatabaseError::DatabaseError)
295 .map_err(AllowlistError::Database)?;
296 Ok(value)
297 },
298 Err(error) => {
299 tx.rollback()
300 .await
301 .map_err(DatabaseError::DatabaseError)
302 .map_err(AllowlistError::Database)?;
303 Err(error)
304 },
305 }
306 }
307}