Skip to main content

miden_standards/account/access/
ownable2step.rs

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