Skip to main content

miden_standards/account/access/
ownable2step.rs

1use miden_protocol::account::component::{
2    AccountComponentMetadata,
3    FeltSchema,
4    StorageSchema,
5    StorageSlotSchema,
6};
7use miden_protocol::account::{
8    AccountComponent,
9    AccountId,
10    AccountStorage,
11    AccountType,
12    StorageSlot,
13    StorageSlotName,
14};
15use miden_protocol::errors::AccountIdError;
16use miden_protocol::utils::sync::LazyLock;
17use miden_protocol::{Felt, Word};
18
19use crate::account::components::ownable2step_library;
20
21static OWNER_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
22    StorageSlotName::new("miden::standards::access::ownable2step::owner_config")
23        .expect("storage slot name should be valid")
24});
25
26/// Two-step ownership management for account components.
27///
28/// This struct holds the current owner and any nominated (pending) owner. A nominated owner
29/// must explicitly accept the transfer before it takes effect, preventing accidental transfers
30/// to incorrect addresses.
31///
32/// ## Storage Layout
33///
34/// The ownership data is stored in a single word:
35///
36/// ```text
37/// Word:  [owner_suffix, owner_prefix, nominated_owner_suffix, nominated_owner_prefix]
38///         word[0]       word[1]        word[2]                  word[3]
39/// ```
40pub struct Ownable2Step {
41    /// The current owner of the component. `None` when ownership has been renounced.
42    owner: Option<AccountId>,
43    nominated_owner: Option<AccountId>,
44}
45
46impl Ownable2Step {
47    /// The name of the component.
48    pub const NAME: &'static str = "miden::standards::components::access::ownable2step";
49
50    // CONSTRUCTORS
51    // --------------------------------------------------------------------------------------------
52
53    /// Creates a new [`Ownable2Step`] with the given owner and no nominated owner.
54    pub fn new(owner: AccountId) -> Self {
55        Self {
56            owner: Some(owner),
57            nominated_owner: None,
58        }
59    }
60
61    /// Reads ownership data from account storage, validating any non-zero account IDs.
62    ///
63    /// Returns an error if either owner or nominated owner contains an invalid (but non-zero)
64    /// account ID.
65    pub fn try_from_storage(storage: &AccountStorage) -> Result<Self, Ownable2StepError> {
66        let word: Word = storage
67            .get_item(Self::slot_name())
68            .map_err(Ownable2StepError::StorageLookupFailed)?;
69
70        Self::try_from_word(word)
71    }
72
73    /// Reconstructs an [`Ownable2Step`] from a raw storage word.
74    ///
75    /// Format: `[owner_suffix, owner_prefix, nominated_suffix, nominated_prefix]`
76    pub fn try_from_word(word: Word) -> Result<Self, Ownable2StepError> {
77        let owner = account_id_from_felt_pair(word[0], word[1])
78            .map_err(Ownable2StepError::InvalidOwnerId)?;
79
80        let nominated_owner = account_id_from_felt_pair(word[2], word[3])
81            .map_err(Ownable2StepError::InvalidNominatedOwnerId)?;
82
83        Ok(Self { owner, nominated_owner })
84    }
85
86    // PUBLIC ACCESSORS
87    // --------------------------------------------------------------------------------------------
88
89    /// Returns the [`StorageSlotName`] where ownership data is stored.
90    pub fn slot_name() -> &'static StorageSlotName {
91        &OWNER_CONFIG_SLOT_NAME
92    }
93
94    /// Returns the storage slot schema for the ownership configuration slot.
95    pub fn slot_schema() -> (StorageSlotName, StorageSlotSchema) {
96        (
97            Self::slot_name().clone(),
98            StorageSlotSchema::value(
99                "Ownership data (owner and nominated owner)",
100                [
101                    FeltSchema::felt("owner_suffix"),
102                    FeltSchema::felt("owner_prefix"),
103                    FeltSchema::felt("nominated_suffix"),
104                    FeltSchema::felt("nominated_prefix"),
105                ],
106            ),
107        )
108    }
109
110    /// Returns the current owner, or `None` if ownership has been renounced.
111    pub fn owner(&self) -> Option<AccountId> {
112        self.owner
113    }
114
115    /// Returns the nominated owner, or `None` if no transfer is in progress.
116    pub fn nominated_owner(&self) -> Option<AccountId> {
117        self.nominated_owner
118    }
119
120    /// Converts this ownership data into a [`StorageSlot`].
121    pub fn to_storage_slot(&self) -> StorageSlot {
122        StorageSlot::with_value(Self::slot_name().clone(), self.to_word())
123    }
124
125    /// Converts this ownership data into a raw [`Word`].
126    pub fn to_word(&self) -> Word {
127        let (owner_suffix, owner_prefix) = match self.owner {
128            Some(id) => (id.suffix(), id.prefix().as_felt()),
129            None => (Felt::ZERO, Felt::ZERO),
130        };
131        let (nominated_suffix, nominated_prefix) = match self.nominated_owner {
132            Some(id) => (id.suffix(), id.prefix().as_felt()),
133            None => (Felt::ZERO, Felt::ZERO),
134        };
135        [owner_suffix, owner_prefix, nominated_suffix, nominated_prefix].into()
136    }
137
138    /// Returns the [`AccountComponentMetadata`] for this component.
139    pub fn component_metadata() -> AccountComponentMetadata {
140        let storage_schema =
141            StorageSchema::new([Self::slot_schema()]).expect("storage schema should be valid");
142
143        AccountComponentMetadata::new(Self::NAME, AccountType::all())
144            .with_description("Two-step ownership management component")
145            .with_storage_schema(storage_schema)
146    }
147}
148
149impl From<Ownable2Step> for AccountComponent {
150    fn from(ownership: Ownable2Step) -> Self {
151        let storage_slot = ownership.to_storage_slot();
152        let metadata = Ownable2Step::component_metadata();
153
154        AccountComponent::new(ownable2step_library(), vec![storage_slot], metadata).expect(
155            "Ownable2Step component should satisfy the requirements of a valid account component",
156        )
157    }
158}
159
160// OWNABLE2STEP ERROR
161// ================================================================================================
162
163/// Errors that can occur when reading [`Ownable2Step`] data from storage.
164#[derive(Debug, thiserror::Error)]
165pub enum Ownable2StepError {
166    #[error("failed to read ownership slot from storage")]
167    StorageLookupFailed(#[source] miden_protocol::errors::AccountError),
168    #[error("invalid owner account ID in storage")]
169    InvalidOwnerId(#[source] AccountIdError),
170    #[error("invalid nominated owner account ID in storage")]
171    InvalidNominatedOwnerId(#[source] AccountIdError),
172}
173
174// HELPERS
175// ================================================================================================
176
177/// Constructs an `Option<AccountId>` from a suffix/prefix felt pair.
178/// Returns `Ok(None)` when both felts are zero (renounced / no nomination).
179fn account_id_from_felt_pair(
180    suffix: Felt,
181    prefix: Felt,
182) -> Result<Option<AccountId>, AccountIdError> {
183    if suffix == Felt::ZERO && prefix == Felt::ZERO {
184        Ok(None)
185    } else {
186        AccountId::try_from_elements(suffix, prefix).map(Some)
187    }
188}