miden_standards/account/access/
ownable2step.rs1use 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
26pub struct Ownable2Step {
41 owner: Option<AccountId>,
43 nominated_owner: Option<AccountId>,
44}
45
46impl Ownable2Step {
47 pub const NAME: &'static str = "miden::standards::components::access::ownable2step";
49
50 pub fn new(owner: AccountId) -> Self {
55 Self {
56 owner: Some(owner),
57 nominated_owner: None,
58 }
59 }
60
61 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 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 pub fn slot_name() -> &'static StorageSlotName {
91 &OWNER_CONFIG_SLOT_NAME
92 }
93
94 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 pub fn owner(&self) -> Option<AccountId> {
112 self.owner
113 }
114
115 pub fn nominated_owner(&self) -> Option<AccountId> {
117 self.nominated_owner
118 }
119
120 pub fn to_storage_slot(&self) -> StorageSlot {
122 StorageSlot::with_value(Self::slot_name().clone(), self.to_word())
123 }
124
125 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 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#[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
174fn 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}