use alloc::string::ToString;
use alloc::vec::Vec;
use crate::account::{Account, AccountCode, AccountId, AccountStorage, AccountStoragePatch};
use crate::asset::AssetVault;
use crate::crypto::SequentialCommit;
use crate::errors::{AccountDeltaError, AccountError};
use crate::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
use crate::{Felt, Word, ZERO};
mod delta_op;
pub use delta_op::AssetDeltaOperation;
mod vault;
pub use vault::{
AccountVaultDelta,
FungibleAssetDelta,
NonFungibleAssetDelta,
NonFungibleDeltaAction,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AccountDelta {
account_id: AccountId,
storage: AccountStoragePatch,
vault: AccountVaultDelta,
code: Option<AccountCode>,
nonce_delta: Felt,
}
impl AccountDelta {
const DOMAIN: Felt = Felt::new_unchecked(1);
pub fn new(
account_id: AccountId,
storage: AccountStoragePatch,
vault: AccountVaultDelta,
code: Option<AccountCode>,
nonce_delta: Felt,
) -> Result<Self, AccountDeltaError> {
validate_nonce(nonce_delta, &storage, &vault)?;
if code.is_some() && storage.contains_non_create_ops() {
return Err(AccountDeltaError::FullStateDeltaContainsNonCreateOp);
}
Ok(Self {
account_id,
storage,
vault,
code,
nonce_delta,
})
}
pub fn vault_mut(&mut self) -> &mut AccountVaultDelta {
&mut self.vault
}
pub fn is_empty(&self) -> bool {
self.storage.is_empty() && self.vault.is_empty() && self.nonce_delta == ZERO
}
pub fn is_full_state(&self) -> bool {
self.code.is_some()
}
pub fn storage(&self) -> &AccountStoragePatch {
&self.storage
}
pub fn vault(&self) -> &AccountVaultDelta {
&self.vault
}
pub fn nonce_delta(&self) -> Felt {
self.nonce_delta
}
pub fn id(&self) -> AccountId {
self.account_id
}
pub fn code(&self) -> Option<&AccountCode> {
self.code.as_ref()
}
pub fn into_parts(self) -> (AccountStoragePatch, AccountVaultDelta, Option<AccountCode>, Felt) {
(self.storage, self.vault, self.code, self.nonce_delta)
}
pub fn to_commitment(&self) -> Word {
<Self as SequentialCommit>::to_commitment(self)
}
}
impl TryFrom<&AccountDelta> for Account {
type Error = AccountError;
fn try_from(delta: &AccountDelta) -> Result<Self, Self::Error> {
if !delta.is_full_state() {
return Err(AccountError::PartialStateDeltaToAccount);
}
let Some(code) = delta.code().cloned() else {
return Err(AccountError::PartialStateDeltaToAccount);
};
if delta.vault().removed_assets().count() != 0 {
return Err(AccountError::AssetsRemovedFromNewAccount);
}
let mut vault = AssetVault::default();
for added_asset in delta.vault().added_assets() {
vault.insert_asset(added_asset).map_err(AccountError::AssetVaultUpdateError)?;
}
let mut storage = AccountStorage::default();
storage.apply_patch(delta.storage())?;
let nonce = delta.nonce_delta();
Account::new(delta.id(), vault, storage, code, nonce, None)
}
}
impl SequentialCommit for AccountDelta {
type Commitment = Word;
fn to_elements(&self) -> Vec<Felt> {
if self.is_empty() {
return Vec::new();
}
let mut elements = Vec::with_capacity(24);
elements.extend_from_slice(&[
Self::DOMAIN,
self.nonce_delta,
self.account_id.suffix(),
self.account_id.prefix().as_felt(),
]);
elements.extend_from_slice(Word::empty().as_elements());
self.vault.append_delta_elements(&mut elements);
self.storage.append_patch_elements(&mut elements);
debug_assert!(
elements.len() % (2 * crate::WORD_SIZE) == 0,
"expected elements to contain an even number of words, but it contained {} elements",
elements.len()
);
elements
}
}
impl Serializable for AccountDelta {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.account_id.write_into(target);
self.storage.write_into(target);
self.vault.write_into(target);
self.code.write_into(target);
self.nonce_delta.write_into(target);
}
fn get_size_hint(&self) -> usize {
self.account_id.get_size_hint()
+ self.storage.get_size_hint()
+ self.vault.get_size_hint()
+ self.code.get_size_hint()
+ self.nonce_delta.get_size_hint()
}
}
impl Deserializable for AccountDelta {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let account_id = AccountId::read_from(source)?;
let storage = AccountStoragePatch::read_from(source)?;
let vault = AccountVaultDelta::read_from(source)?;
let code = <Option<AccountCode>>::read_from(source)?;
let nonce_delta = Felt::read_from(source)?;
validate_nonce(nonce_delta, &storage, &vault)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
Ok(Self {
account_id,
storage,
vault,
code,
nonce_delta,
})
}
}
fn validate_nonce(
nonce_delta: Felt,
storage: &AccountStoragePatch,
vault: &AccountVaultDelta,
) -> Result<(), AccountDeltaError> {
if (!storage.is_empty() || !vault.is_empty()) && nonce_delta == ZERO {
return Err(AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta);
}
Ok(())
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use rstest::rstest;
use super::{AccountDelta, AccountStoragePatch, AccountVaultDelta};
use crate::account::{
Account,
AccountCode,
AccountId,
AccountStorage,
AccountType,
StorageMapKey,
StorageMapPatch,
StorageSlotName,
};
use crate::asset::{
Asset,
AssetVault,
FungibleAsset,
NonFungibleAsset,
NonFungibleAssetDetails,
};
use crate::errors::AccountDeltaError;
use crate::testing::account_id::{
ACCOUNT_ID_PRIVATE_SENDER,
ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
AccountIdBuilder,
};
use crate::utils::serde::Serializable;
use crate::{ONE, Word, ZERO};
#[test]
fn account_delta_nonce_validation() {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let storage_patch = AccountStoragePatch::new();
let vault_delta = AccountVaultDelta::default();
AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ZERO)
.unwrap();
AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ONE)
.unwrap();
let storage_patch = AccountStoragePatch::from_iters([StorageSlotName::mock(1)], [], []);
assert_matches!(
AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ZERO)
.unwrap_err(),
AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta
);
AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ONE)
.unwrap();
}
#[rstest]
#[case::update(
AccountStoragePatch::builder().update_value(StorageSlotName::mock(1), Word::empty()).build()
)]
#[case::remove(
AccountStoragePatch::builder().remove_value(StorageSlotName::mock(1)).build()
)]
fn account_delta_new_rejects_full_state_with_non_create_op(
#[case] storage: AccountStoragePatch,
) -> anyhow::Result<()> {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
let error = AccountDelta::new(
account_id,
storage,
AccountVaultDelta::default(),
Some(AccountCode::mock()),
ONE,
)
.unwrap_err();
assert_matches!(error, AccountDeltaError::FullStateDeltaContainsNonCreateOp);
Ok(())
}
#[test]
fn account_delta_full_state_with_create_reconstructs() -> anyhow::Result<()> {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
let code = AccountCode::mock();
let created_slot = StorageSlotName::mock(1);
let created_value = Word::from([7u32, 0, 0, 0]);
let storage = AccountStoragePatch::builder()
.create_value(created_slot.clone(), created_value)
.build();
let delta = AccountDelta::new(
account_id,
storage,
AccountVaultDelta::default(),
Some(code.clone()),
ONE,
)?;
assert!(delta.is_full_state());
let account = Account::try_from(&delta)?;
assert_eq!(account.code(), &code);
assert_eq!(account.storage().get_item(&created_slot)?, created_value);
Ok(())
}
#[test]
fn account_delta_size_hint() {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let storage_patch = AccountStoragePatch::new();
let vault_delta = AccountVaultDelta::default();
assert_eq!(storage_patch.to_bytes().len(), storage_patch.get_size_hint());
assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
let account_delta =
AccountDelta::new(account_id, storage_patch, vault_delta, None, ZERO).unwrap();
assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
let storage_patch = AccountStoragePatch::from_iters(
[StorageSlotName::mock(1)],
[
(StorageSlotName::mock(2), Word::from([1, 1, 1, 1u32])),
(StorageSlotName::mock(3), Word::from([1, 1, 0, 1u32])),
],
[(
StorageSlotName::mock(4),
StorageMapPatch::from_iters(
[
StorageMapKey::from_array([1, 1, 1, 0]),
StorageMapKey::from_array([0, 1, 1, 1]),
],
[(StorageMapKey::from_array([1, 1, 1, 1]), Word::from([1, 1, 1, 1u32]))],
),
)],
);
let non_fungible: Asset = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
AccountIdBuilder::new()
.account_type(AccountType::Public)
.build_with_rng(&mut rand::rng()),
vec![6],
))
.into();
let fungible_2: Asset = FungibleAsset::new(
AccountIdBuilder::new()
.account_type(AccountType::Public)
.build_with_rng(&mut rand::rng()),
10,
)
.unwrap()
.into();
let vault_delta = AccountVaultDelta::from_iters([non_fungible], [fungible_2]);
assert_eq!(storage_patch.to_bytes().len(), storage_patch.get_size_hint());
assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
let account_delta =
AccountDelta::new(account_id, storage_patch, vault_delta, None, ONE).unwrap();
assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
let account_id =
AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap();
let asset_vault = AssetVault::mock();
assert_eq!(asset_vault.to_bytes().len(), asset_vault.get_size_hint());
let account_storage = AccountStorage::mock();
assert_eq!(account_storage.to_bytes().len(), account_storage.get_size_hint());
let account_code = AccountCode::mock();
assert_eq!(account_code.to_bytes().len(), account_code.get_size_hint());
let account =
Account::new_existing(account_id, asset_vault, account_storage, account_code, ONE);
assert_eq!(account.to_bytes().len(), account.get_size_hint());
}
}