Skip to main content

miden_node_rpc/server/
admission.rs

1use std::sync::Arc;
2
3use miden_node_store::DatabaseError;
4use miden_node_store::allowlist::{AccountAllowlist, InvitationCode, InvitationStatus};
5use miden_node_tracing::{error, miden_instrument};
6use miden_protocol::account::{AccountId, AccountUpdateDetails};
7use miden_protocol::transaction::TxAccountUpdate;
8use miden_standards::account::auth::NetworkAccount;
9use tonic::Status;
10
11use crate::{COMPONENT, FundingClient, LOG_TARGET};
12
13/// Account creation policy shared by the public and internal sequencer APIs.
14#[derive(Clone)]
15pub struct AccountAdmission {
16    pub(crate) allowlist: Arc<AccountAllowlist>,
17    pub(crate) funding: Option<FundingClient>,
18    disabled: bool,
19}
20
21impl AccountAdmission {
22    pub fn enabled(allowlist: Arc<AccountAllowlist>) -> Self {
23        Self {
24            allowlist,
25            funding: None,
26            disabled: false,
27        }
28    }
29
30    pub fn disabled(allowlist: Arc<AccountAllowlist>) -> Self {
31        Self { allowlist, funding: None, disabled: true }
32    }
33
34    pub(crate) fn is_disabled(&self) -> bool {
35        self.disabled
36    }
37
38    #[must_use]
39    pub fn with_funding_client(mut self, client: Option<FundingClient>) -> Self {
40        self.funding = client;
41        self
42    }
43
44    /// Returns true if enforcement is disabled or the account is allowlisted.
45    pub(crate) async fn is_account_allowed(
46        &self,
47        account_id: AccountId,
48    ) -> Result<bool, DatabaseError> {
49        if self.disabled {
50            return Ok(true);
51        }
52        self.allowlist.contains_account(account_id).await
53    }
54
55    /// Returns true if the invitation code can be used to register an account. The code must be
56    /// registered in the invitation registry and unused.
57    ///
58    /// Always returns true when account allowlist enforcement is disabled.
59    pub(crate) async fn is_invitation_code_valid(
60        &self,
61        invitation_code: InvitationCode,
62    ) -> Result<bool, DatabaseError> {
63        if self.disabled {
64            return Ok(true);
65        }
66        let status = self.allowlist.invitation_status(invitation_code).await?;
67        Ok(status == InvitationStatus::Unused)
68    }
69
70    /// Rejects the submission if it creates an unregistered, non-network account.
71    #[miden_instrument(
72        target = COMPONENT,
73        name = "account_admission.check",
74        fields(account.id = update.account_id()),
75        err,
76    )]
77    pub(crate) async fn check(&self, update: &TxAccountUpdate) -> tonic::Result<()> {
78        if self.disabled || !update.initial_state_commitment().is_empty() {
79            return Ok(());
80        }
81
82        // New public accounts include their full state. Use the store's network-account
83        // classification rule before the account exists on chain.
84        if let AccountUpdateDetails::Public(patch) = update.details() {
85            let account = patch
86                .try_to_new_account()
87                .map_err(|error| Status::invalid_argument(error.to_string()))?;
88            if NetworkAccount::new(account).is_ok() {
89                return Ok(());
90            }
91        }
92
93        let account_id = update.account_id();
94        let allowed = self.is_account_allowed(account_id).await.map_err(|err| {
95            error!(err, target: LOG_TARGET, "Account allowlist lookup failed");
96            Status::internal("account allowlist lookup failed")
97        })?;
98        if !allowed {
99            return Err(Status::permission_denied(format!(
100                "account {account_id} is not registered"
101            )));
102        }
103
104        Ok(())
105    }
106}