Skip to main content

miden_standards/account/wallets/
mod.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::component::{AccountComponentCode, AccountComponentMetadata};
4use miden_protocol::account::{
5    Account,
6    AccountBuilder,
7    AccountComponent,
8    AccountComponentName,
9    AccountProcedureRoot,
10    AccountType,
11};
12use miden_protocol::errors::AccountError;
13
14use crate::account::account_component_code;
15use crate::account::auth::{
16    Approver,
17    ApproverSet,
18    AuthGuardedMultisig,
19    AuthGuardedMultisigConfig,
20    AuthMultisig,
21    AuthMultisigConfig,
22    AuthSingleSig,
23    GuardianConfig,
24};
25use crate::procedure_root;
26
27// BASIC WALLET
28// ================================================================================================
29
30account_component_code!(BASIC_WALLET_CODE, "miden-standards-wallets-basic-wallet.masp");
31
32// PROCEDURE ROOTS
33// ================================================================================================
34
35/// MASL library namespace used for procedure-root lookups. Distinct from [`BasicWallet::NAME`],
36/// which mirrors the standards-side MASM module path.
37const BASIC_WALLET_LIBRARY_PATH: &str = "miden::standards::components::wallets::basic_wallet";
38
39// Initialize the procedure root of the `receive_asset` procedure of the Basic Wallet only once.
40procedure_root!(
41    BASIC_WALLET_RECEIVE_ASSET,
42    BASIC_WALLET_LIBRARY_PATH,
43    BasicWallet::RECEIVE_ASSET_PROC_NAME,
44    BasicWallet::code()
45);
46
47// Initialize the procedure root of the `move_asset_to_note` procedure of the Basic Wallet only
48// once.
49procedure_root!(
50    BASIC_WALLET_MOVE_ASSET_TO_NOTE,
51    BASIC_WALLET_LIBRARY_PATH,
52    BasicWallet::MOVE_ASSET_TO_NOTE_PROC_NAME,
53    BasicWallet::code()
54);
55
56// Initialize the procedure root of the `create_note` procedure of the Basic Wallet only once.
57procedure_root!(
58    BASIC_WALLET_CREATE_NOTE,
59    BASIC_WALLET_LIBRARY_PATH,
60    BasicWallet::CREATE_NOTE_PROC_NAME,
61    BasicWallet::code()
62);
63
64/// An [`AccountComponent`] implementing a basic wallet.
65///
66/// It reexports the procedures from `miden::standards::wallets::basic` module. When linking against
67/// this component, the `miden` library (i.e. [`ProtocolLib`](miden_protocol::ProtocolLib)) must be
68/// available to the assembler which is the case when using [`CodeBuilder`][builder]. The procedures
69/// of this component are:
70/// - `receive_asset`, which can be used to add an asset to the account.
71/// - `move_asset_to_note`, which can be used to remove the specified asset from the account and add
72///   it to the output note with the specified index.
73/// - `create_note`, which can be used to create a new output note and return its index.
74///
75/// All methods require authentication. Thus, this component must be combined with a component
76/// providing authentication.
77///
78/// [builder]: crate::code_builder::CodeBuilder
79pub struct BasicWallet;
80
81impl BasicWallet {
82    // CONSTANTS
83    // --------------------------------------------------------------------------------------------
84
85    /// The name of the component.
86    pub const NAME: &'static str = "miden::standards::wallets::basic_wallet";
87
88    const RECEIVE_ASSET_PROC_NAME: &str = "receive_asset";
89    const MOVE_ASSET_TO_NOTE_PROC_NAME: &str = "move_asset_to_note";
90    const CREATE_NOTE_PROC_NAME: &str = "create_note";
91
92    /// Returns the canonical [`AccountComponentName`] of this component.
93    pub const fn name() -> AccountComponentName {
94        AccountComponentName::from_static_str(Self::NAME)
95    }
96
97    // PUBLIC ACCESSORS
98    // --------------------------------------------------------------------------------------------
99
100    /// Returns the [`AccountComponentCode`] of this component.
101    pub fn code() -> &'static AccountComponentCode {
102        &BASIC_WALLET_CODE
103    }
104
105    /// Returns the procedure root of the `receive_asset` wallet procedure.
106    pub fn receive_asset_root() -> AccountProcedureRoot {
107        *BASIC_WALLET_RECEIVE_ASSET
108    }
109
110    /// Returns the procedure root of the `move_asset_to_note` wallet procedure.
111    pub fn move_asset_to_note_root() -> AccountProcedureRoot {
112        *BASIC_WALLET_MOVE_ASSET_TO_NOTE
113    }
114
115    /// Returns the procedure root of the `create_note` wallet procedure.
116    pub fn create_note_root() -> AccountProcedureRoot {
117        *BASIC_WALLET_CREATE_NOTE
118    }
119
120    /// Returns the [`AccountComponentMetadata`] for this component.
121    pub fn component_metadata() -> AccountComponentMetadata {
122        AccountComponentMetadata::new(Self::NAME)
123            .with_description("Basic wallet component for receiving and sending assets")
124    }
125}
126
127impl From<BasicWallet> for AccountComponent {
128    fn from(_: BasicWallet) -> Self {
129        let metadata = BasicWallet::component_metadata();
130
131        AccountComponent::new(BasicWallet::code().clone(), vec![], metadata).expect(
132            "basic wallet component should satisfy the requirements of a valid account component",
133        )
134    }
135}
136
137// WALLET CREATION
138// ================================================================================================
139
140/// Creates a new account with a basic wallet interface, single signature authentication and the
141/// specified account type.
142///
143/// The basic wallet interface exposes three procedures:
144/// - `receive_asset`, which can be used to add an asset to the account.
145/// - `move_asset_to_note`, which can be used to remove the specified asset from the account and add
146///   it to the output note with the specified index.
147/// - `create_note`, which can be used to create an output note.
148///
149/// All methods require authentication, which is provided by an [`AuthSingleSig`] component
150/// configured with the given approver.
151pub fn create_basic_wallet(
152    init_seed: [u8; 32],
153    approver: Approver,
154    account_type: AccountType,
155) -> Result<Account, AccountError> {
156    let auth_component: AccountComponent = AuthSingleSig::new(approver).into();
157
158    create_wallet(init_seed, auth_component, account_type)
159}
160
161/// Creates a new account with a basic wallet interface, multi-signature authentication and the
162/// specified account type.
163///
164/// Authentication is provided by an [`AuthMultisig`] component requiring the default threshold of
165/// `approver_set` approver signatures, with optional per-procedure threshold overrides in
166/// `proc_thresholds`.
167///
168/// # Security
169///
170/// See [`AuthMultisig`] for important caveats regarding per-procedure thresholds and private
171/// account state withholding. For private accounts this constructor rejects per-procedure
172/// thresholds below the default threshold (a lower threshold would let a sub-quorum advance and
173/// withhold the private account state); public accounts allow any per-procedure threshold.
174pub fn create_multisig_wallet(
175    init_seed: [u8; 32],
176    approver_set: ApproverSet,
177    proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
178    account_type: AccountType,
179) -> Result<Account, AccountError> {
180    let default_threshold = approver_set.threshold().get();
181    if account_type == AccountType::Private
182        && proc_thresholds
183            .iter()
184            .any(|(_, proc_threshold)| *proc_threshold < default_threshold)
185    {
186        return Err(AccountError::other(
187            "private multisig wallets do not allow per-procedure thresholds below the default \
188             threshold, as a lower threshold would let a sub-quorum advance and withhold the \
189             private account state; use a guarded wallet to lower thresholds safely",
190        ));
191    }
192
193    let config = AuthMultisigConfig::new(approver_set).with_proc_thresholds(proc_thresholds)?;
194    let auth_component: AccountComponent = AuthMultisig::new(config)?.into();
195
196    create_wallet(init_seed, auth_component, account_type)
197}
198
199/// Creates a new account with a basic wallet interface, guarded multi-signature authentication and
200/// the specified account type.
201///
202/// Authentication is provided by an [`AuthGuardedMultisig`] component: every operation requires
203/// both the default threshold of `approver_set` approver signatures (with optional per-procedure
204/// overrides in `proc_thresholds`) and a valid signature from the configured `guardian`.
205pub fn create_guarded_wallet(
206    init_seed: [u8; 32],
207    approver_set: ApproverSet,
208    proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
209    guardian: GuardianConfig,
210    account_type: AccountType,
211) -> Result<Account, AccountError> {
212    let config = AuthGuardedMultisigConfig::new(approver_set, guardian)?
213        .with_proc_thresholds(proc_thresholds)?;
214    let auth_component: AccountComponent = AuthGuardedMultisig::new(config)?.into();
215
216    create_wallet(init_seed, auth_component, account_type)
217}
218
219/// Creates a basic wallet account from the given authentication component and account type.
220fn create_wallet(
221    init_seed: [u8; 32],
222    auth_component: AccountComponent,
223    account_type: AccountType,
224) -> Result<Account, AccountError> {
225    AccountBuilder::new(init_seed)
226        .account_type(account_type)
227        .with_component(auth_component)
228        .with_component(BasicWallet)
229        .build()
230}
231
232// TESTS
233// ================================================================================================
234
235#[cfg(test)]
236mod tests {
237    use alloc::string::ToString;
238
239    use miden_protocol::account::auth::{self, PublicKeyCommitment};
240    use miden_protocol::utils::serde::{Deserializable, Serializable};
241    use miden_protocol::{ONE, Word};
242
243    use super::{
244        Account,
245        AccountType,
246        Approver,
247        ApproverSet,
248        AuthMultisig,
249        GuardianConfig,
250        create_basic_wallet,
251        create_guarded_wallet,
252        create_multisig_wallet,
253    };
254    use crate::account::wallets::BasicWallet;
255
256    fn approver(seed: u32) -> Approver {
257        Approver::new(
258            PublicKeyCommitment::from(Word::from([seed, seed, seed, seed])),
259            auth::AuthScheme::Falcon512Poseidon2,
260        )
261    }
262
263    #[test]
264    fn test_create_basic_wallet() -> anyhow::Result<()> {
265        create_basic_wallet([1; 32], approver(1), AccountType::Public)?;
266        Ok(())
267    }
268
269    #[test]
270    fn test_serialize_basic_wallet() -> anyhow::Result<()> {
271        let approver = Approver::new(
272            PublicKeyCommitment::from(Word::from([ONE; 4])),
273            auth::AuthScheme::EcdsaK256Keccak,
274        );
275        let wallet = create_basic_wallet([1; 32], approver, AccountType::Public)?;
276
277        let bytes = wallet.to_bytes();
278        let deserialized_wallet = Account::read_from_bytes(&bytes)?;
279        assert_eq!(wallet, deserialized_wallet);
280
281        Ok(())
282    }
283
284    #[test]
285    fn test_create_multisig_wallet_public_allows_lower_override() -> anyhow::Result<()> {
286        let approver_set = ApproverSet::new(vec![approver(1), approver(2)], 2)?;
287        let proc_thresholds = vec![(BasicWallet::receive_asset_root(), 1)];
288
289        // A public account may use a per-procedure threshold below the default.
290        create_multisig_wallet([1; 32], approver_set, proc_thresholds, AccountType::Public)?;
291
292        Ok(())
293    }
294
295    #[test]
296    fn test_create_multisig_wallet_private_no_override_succeeds() -> anyhow::Result<()> {
297        let approver_set = ApproverSet::new(vec![approver(1), approver(2)], 2)?;
298
299        // No overrides is always allowed for private accounts.
300        create_multisig_wallet([1; 32], approver_set, vec![], AccountType::Private)?;
301
302        Ok(())
303    }
304
305    #[test]
306    fn test_create_multisig_wallet_private_higher_override_succeeds() -> anyhow::Result<()> {
307        let approver_set = ApproverSet::new(vec![approver(1), approver(2), approver(3)], 2)?;
308        // Hardening a procedure above the default (2 -> 3) is safe for private accounts, as long as
309        // the override editing procedure is hardened to the same level so the override cannot be
310        // lowered by a smaller quorum.
311        let proc_thresholds = vec![
312            (BasicWallet::move_asset_to_note_root(), 3),
313            (AuthMultisig::set_procedure_threshold_root(), 3),
314        ];
315
316        create_multisig_wallet([1; 32], approver_set, proc_thresholds, AccountType::Private)?;
317
318        Ok(())
319    }
320
321    #[test]
322    fn test_create_multisig_wallet_private_lower_override_rejected() -> anyhow::Result<()> {
323        let approver_set = ApproverSet::new(vec![approver(1), approver(2)], 2)?;
324        let proc_thresholds = vec![(BasicWallet::receive_asset_root(), 1)];
325
326        let err =
327            create_multisig_wallet([1; 32], approver_set, proc_thresholds, AccountType::Private)
328                .expect_err("private multisig with a below-default threshold must be rejected");
329
330        assert!(
331            err.to_string()
332                .contains("do not allow per-procedure thresholds below the default threshold")
333        );
334
335        Ok(())
336    }
337
338    #[test]
339    fn test_create_guarded_wallet_private_override_allowed() -> anyhow::Result<()> {
340        let approver_set = ApproverSet::new(vec![approver(1), approver(2)], 2)?;
341        let proc_thresholds = vec![(BasicWallet::receive_asset_root(), 1)];
342        let guardian = GuardianConfig::new(approver(3));
343
344        // The guardian forwards state, so a private guarded wallet may use overrides.
345        create_guarded_wallet(
346            [1; 32],
347            approver_set,
348            proc_thresholds,
349            guardian,
350            AccountType::Private,
351        )?;
352
353        Ok(())
354    }
355
356    /// Check that the obtaining of the basic wallet procedure roots does not panic.
357    #[test]
358    fn get_faucet_procedures() {
359        let _receive_asset_root = BasicWallet::receive_asset_root();
360        let _move_asset_to_note_root = BasicWallet::move_asset_to_note_root();
361    }
362}