1mod vault;
2
3mod storage;
4mod update_details;
5use alloc::string::ToString;
6use alloc::vec::Vec;
7
8pub use storage::{
9 AccountStoragePatch,
10 StorageMapPatch,
11 StorageMapPatchEntries,
12 StoragePatchOperation,
13 StorageSlotPatch,
14 StorageValuePatch,
15};
16pub use update_details::AccountUpdateDetails;
17pub use vault::AccountVaultPatch;
18
19use crate::account::{Account, AccountCode, AccountId, AccountStorage};
20use crate::asset::AssetVault;
21use crate::crypto::SequentialCommit;
22use crate::errors::{AccountError, AccountPatchError};
23use crate::utils::serde::{
24 ByteReader,
25 ByteWriter,
26 Deserializable,
27 DeserializationError,
28 Serializable,
29};
30use crate::{Felt, Word};
31
32#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct AccountPatch {
63 account_id: AccountId,
65 storage: AccountStoragePatch,
67 vault: AccountVaultPatch,
69 code: Option<AccountCode>,
71 final_nonce: Option<Felt>,
75}
76
77impl AccountPatch {
78 const DOMAIN: Felt = Felt::new_unchecked(2);
83
84 pub fn new(
103 account_id: AccountId,
104 storage: AccountStoragePatch,
105 vault: AccountVaultPatch,
106 code: Option<AccountCode>,
107 final_nonce: Option<Felt>,
108 ) -> Result<Self, AccountPatchError> {
109 if final_nonce.is_some_and(|final_nonce| final_nonce == Felt::ZERO) {
113 return Err(AccountPatchError::FinalNonceIsZero);
114 }
115
116 if (!storage.is_empty() || !vault.is_empty() || code.is_some()) && final_nonce.is_none() {
120 return Err(AccountPatchError::StateChangeRequiresNonceUpdate);
121 }
122
123 if final_nonce.is_some_and(|final_nonce| final_nonce == Felt::ONE) && code.is_none() {
127 return Err(AccountPatchError::CodeMustBeProvidedForNewAccounts);
128 }
129
130 if code.is_some() && storage.contains_non_create_ops() {
138 return Err(AccountPatchError::FullStatePatchContainsNonCreateStorageOp);
139 }
140
141 Ok(Self {
142 account_id,
143 storage,
144 vault,
145 code,
146 final_nonce,
147 })
148 }
149
150 pub fn empty(account_id: AccountId) -> Self {
152 AccountPatch::new(
153 account_id,
154 AccountStoragePatch::default(),
155 AccountVaultPatch::default(),
156 None,
157 None,
158 )
159 .expect("empty patch should be valid")
160 }
161
162 pub fn merge(&mut self, other: Self) -> Result<(), AccountPatchError> {
200 if self.account_id != other.account_id {
201 return Err(AccountPatchError::AccountIdMismatch {
202 expected: self.account_id,
203 actual: other.account_id,
204 });
205 }
206
207 match (self.final_nonce, other.final_nonce) {
208 (None, None) => return Ok(()),
210
211 (None, Some(_)) => {
213 *self = other;
214 return Ok(());
215 },
216
217 (Some(_), None) => return Ok(()),
219
220 (Some(current), Some(new)) => {
221 if new != current + Felt::ONE {
222 return Err(AccountPatchError::NonceMustIncrementByOne { current, new });
223 }
224 self.final_nonce = Some(new);
225 },
226 }
227
228 if other.is_full_state() {
231 return Err(AccountPatchError::MergeIncomingFullStatePatch);
232 }
233
234 self.storage.merge(other.storage)?;
235 self.vault.merge(other.vault);
236
237 debug_assert!(
242 !self.is_full_state() || !self.storage.contains_non_create_ops(),
243 "merging should never add storage updates or removals to a full state patch",
244 );
245
246 Ok(())
247 }
248
249 pub fn id(&self) -> AccountId {
254 self.account_id
255 }
256
257 pub fn storage(&self) -> &AccountStoragePatch {
259 &self.storage
260 }
261
262 pub fn vault(&self) -> &AccountVaultPatch {
264 &self.vault
265 }
266
267 pub fn code(&self) -> Option<&AccountCode> {
269 self.code.as_ref()
270 }
271
272 pub fn final_nonce(&self) -> Option<Felt> {
275 self.final_nonce
276 }
277
278 pub fn is_full_state(&self) -> bool {
283 self.code.is_some()
292 }
293
294 pub fn is_empty(&self) -> bool {
297 self.final_nonce.is_none()
300 }
301
302 pub fn to_commitment(&self) -> Word {
364 <Self as SequentialCommit>::to_commitment(self)
365 }
366}
367
368impl TryFrom<&AccountPatch> for Account {
369 type Error = AccountError;
370
371 fn try_from(patch: &AccountPatch) -> Result<Self, Self::Error> {
384 if !patch.is_full_state() {
385 return Err(AccountError::PartialStatePatchToAccount);
386 }
387
388 let code = patch.code().cloned().expect("full state patch must carry code");
390 let nonce = patch.final_nonce().expect("full state patch must carry final nonce");
391
392 let mut vault = AssetVault::default();
393 vault.apply_patch(patch.vault()).map_err(AccountError::AssetVaultUpdateError)?;
394
395 let mut storage = AccountStorage::default();
398 storage.apply_patch(patch.storage())?;
399
400 Account::new(patch.id(), vault, storage, code, nonce, None)
401 }
402}
403
404impl SequentialCommit for AccountPatch {
405 type Commitment = Word;
406
407 fn to_elements(&self) -> Vec<Felt> {
411 if self.is_empty() {
413 return Vec::new();
414 }
415
416 let mut elements = Vec::with_capacity(8);
418
419 let final_nonce = self.final_nonce.expect("non-empty patches should have a new nonce set");
421 elements.extend_from_slice(&[
422 Self::DOMAIN,
423 final_nonce,
424 self.account_id.suffix(),
425 self.account_id.prefix().as_felt(),
426 ]);
427 elements.extend_from_slice(Word::empty().as_elements());
428
429 self.vault.append_patch_elements(&mut elements);
431
432 self.storage.append_patch_elements(&mut elements);
434
435 debug_assert!(
436 elements.len() % (2 * crate::WORD_SIZE) == 0,
437 "expected elements to contain an even number of words, but it contained {} elements",
438 elements.len()
439 );
440
441 elements
442 }
443}
444
445impl Serializable for AccountPatch {
446 fn write_into<W: ByteWriter>(&self, target: &mut W) {
447 self.account_id.write_into(target);
448 self.storage.write_into(target);
449 self.vault.write_into(target);
450 self.code.write_into(target);
451 self.final_nonce.write_into(target);
452 }
453
454 fn get_size_hint(&self) -> usize {
455 self.account_id.get_size_hint()
456 + self.storage.get_size_hint()
457 + self.vault.get_size_hint()
458 + self.code.get_size_hint()
459 + self.final_nonce.get_size_hint()
460 }
461}
462
463impl Deserializable for AccountPatch {
464 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
465 let account_id = AccountId::read_from(source)?;
466 let storage = AccountStoragePatch::read_from(source)?;
467 let vault = AccountVaultPatch::read_from(source)?;
468 let code = <Option<AccountCode>>::read_from(source)?;
469 let final_nonce = <Option<Felt>>::read_from(source)?;
470
471 Self::new(account_id, storage, vault, code, final_nonce)
472 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
473 }
474}
475
476#[cfg(test)]
480mod tests {
481 use assert_matches::assert_matches;
482 use miden_core::serde::Deserializable;
483 use rstest::rstest;
484
485 use super::{AccountPatch, AccountVaultPatch};
486 use crate::account::{
487 Account,
488 AccountCode,
489 AccountId,
490 AccountStoragePatch,
491 StorageMapKey,
492 StorageMapPatch,
493 StorageSlotName,
494 StorageSlotPatch,
495 StorageValuePatch,
496 };
497 use crate::asset::{Asset, FungibleAsset, NonFungibleAsset};
498 use crate::errors::{AccountError, AccountPatchError};
499 use crate::testing::account_id::{
500 ACCOUNT_ID_PRIVATE_SENDER,
501 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
502 };
503 use crate::utils::serde::Serializable;
504 use crate::{Felt, Word};
505
506 fn patch_id() -> AccountId {
507 AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap()
508 }
509
510 #[test]
511 fn account_patch_serde() -> anyhow::Result<()> {
512 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
513 let asset_0 = FungibleAsset::mock(100);
514 let asset_1 = FungibleAsset::new(ACCOUNT_ID_PRIVATE_SENDER.try_into()?, 500_000)?.into();
515 let asset_2 = NonFungibleAsset::mock(&[10]);
516 let asset_3 = NonFungibleAsset::mock(&[20]);
517 let vault_patch = AccountVaultPatch::with_assets([asset_0, asset_1, asset_2, asset_3]);
518
519 let storage_patch = AccountStoragePatch::from_iters(
520 [StorageSlotName::mock(1)],
521 [
522 (StorageSlotName::mock(2), Word::from([1, 1, 1, 1u32])),
523 (StorageSlotName::mock(3), Word::from([1, 1, 0, 1u32])),
524 ],
525 [(
526 StorageSlotName::mock(4),
527 StorageMapPatch::from_iters(
528 [
529 StorageMapKey::from_array([1, 1, 1, 0]),
530 StorageMapKey::from_array([0, 1, 1, 1]),
531 ],
532 [(StorageMapKey::from_array([1, 1, 1, 1]), Word::from([1, 1, 1, 1u32]))],
533 ),
534 )],
535 );
536
537 assert_eq!(storage_patch.to_bytes().len(), storage_patch.get_size_hint());
538 assert_eq!(vault_patch.to_bytes().len(), vault_patch.get_size_hint());
539
540 let account_patch =
541 AccountPatch::new(account_id, storage_patch, vault_patch, None, Some(Felt::from(5u8)))?;
542 assert_eq!(AccountPatch::read_from_bytes(&account_patch.to_bytes())?, account_patch);
543 assert_eq!(account_patch.to_bytes().len(), account_patch.get_size_hint());
544
545 Ok(())
546 }
547
548 #[test]
551 fn account_patch_final_nonce_is_zero() -> anyhow::Result<()> {
552 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
553
554 let error = AccountPatch::new(
555 account_id,
556 AccountStoragePatch::new(),
557 AccountVaultPatch::default(),
558 None,
559 Some(Felt::ZERO),
560 )
561 .unwrap_err();
562
563 assert_matches!(error, AccountPatchError::FinalNonceIsZero);
564
565 Ok(())
566 }
567
568 #[rstest::rstest]
571 #[case::non_empty_storage(
572 AccountStoragePatch::from_iters([StorageSlotName::mock(1)], [], []),
573 AccountVaultPatch::default(),
574 None,
575 )]
576 #[case::non_empty_vault(
577 AccountStoragePatch::new(),
578 AccountVaultPatch::with_assets([FungibleAsset::mock(100)]),
579 None,
580 )]
581 #[case::present_code(
582 AccountStoragePatch::new(),
583 AccountVaultPatch::default(),
584 Some(AccountCode::mock())
585 )]
586 #[test]
587 fn account_patch_with_state_change_requires_nonce_update(
588 #[case] storage: AccountStoragePatch,
589 #[case] vault: AccountVaultPatch,
590 #[case] code: Option<AccountCode>,
591 ) -> anyhow::Result<()> {
592 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
593
594 let error = AccountPatch::new(account_id, storage, vault, code, None).unwrap_err();
595 assert_matches!(error, AccountPatchError::StateChangeRequiresNonceUpdate);
596
597 Ok(())
598 }
599
600 #[test]
603 fn account_patch_new_account_requires_code() -> anyhow::Result<()> {
604 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
605
606 let error = AccountPatch::new(
607 account_id,
608 AccountStoragePatch::new(),
609 AccountVaultPatch::default(),
610 None,
611 Some(Felt::ONE),
612 )
613 .unwrap_err();
614 assert_matches!(error, AccountPatchError::CodeMustBeProvidedForNewAccounts);
615
616 AccountPatch::new(
618 account_id,
619 AccountStoragePatch::new(),
620 AccountVaultPatch::default(),
621 Some(AccountCode::mock()),
622 Some(Felt::ONE),
623 )?;
624
625 Ok(())
626 }
627
628 #[test]
631 fn account_patch_roundtrip() -> anyhow::Result<()> {
632 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
633 let code = AccountCode::mock();
634 let asset = FungibleAsset::mock(42);
635
636 let slot_name = StorageSlotName::mock(4);
637 let slot_value = Word::from([1, 2, 3, 4u32]);
638
639 let storage_patch = AccountStoragePatch::from_entries([(
641 slot_name.clone(),
642 StorageSlotPatch::Value(StorageValuePatch::Create { value: slot_value }),
643 )])?;
644
645 let patch = AccountPatch::new(
646 account_id,
647 storage_patch,
648 AccountVaultPatch::with_assets([asset]),
649 Some(code.clone()),
650 Some(Felt::ONE),
651 )?;
652
653 let account = Account::try_from(&patch)?;
654
655 assert_eq!(account.id(), account_id);
656 assert_eq!(account.code(), &code);
657 assert_eq!(account.nonce(), Felt::ONE);
658 assert_eq!(account.storage().get_item(&slot_name)?, slot_value);
659 assert_eq!(account.vault().get(asset.id()), Some(asset));
660
661 let roundtripped_patch = AccountPatch::try_from(account)?;
663 assert_eq!(roundtripped_patch, patch);
664
665 Ok(())
666 }
667
668 #[rstest::rstest]
671 #[case::missing_code(Some(Felt::from(2_u32)))]
672 #[case::empty_patch(None)]
673 #[test]
674 fn account_try_from_partial_patch_fails(
675 #[case] final_nonce: Option<Felt>,
676 ) -> anyhow::Result<()> {
677 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
678
679 let patch = AccountPatch::new(
680 account_id,
681 AccountStoragePatch::new(),
682 AccountVaultPatch::default(),
683 None,
684 final_nonce,
685 )?;
686 assert_matches!(
687 Account::try_from(&patch).unwrap_err(),
688 AccountError::PartialStatePatchToAccount
689 );
690
691 Ok(())
692 }
693
694 #[rstest]
697 #[case::update(
698 AccountStoragePatch::builder().update_value(StorageSlotName::mock(1), Word::empty()).build()
699 )]
700 #[case::remove(
701 AccountStoragePatch::builder().remove_value(StorageSlotName::mock(1)).build()
702 )]
703 fn account_patch_new_rejects_full_state_with_non_create_op(
704 #[case] storage: AccountStoragePatch,
705 ) -> anyhow::Result<()> {
706 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
707
708 let error = AccountPatch::new(
709 account_id,
710 storage,
711 AccountVaultPatch::default(),
712 Some(AccountCode::mock()),
713 Some(Felt::ONE),
714 )
715 .unwrap_err();
716 assert_matches!(error, AccountPatchError::FullStatePatchContainsNonCreateStorageOp);
717
718 Ok(())
719 }
720
721 #[test]
723 fn account_patch_full_state_with_create_reconstructs() -> anyhow::Result<()> {
724 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
725 let code = AccountCode::mock();
726 let created_slot = StorageSlotName::mock(1);
727 let created_value = Word::from([7u32, 0, 0, 0]);
728
729 let storage = AccountStoragePatch::builder()
730 .create_value(created_slot.clone(), created_value)
731 .build();
732
733 let patch = AccountPatch::new(
734 account_id,
735 storage,
736 AccountVaultPatch::default(),
737 Some(code.clone()),
738 Some(Felt::ONE),
739 )?;
740
741 assert!(patch.is_full_state());
742
743 let account = Account::try_from(&patch)?;
744 assert_eq!(account.code(), &code);
745 assert_eq!(account.storage().get_item(&created_slot)?, created_value);
746
747 Ok(())
748 }
749
750 fn full_patch(account_id: AccountId, final_nonce: u32) -> anyhow::Result<AccountPatch> {
756 let storage_patch = AccountStoragePatch::builder()
757 .create_value(StorageSlotName::mock(1), Word::from([1u32, 0, 0, 0]))
758 .build();
759
760 AccountPatch::new(
761 account_id,
762 storage_patch,
763 AccountVaultPatch::default(),
764 Some(AccountCode::mock()),
765 Some(Felt::from(final_nonce)),
766 )
767 .map_err(Into::into)
768 }
769
770 fn partial_patch(account_id: AccountId, final_nonce: u32) -> anyhow::Result<AccountPatch> {
773 let storage = AccountStoragePatch::from_iters(
774 [],
775 [(StorageSlotName::mock(1), Word::from([1u32, 0, 0, 0]))],
776 [],
777 );
778 AccountPatch::new(
779 account_id,
780 storage,
781 AccountVaultPatch::default(),
782 None,
783 Some(Felt::from(final_nonce)),
784 )
785 .map_err(Into::into)
786 }
787
788 #[test]
789 fn account_patch_merge_rejects_id_mismatch() -> anyhow::Result<()> {
790 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
791 let other_account_id =
792 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)?;
793
794 let mut patch = partial_patch(account_id, 2)?;
795 let other = partial_patch(other_account_id, 3)?;
796
797 assert_matches!(
798 patch.merge(other).unwrap_err(),
799 AccountPatchError::AccountIdMismatch { expected, actual } => {
800 assert_eq!(expected, account_id);
801 assert_eq!(actual, other_account_id);
802 }
803 );
804
805 Ok(())
806 }
807
808 #[rstest]
811 #[case::partial_state_plus_full_state(
812 partial_patch(patch_id(), 3)?
813 )]
814 #[case::full_state_plus_full_state(
815 full_patch(patch_id(), 3)?
816 )]
817 fn account_patch_merge_rejects_incoming_full_state(
818 #[case] mut patch: AccountPatch,
819 ) -> anyhow::Result<()> {
820 let other = full_patch(patch_id(), 4)?;
821 assert_matches!(
822 patch.merge(other).unwrap_err(),
823 AccountPatchError::MergeIncomingFullStatePatch
824 );
825
826 Ok(())
827 }
828
829 #[rstest::rstest]
830 #[case::equal(3, 3)]
831 #[case::smaller(3, 2)]
832 #[case::gap(3, 5)]
833 fn account_patch_merge_rejects_non_incrementing_nonce(
834 #[case] self_nonce: u32,
835 #[case] other_nonce: u32,
836 ) -> anyhow::Result<()> {
837 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
838 let mut patch = partial_patch(account_id, self_nonce)?;
839 let other = partial_patch(account_id, other_nonce)?;
840
841 assert_matches!(
842 patch.merge(other).unwrap_err(),
843 AccountPatchError::NonceMustIncrementByOne { current, new } => {
844 assert_eq!(current, Felt::from(self_nonce));
845 assert_eq!(new, Felt::from(other_nonce));
846 }
847 );
848
849 Ok(())
850 }
851
852 #[test]
853 fn account_patch_merge_rejects_storage_slot_type_conflict() -> anyhow::Result<()> {
854 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
855 let shared_slot = StorageSlotName::mock(7);
856
857 let value_storage = AccountStoragePatch::from_iters(
858 [],
859 [(shared_slot.clone(), Word::from([9u32, 0, 0, 0]))],
860 [],
861 );
862 let map_storage = AccountStoragePatch::from_iters(
863 [],
864 [],
865 [(shared_slot.clone(), StorageMapPatch::from_iters([], []))],
866 );
867
868 let mut patch = AccountPatch::new(
869 account_id,
870 value_storage,
871 AccountVaultPatch::default(),
872 None,
873 Some(Felt::from(2u32)),
874 )?;
875 let other = AccountPatch::new(
876 account_id,
877 map_storage,
878 AccountVaultPatch::default(),
879 None,
880 Some(Felt::from(3u32)),
881 )?;
882
883 assert_matches!(
884 patch.merge(other).unwrap_err(),
885 AccountPatchError::StorageSlotUsedAsDifferentTypes(slot) => {
886 assert_eq!(slot, shared_slot);
887 }
888 );
889
890 Ok(())
891 }
892
893 #[test]
894 fn account_patch_merge_overrides_vault_entry() -> anyhow::Result<()> {
895 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
896 let asset_initial: Asset = FungibleAsset::mock(100);
897 let asset_updated: Asset = FungibleAsset::mock(250);
898 assert_eq!(asset_initial.id(), asset_updated.id());
899
900 let mut patch = AccountPatch::new(
901 account_id,
902 AccountStoragePatch::new(),
903 AccountVaultPatch::with_assets([asset_initial]),
904 None,
905 Some(Felt::from(2u32)),
906 )?;
907 let other = AccountPatch::new(
908 account_id,
909 AccountStoragePatch::new(),
910 AccountVaultPatch::with_assets([asset_updated]),
911 None,
912 Some(Felt::from(3u32)),
913 )?;
914
915 patch.merge(other)?;
916
917 assert_eq!(patch.vault().num_assets(), 1);
918 assert_eq!(
919 patch.vault().as_map().get(&asset_updated.id()).copied(),
920 Some(asset_updated.to_value_word())
921 );
922
923 Ok(())
924 }
925
926 #[test]
927 fn account_patch_merge_overrides_storage_value() -> anyhow::Result<()> {
928 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
929 let slot_name = StorageSlotName::mock(1);
930 let initial_value = Word::from([1u32, 0, 0, 0]);
931 let updated_value = Word::from([2u32, 0, 0, 0]);
932
933 let mut patch = AccountPatch::new(
934 account_id,
935 AccountStoragePatch::from_iters([], [(slot_name.clone(), initial_value)], []),
936 AccountVaultPatch::default(),
937 None,
938 Some(Felt::from(2u32)),
939 )?;
940 let other = AccountPatch::new(
941 account_id,
942 AccountStoragePatch::from_iters([], [(slot_name.clone(), updated_value)], []),
943 AccountVaultPatch::default(),
944 None,
945 Some(Felt::from(3u32)),
946 )?;
947
948 patch.merge(other)?;
949
950 assert_eq!(patch.storage().num_slots(), 1);
951 assert_eq!(patch.storage().updated_value(&slot_name), Some(updated_value));
952
953 Ok(())
954 }
955
956 #[test]
957 fn account_patch_merge_extends_storage_map() -> anyhow::Result<()> {
958 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
959 let map_slot = StorageSlotName::mock(1);
960 let key_self = StorageMapKey::from_array([1, 0, 0, 0]);
961 let value_self = Word::from([10u32, 0, 0, 0]);
962 let key_other = StorageMapKey::from_array([2, 0, 0, 0]);
963 let value_other = Word::from([20u32, 0, 0, 0]);
964
965 let mut patch = AccountPatch::new(
966 account_id,
967 AccountStoragePatch::from_iters(
968 [],
969 [],
970 [(map_slot.clone(), StorageMapPatch::from_iters([], [(key_self, value_self)]))],
971 ),
972 AccountVaultPatch::default(),
973 None,
974 Some(Felt::from(2u32)),
975 )?;
976 let other = AccountPatch::new(
977 account_id,
978 AccountStoragePatch::from_iters(
979 [],
980 [],
981 [(map_slot.clone(), StorageMapPatch::from_iters([], [(key_other, value_other)]))],
982 ),
983 AccountVaultPatch::default(),
984 None,
985 Some(Felt::from(3u32)),
986 )?;
987
988 patch.merge(other)?;
989
990 assert_eq!(patch.storage().num_slots(), 1);
991 assert_eq!(patch.storage().updated_map(&map_slot).unwrap().num_entries(), 2);
992 assert_eq!(patch.storage().updated_map_item(&map_slot, &key_self), Some(value_self));
993 assert_eq!(patch.storage().updated_map_item(&map_slot, &key_other), Some(value_other));
994
995 Ok(())
996 }
997
998 #[test]
1001 fn account_patch_merge_full_base_with_partial_stays_full_state() -> anyhow::Result<()> {
1002 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
1003 let code = AccountCode::mock();
1004 let slot_name = StorageSlotName::mock(1);
1005 let created_value = Word::from([1u32, 0, 0, 0]);
1006 let updated_value = Word::from([2u32, 0, 0, 0]);
1007
1008 let mut patch = AccountPatch::new(
1010 account_id,
1011 AccountStoragePatch::builder()
1012 .create_value(slot_name.clone(), created_value)
1013 .build(),
1014 AccountVaultPatch::default(),
1015 Some(code.clone()),
1016 Some(Felt::ONE),
1017 )?;
1018
1019 let other = AccountPatch::new(
1021 account_id,
1022 AccountStoragePatch::builder()
1023 .update_value(slot_name.clone(), updated_value)
1024 .build(),
1025 AccountVaultPatch::default(),
1026 None,
1027 Some(Felt::from(2u32)),
1028 )?;
1029
1030 patch.merge(other)?;
1031
1032 assert!(patch.is_full_state());
1033 assert_eq!(patch.code(), Some(&code));
1034 assert_eq!(patch.final_nonce(), Some(Felt::from(2u32)));
1035 assert!(!patch.storage().contains_non_create_ops());
1036 assert_eq!(patch.storage().created_value(&slot_name), Some(updated_value));
1037
1038 Ok(())
1039 }
1040
1041 #[test]
1043 fn account_patch_merge_empty_other_is_noop() -> anyhow::Result<()> {
1044 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
1045 let mut patch = partial_patch(account_id, 4)?;
1046 let snapshot = patch.clone();
1047
1048 let empty = AccountPatch::empty(account_id);
1049
1050 patch.merge(empty)?;
1051 assert_eq!(patch, snapshot);
1052
1053 Ok(())
1054 }
1055
1056 #[test]
1058 fn account_patch_merge_empty_self_adopts_other() -> anyhow::Result<()> {
1059 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
1060 let mut empty = AccountPatch::empty(account_id);
1061 let other = partial_patch(account_id, 7)?;
1062 let expected = other.clone();
1063
1064 empty.merge(other)?;
1065 assert_eq!(empty, expected);
1066
1067 Ok(())
1068 }
1069}