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