use alloc::boxed::Box;
use alloc::vec::Vec;
use miden_core::FieldElement;
use crate::account::{
Account,
AccountCode,
AccountComponent,
AccountId,
AccountIdV0,
AccountIdVersion,
AccountStorage,
AccountStorageMode,
AccountType,
};
use crate::asset::AssetVault;
use crate::{AccountError, Felt, Word};
#[derive(Debug, Clone)]
pub struct AccountBuilder {
#[cfg(any(feature = "testing", test))]
assets: Vec<crate::asset::Asset>,
#[cfg(any(feature = "testing", test))]
nonce: Option<Felt>,
components: Vec<AccountComponent>,
auth_component: Option<AccountComponent>,
account_type: AccountType,
storage_mode: AccountStorageMode,
init_seed: [u8; 32],
id_version: AccountIdVersion,
}
impl AccountBuilder {
pub fn new(init_seed: [u8; 32]) -> Self {
Self {
#[cfg(any(feature = "testing", test))]
assets: vec![],
#[cfg(any(feature = "testing", test))]
nonce: None,
components: vec![],
auth_component: None,
init_seed,
account_type: AccountType::RegularAccountUpdatableCode,
storage_mode: AccountStorageMode::Private,
id_version: AccountIdVersion::Version0,
}
}
pub fn version(mut self, version: AccountIdVersion) -> Self {
self.id_version = version;
self
}
pub fn account_type(mut self, account_type: AccountType) -> Self {
self.account_type = account_type;
self
}
pub fn storage_mode(mut self, storage_mode: AccountStorageMode) -> Self {
self.storage_mode = storage_mode;
self
}
pub fn with_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
self.components.push(account_component.into());
self
}
pub fn with_auth_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
self.auth_component = Some(account_component.into());
self
}
fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> {
#[cfg(any(feature = "testing", test))]
let vault = AssetVault::new(&self.assets).map_err(|err| {
AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
})?;
#[cfg(all(not(feature = "testing"), not(test)))]
let vault = AssetVault::default();
let auth_component = self
.auth_component
.take()
.ok_or(AccountError::BuildError("auth component must be set".into(), None))?;
let mut components = vec![auth_component];
components.append(&mut self.components);
let (code, storage) = Account::initialize_from_components(self.account_type, &components)
.map_err(|err| {
AccountError::BuildError(
"account components failed to build".into(),
Some(Box::new(err)),
)
})?;
Ok((vault, code, storage))
}
fn grind_account_id(
&self,
init_seed: [u8; 32],
version: AccountIdVersion,
code_commitment: Word,
storage_commitment: Word,
) -> Result<Word, AccountError> {
let seed = AccountIdV0::compute_account_seed(
init_seed,
self.account_type,
self.storage_mode,
version,
code_commitment,
storage_commitment,
)
.map_err(|err| {
AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
})?;
Ok(seed)
}
pub fn build(mut self) -> Result<Account, AccountError> {
let (vault, code, storage) = self.build_inner()?;
#[cfg(any(feature = "testing", test))]
if !vault.is_empty() {
return Err(AccountError::BuildError(
"account asset vault must be empty on new accounts".into(),
None,
));
}
let seed = self.grind_account_id(
self.init_seed,
self.id_version,
code.commitment(),
storage.commitment(),
)?;
let account_id = AccountId::new(
seed,
AccountIdVersion::Version0,
code.commitment(),
storage.commitment(),
)
.expect("get_account_seed should provide a suitable seed");
debug_assert_eq!(account_id.account_type(), self.account_type);
debug_assert_eq!(account_id.storage_mode(), self.storage_mode);
let account =
Account::new_unchecked(account_id, vault, storage, code, Felt::ZERO, Some(seed));
Ok(account)
}
}
#[cfg(any(feature = "testing", test))]
impl AccountBuilder {
pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
self.assets.extend(assets);
self
}
pub fn nonce(mut self, nonce: Felt) -> Self {
self.nonce = Some(nonce);
self
}
pub fn build_existing(mut self) -> Result<Account, AccountError> {
let (vault, code, storage) = self.build_inner()?;
let account_id = {
let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
.expect("we should have sliced exactly 15 bytes off");
AccountId::dummy(
bytes,
AccountIdVersion::Version0,
self.account_type,
self.storage_mode,
)
};
let nonce = self.nonce.unwrap_or(Felt::ONE);
Ok(Account::new_existing(account_id, vault, storage, code, nonce))
}
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use assert_matches::assert_matches;
use miden_assembly::{Assembler, Library};
use miden_core::FieldElement;
use miden_processor::MastNodeExt;
use super::*;
use crate::account::StorageSlot;
use crate::testing::noop_auth_component::NoopAuthComponent;
const CUSTOM_CODE1: &str = "
export.foo
push.2.2 add eq.4
end
";
const CUSTOM_CODE2: &str = "
export.bar
push.4.4 add eq.8
end
";
static CUSTOM_LIBRARY1: LazyLock<Library> = LazyLock::new(|| {
Assembler::default()
.assemble_library([CUSTOM_CODE1])
.expect("code should be valid")
});
static CUSTOM_LIBRARY2: LazyLock<Library> = LazyLock::new(|| {
Assembler::default()
.assemble_library([CUSTOM_CODE2])
.expect("code should be valid")
});
struct CustomComponent1 {
slot0: u64,
}
impl From<CustomComponent1> for AccountComponent {
fn from(custom: CustomComponent1) -> Self {
let mut value = Word::empty();
value[0] = Felt::new(custom.slot0);
AccountComponent::new(CUSTOM_LIBRARY1.clone(), vec![StorageSlot::Value(value)])
.expect("component should be valid")
.with_supports_all_types()
}
}
struct CustomComponent2 {
slot0: u64,
slot1: u64,
}
impl From<CustomComponent2> for AccountComponent {
fn from(custom: CustomComponent2) -> Self {
let mut value0 = Word::empty();
value0[3] = Felt::new(custom.slot0);
let mut value1 = Word::empty();
value1[3] = Felt::new(custom.slot1);
AccountComponent::new(
CUSTOM_LIBRARY2.clone(),
vec![StorageSlot::Value(value0), StorageSlot::Value(value1)],
)
.expect("component should be valid")
.with_supports_all_types()
}
}
#[test]
fn account_builder() {
let storage_slot0 = 25;
let storage_slot1 = 12;
let storage_slot2 = 42;
let account = Account::builder([5; 32])
.with_auth_component(NoopAuthComponent)
.with_component(CustomComponent1 { slot0: storage_slot0 })
.with_component(CustomComponent2 {
slot0: storage_slot1,
slot1: storage_slot2,
})
.build()
.unwrap();
assert_eq!(account.nonce(), Felt::ZERO);
let computed_id = AccountId::new(
account.seed().unwrap(),
AccountIdVersion::Version0,
account.code.commitment(),
account.storage.commitment(),
)
.unwrap();
assert_eq!(account.id(), computed_id);
assert_eq!(account.code.procedure_roots().count(), 3);
let foo_root = CUSTOM_LIBRARY1.mast_forest()
[CUSTOM_LIBRARY1.get_export_node_id(&CUSTOM_LIBRARY1.exports().next().unwrap().name)]
.digest();
let bar_root = CUSTOM_LIBRARY2.mast_forest()
[CUSTOM_LIBRARY2.get_export_node_id(&CUSTOM_LIBRARY2.exports().next().unwrap().name)]
.digest();
let foo_procedure_info = &account
.code()
.procedures()
.iter()
.find(|info| info.mast_root() == &foo_root)
.unwrap();
assert_eq!(foo_procedure_info.storage_offset(), 0);
assert_eq!(foo_procedure_info.storage_size(), 1);
let bar_procedure_info = &account
.code()
.procedures()
.iter()
.find(|info| info.mast_root() == &bar_root)
.unwrap();
assert_eq!(bar_procedure_info.storage_offset(), 1);
assert_eq!(bar_procedure_info.storage_size(), 2);
assert_eq!(
account.storage().get_item(0).unwrap(),
[Felt::new(storage_slot0), Felt::new(0), Felt::new(0), Felt::new(0)].into()
);
assert_eq!(
account.storage().get_item(1).unwrap(),
[Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot1)].into()
);
assert_eq!(
account.storage().get_item(2).unwrap(),
[Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot2)].into()
);
}
#[test]
fn account_builder_non_empty_vault_on_new_account() {
let storage_slot0 = 25;
let build_error = Account::builder([0xff; 32])
.with_auth_component(NoopAuthComponent)
.with_component(CustomComponent1 { slot0: storage_slot0 })
.with_assets(AssetVault::mock().assets())
.build()
.unwrap_err();
assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
}
}