near_primitives_core/
account.rs

1use crate::hash::CryptoHash;
2use crate::types::{Balance, Nonce, NonceIndex, StorageUsage};
3use borsh::{BorshDeserialize, BorshSerialize};
4pub use near_account_id as id;
5use near_account_id::AccountId;
6use near_schema_checker_lib::ProtocolSchema;
7use std::borrow::Cow;
8use std::io;
9
10#[derive(
11    BorshSerialize,
12    BorshDeserialize,
13    PartialEq,
14    PartialOrd,
15    Eq,
16    Clone,
17    Copy,
18    Debug,
19    Default,
20    serde::Serialize,
21    serde::Deserialize,
22    ProtocolSchema,
23)]
24#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
25pub enum AccountVersion {
26    #[default]
27    V1,
28    V2,
29}
30
31/// Per account information stored in the state.
32/// When introducing new version:
33/// - introduce new AccountV[NewVersion] struct
34/// - add new Account enum option V[NewVersion](AccountV[NewVersion])
35/// - add new BorshVersionedAccount enum option V[NewVersion](AccountV[NewVersion])
36/// - update SerdeAccount with newly added fields
37/// - update serde ser/deser to properly handle conversions
38#[derive(PartialEq, Eq, Debug, Clone, ProtocolSchema)]
39pub enum Account {
40    V1(AccountV1),
41    V2(AccountV2),
42}
43
44// Original representation of the account.
45#[derive(
46    BorshSerialize,
47    serde::Serialize,
48    serde::Deserialize,
49    PartialEq,
50    Eq,
51    Debug,
52    Clone,
53    ProtocolSchema,
54)]
55pub struct AccountV1 {
56    /// The total not locked tokens.
57    amount: Balance,
58    /// The amount locked due to staking.
59    locked: Balance,
60    /// Hash of the code stored in the storage for this account.
61    code_hash: CryptoHash,
62    /// Storage used by the given account, includes account id, this struct, access keys and other data.
63    storage_usage: StorageUsage,
64}
65
66#[allow(dead_code)]
67impl AccountV1 {
68    fn to_v2(&self) -> AccountV2 {
69        AccountV2 {
70            amount: self.amount,
71            locked: self.locked,
72            storage_usage: self.storage_usage,
73            contract: AccountContract::from_local_code_hash(self.code_hash),
74        }
75    }
76}
77
78#[derive(
79    BorshSerialize,
80    BorshDeserialize,
81    serde::Serialize,
82    serde::Deserialize,
83    PartialEq,
84    Eq,
85    Debug,
86    Clone,
87    ProtocolSchema,
88)]
89pub enum AccountContract {
90    None,
91    Local(CryptoHash),
92    Global(CryptoHash),
93    GlobalByAccount(AccountId),
94}
95
96impl AccountContract {
97    pub fn local_code(&self) -> Option<CryptoHash> {
98        match self {
99            AccountContract::None
100            | AccountContract::GlobalByAccount(_)
101            | AccountContract::Global(_) => None,
102            AccountContract::Local(hash) => Some(*hash),
103        }
104    }
105
106    pub fn from_local_code_hash(code_hash: CryptoHash) -> AccountContract {
107        if code_hash == CryptoHash::default() {
108            AccountContract::None
109        } else {
110            AccountContract::Local(code_hash)
111        }
112    }
113
114    pub fn is_none(&self) -> bool {
115        matches!(self, Self::None)
116    }
117
118    pub fn is_local(&self) -> bool {
119        matches!(self, Self::Local(_))
120    }
121
122    pub fn identifier_storage_usage(&self) -> u64 {
123        match self {
124            AccountContract::None | AccountContract::Local(_) => 0u64,
125            AccountContract::Global(_) => 32u64,
126            AccountContract::GlobalByAccount(id) => id.len() as u64,
127        }
128    }
129}
130
131#[derive(
132    BorshSerialize,
133    BorshDeserialize,
134    serde::Serialize,
135    serde::Deserialize,
136    PartialEq,
137    Eq,
138    Debug,
139    Clone,
140    ProtocolSchema,
141)]
142pub struct AccountV2 {
143    /// The total not locked tokens.
144    amount: Balance,
145    /// The amount locked due to staking.
146    locked: Balance,
147    /// Storage used by the given account, includes account id, this struct, access keys and other data.
148    storage_usage: StorageUsage,
149    /// Type of contract deployed to this account, if any.
150    contract: AccountContract,
151}
152
153impl Account {
154    /// Max number of bytes an account can have in its state (excluding contract code)
155    /// before it is infeasible to delete.
156    pub const MAX_ACCOUNT_DELETION_STORAGE_USAGE: u64 = 10_000;
157    /// HACK: Using u128::MAX as a sentinel value, there are not enough tokens
158    /// in total supply which makes it an invalid value. We use it to
159    /// differentiate AccountVersion V1 from newer versions.
160    const SERIALIZATION_SENTINEL: Balance = Balance::MAX;
161
162    pub fn new(
163        amount: Balance,
164        locked: Balance,
165        contract: AccountContract,
166        storage_usage: StorageUsage,
167    ) -> Self {
168        match contract {
169            AccountContract::None => Self::V1(AccountV1 {
170                amount,
171                locked,
172                code_hash: CryptoHash::default(),
173                storage_usage,
174            }),
175            AccountContract::Local(code_hash) => {
176                Self::V1(AccountV1 { amount, locked, code_hash, storage_usage })
177            }
178            _ => Self::V2(AccountV2 { amount, locked, storage_usage, contract }),
179        }
180    }
181
182    #[inline]
183    pub fn amount(&self) -> Balance {
184        match self {
185            Self::V1(account) => account.amount,
186            Self::V2(account) => account.amount,
187        }
188    }
189
190    #[inline]
191    pub fn locked(&self) -> Balance {
192        match self {
193            Self::V1(account) => account.locked,
194            Self::V2(account) => account.locked,
195        }
196    }
197
198    #[inline]
199    pub fn contract(&self) -> Cow<'_, AccountContract> {
200        match self {
201            Self::V1(account) => {
202                Cow::Owned(AccountContract::from_local_code_hash(account.code_hash))
203            }
204            Self::V2(account) => Cow::Borrowed(&account.contract),
205        }
206    }
207
208    #[inline]
209    pub fn storage_usage(&self) -> StorageUsage {
210        match self {
211            Self::V1(account) => account.storage_usage,
212            Self::V2(account) => account.storage_usage,
213        }
214    }
215
216    #[inline]
217    pub fn version(&self) -> AccountVersion {
218        match self {
219            Self::V1(_) => AccountVersion::V1,
220            Self::V2(_) => AccountVersion::V2,
221        }
222    }
223
224    #[inline]
225    pub fn global_contract_hash(&self) -> Option<CryptoHash> {
226        match self {
227            Self::V2(AccountV2 { contract: AccountContract::Global(hash), .. }) => Some(*hash),
228            Self::V1(_) | Self::V2(_) => None,
229        }
230    }
231
232    #[inline]
233    pub fn global_contract_account_id(&self) -> Option<&AccountId> {
234        match self {
235            Self::V2(AccountV2 { contract: AccountContract::GlobalByAccount(account), .. }) => {
236                Some(account)
237            }
238            Self::V1(_) | Self::V2(_) => None,
239        }
240    }
241
242    #[inline]
243    pub fn local_contract_hash(&self) -> Option<CryptoHash> {
244        match self {
245            Self::V1(account) => {
246                AccountContract::from_local_code_hash(account.code_hash).local_code()
247            }
248            Self::V2(AccountV2 { contract: AccountContract::Local(hash), .. }) => Some(*hash),
249            Self::V2(AccountV2 { contract: AccountContract::None, .. })
250            | Self::V2(AccountV2 { contract: AccountContract::Global(_), .. })
251            | Self::V2(AccountV2 { contract: AccountContract::GlobalByAccount(_), .. }) => None,
252        }
253    }
254
255    #[inline]
256    pub fn set_amount(&mut self, amount: Balance) {
257        match self {
258            Self::V1(account) => account.amount = amount,
259            Self::V2(account) => account.amount = amount,
260        }
261    }
262
263    #[inline]
264    pub fn set_locked(&mut self, locked: Balance) {
265        match self {
266            Self::V1(account) => account.locked = locked,
267            Self::V2(account) => account.locked = locked,
268        }
269    }
270
271    #[inline]
272    pub fn set_contract(&mut self, contract: AccountContract) {
273        match self {
274            Self::V1(account) => match contract {
275                AccountContract::None | AccountContract::Local(_) => {
276                    account.code_hash = contract.local_code().unwrap_or_default();
277                }
278                _ => {
279                    let mut account_v2 = account.to_v2();
280                    account_v2.contract = contract;
281                    *self = Self::V2(account_v2);
282                }
283            },
284            Self::V2(account) => {
285                account.contract = contract;
286            }
287        }
288    }
289
290    #[inline]
291    pub fn set_storage_usage(&mut self, storage_usage: StorageUsage) {
292        match self {
293            Self::V1(account) => account.storage_usage = storage_usage,
294            Self::V2(account) => account.storage_usage = storage_usage,
295        }
296    }
297}
298
299/// Account representation for serde ser/deser that maintains both backward
300/// and forward compatibility.
301#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone, ProtocolSchema)]
302#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
303struct SerdeAccount {
304    amount: Balance,
305    locked: Balance,
306    code_hash: CryptoHash,
307    storage_usage: StorageUsage,
308    /// Version of Account in re migrations and similar.
309    #[serde(default)]
310    version: AccountVersion,
311    /// Global contracts fields
312    #[serde(default, skip_serializing_if = "Option::is_none")]
313    global_contract_hash: Option<CryptoHash>,
314    #[serde(default, skip_serializing_if = "Option::is_none")]
315    global_contract_account_id: Option<AccountId>,
316}
317
318impl<'de> serde::Deserialize<'de> for Account {
319    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
320    where
321        D: serde::Deserializer<'de>,
322    {
323        let account_data = SerdeAccount::deserialize(deserializer)?;
324        if account_data.code_hash != CryptoHash::default()
325            && (account_data.global_contract_hash.is_some()
326                || account_data.global_contract_account_id.is_some())
327        {
328            return Err(serde::de::Error::custom(
329                "An Account can't contain both a local and global contract",
330            ));
331        }
332        if account_data.global_contract_hash.is_some()
333            && account_data.global_contract_account_id.is_some()
334        {
335            return Err(serde::de::Error::custom(
336                "An Account can't contain both types of global contracts",
337            ));
338        }
339
340        match account_data.version {
341            AccountVersion::V1 => Ok(Account::V1(AccountV1 {
342                amount: account_data.amount,
343                locked: account_data.locked,
344                code_hash: account_data.code_hash,
345                storage_usage: account_data.storage_usage,
346            })),
347            AccountVersion::V2 => {
348                let contract = match account_data.global_contract_account_id {
349                    Some(account_id) => AccountContract::GlobalByAccount(account_id),
350                    None => match account_data.global_contract_hash {
351                        Some(hash) => AccountContract::Global(hash),
352                        None => AccountContract::from_local_code_hash(account_data.code_hash),
353                    },
354                };
355
356                Ok(Account::V2(AccountV2 {
357                    amount: account_data.amount,
358                    locked: account_data.locked,
359                    storage_usage: account_data.storage_usage,
360                    contract,
361                }))
362            }
363        }
364    }
365}
366
367impl serde::Serialize for Account {
368    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
369    where
370        S: serde::Serializer,
371    {
372        let version = self.version();
373        let code_hash = self.local_contract_hash().unwrap_or_default();
374        let repr = SerdeAccount {
375            amount: self.amount(),
376            locked: self.locked(),
377            code_hash,
378            storage_usage: self.storage_usage(),
379            version,
380            global_contract_hash: self.global_contract_hash(),
381            global_contract_account_id: self.global_contract_account_id().cloned(),
382        };
383        repr.serialize(serializer)
384    }
385}
386
387#[cfg(feature = "schemars")]
388impl schemars::JsonSchema for Account {
389    fn schema_name() -> std::borrow::Cow<'static, str> {
390        "Account".to_string().into()
391    }
392
393    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
394        SerdeAccount::json_schema(generator)
395    }
396}
397
398#[derive(BorshSerialize, BorshDeserialize)]
399#[borsh(use_discriminant = true)]
400#[repr(u8)]
401enum BorshVersionedAccount {
402    // V1 is not included since it is serialized directly without being wrapped in enum
403    V2(AccountV2) = 0,
404}
405
406impl BorshDeserialize for Account {
407    fn deserialize_reader<R: io::Read>(rd: &mut R) -> io::Result<Self> {
408        // The first value of all Account serialization formats is a u128,
409        // either a sentinel or a balance.
410        let sentinel_or_amount = Balance::deserialize_reader(rd)?;
411        if sentinel_or_amount == Account::SERIALIZATION_SENTINEL {
412            let versioned_account = BorshVersionedAccount::deserialize_reader(rd)?;
413            let account = match versioned_account {
414                BorshVersionedAccount::V2(account_v2) => Account::V2(account_v2),
415            };
416            Ok(account)
417        } else {
418            // Legacy unversioned representation of Account
419            let locked = Balance::deserialize_reader(rd)?;
420            let code_hash = CryptoHash::deserialize_reader(rd)?;
421            let storage_usage = StorageUsage::deserialize_reader(rd)?;
422
423            Ok(Account::V1(AccountV1 {
424                amount: sentinel_or_amount,
425                locked,
426                code_hash,
427                storage_usage,
428            }))
429        }
430    }
431}
432
433impl BorshSerialize for Account {
434    fn serialize<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
435        let versioned_account = match self {
436            Account::V1(account_v1) => return account_v1.serialize(writer),
437            Account::V2(account_v2) => BorshVersionedAccount::V2(account_v2.clone()),
438        };
439        let sentinel = Account::SERIALIZATION_SENTINEL;
440        BorshSerialize::serialize(&sentinel, writer)?;
441        BorshSerialize::serialize(&versioned_account, writer)
442    }
443}
444
445/// Access key provides limited access to an account. Each access key belongs to some account and
446/// is identified by a unique (within the account) public key. One account may have large number of
447/// access keys. Access keys allow to act on behalf of the account by restricting transactions
448/// that can be issued.
449/// `account_id,public_key` is a key in the state
450#[derive(
451    BorshSerialize,
452    BorshDeserialize,
453    PartialEq,
454    Eq,
455    Hash,
456    Clone,
457    Debug,
458    serde::Serialize,
459    serde::Deserialize,
460    ProtocolSchema,
461)]
462#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
463pub struct AccessKey {
464    /// Nonce for this access key, used for tx nonce generation. When access key is created, nonce
465    /// is set to `(block_height - 1) * 1e6` to avoid tx hash collision on access key re-creation.
466    /// See <https://github.com/near/nearcore/issues/3779> for more details.
467    pub nonce: Nonce,
468
469    /// Defines permissions for this access key.
470    pub permission: AccessKeyPermission,
471}
472
473impl AccessKey {
474    pub const ACCESS_KEY_NONCE_RANGE_MULTIPLIER: u64 = 1_000_000;
475
476    pub fn full_access() -> Self {
477        Self { nonce: 0, permission: AccessKeyPermission::FullAccess }
478    }
479}
480
481/// Gas key is like an access key, except it stores a balance separately, and transactions signed
482/// with it deduct their cost from the gas key balance instead of the account balance.
483#[derive(
484    BorshSerialize,
485    BorshDeserialize,
486    PartialEq,
487    Eq,
488    Hash,
489    Clone,
490    Debug,
491    serde::Serialize,
492    serde::Deserialize,
493    ProtocolSchema,
494)]
495#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
496pub struct GasKey {
497    /// The number of nonces this gas key has.
498    pub num_nonces: NonceIndex,
499    /// The balance of the gas key.
500    pub balance: Balance,
501    /// Defines the permissions for this gas key.
502    /// If this is a `FunctionCallPermission`, the allowance must be None (unlimited).
503    pub permission: AccessKeyPermission,
504}
505
506/// Defines permissions for AccessKey
507#[derive(
508    BorshSerialize,
509    BorshDeserialize,
510    PartialEq,
511    Eq,
512    Hash,
513    Clone,
514    Debug,
515    serde::Serialize,
516    serde::Deserialize,
517    ProtocolSchema,
518)]
519#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
520pub enum AccessKeyPermission {
521    FunctionCall(FunctionCallPermission),
522
523    /// Grants full access to the account.
524    /// NOTE: It's used to replace account-level public keys.
525    FullAccess,
526}
527
528/// Grants limited permission to make transactions with FunctionCallActions
529/// The permission can limit the allowed balance to be spent on the prepaid gas.
530/// It also restrict the account ID of the receiver for this function call.
531/// It also can restrict the method name for the allowed function calls.
532#[derive(
533    BorshSerialize,
534    BorshDeserialize,
535    serde::Serialize,
536    serde::Deserialize,
537    PartialEq,
538    Eq,
539    Hash,
540    Clone,
541    Debug,
542    ProtocolSchema,
543)]
544#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
545pub struct FunctionCallPermission {
546    /// Allowance is a balance limit to use by this access key to pay for function call gas and
547    /// transaction fees. When this access key is used, both account balance and the allowance is
548    /// decreased by the same value.
549    /// `None` means unlimited allowance.
550    /// NOTE: To change or increase the allowance, the old access key needs to be deleted and a new
551    /// access key should be created.
552    pub allowance: Option<Balance>,
553
554    // This isn't an AccountId because already existing records in testnet genesis have invalid
555    // values for this field (see: https://github.com/near/nearcore/pull/4621#issuecomment-892099860)
556    // we accommodate those by using a string, allowing us to read and parse genesis.
557    /// The access key only allows transactions with the given receiver's account id.
558    pub receiver_id: String,
559
560    /// A list of method names that can be used. The access key only allows transactions with the
561    /// function call of one of the given method names.
562    /// Empty list means any method name can be used.
563    pub method_names: Vec<String>,
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569
570    fn create_serde_account(
571        code_hash: CryptoHash,
572        global_contract_hash: Option<CryptoHash>,
573        global_contract_account_id: Option<AccountId>,
574    ) -> SerdeAccount {
575        SerdeAccount {
576            amount: Balance::from_yoctonear(10_000_000),
577            locked: Balance::from_yoctonear(100_000),
578            code_hash,
579            storage_usage: 1000,
580            version: AccountVersion::V2,
581            global_contract_hash,
582            global_contract_account_id,
583        }
584    }
585
586    #[test]
587    fn test_v1_account_serde_serialization() {
588        let old_account = AccountV1 {
589            amount: Balance::from_yoctonear(1_000_000),
590            locked: Balance::from_yoctonear(1_000_000),
591            code_hash: CryptoHash::hash_bytes(&[42]),
592            storage_usage: 100,
593        };
594
595        let serialized_account = serde_json::to_string(&old_account).unwrap();
596        let expected_serde_repr = SerdeAccount {
597            amount: old_account.amount,
598            locked: old_account.locked,
599            code_hash: old_account.code_hash,
600            storage_usage: old_account.storage_usage,
601            version: AccountVersion::V1,
602            global_contract_hash: None,
603            global_contract_account_id: None,
604        };
605        let actual_serde_repr: SerdeAccount = serde_json::from_str(&serialized_account).unwrap();
606        assert_eq!(actual_serde_repr, expected_serde_repr);
607
608        let new_account: Account = serde_json::from_str(&serialized_account).unwrap();
609        assert_eq!(new_account, Account::V1(old_account));
610
611        let new_serialized_account = serde_json::to_string(&new_account).unwrap();
612        let deserialized_account: Account = serde_json::from_str(&new_serialized_account).unwrap();
613        assert_eq!(deserialized_account, new_account);
614    }
615
616    #[test]
617    fn test_v1_account_borsh_serialization() {
618        let old_account = AccountV1 {
619            amount: Balance::from_yoctonear(100),
620            locked: Balance::from_yoctonear(200),
621            code_hash: CryptoHash::hash_bytes(&[42]),
622            storage_usage: 300,
623        };
624        let old_bytes = borsh::to_vec(&old_account).unwrap();
625        let new_account = <Account as BorshDeserialize>::deserialize(&mut &old_bytes[..]).unwrap();
626        assert_eq!(new_account, Account::V1(old_account));
627
628        let new_bytes = borsh::to_vec(&new_account).unwrap();
629        assert_eq!(new_bytes, old_bytes);
630        let deserialized_account =
631            <Account as BorshDeserialize>::deserialize(&mut &new_bytes[..]).unwrap();
632        assert_eq!(deserialized_account, new_account);
633    }
634
635    #[test]
636    fn test_account_v2_serde_serialization() {
637        let account_v2 = AccountV2 {
638            amount: Balance::from_yoctonear(10_000_000),
639            locked: Balance::from_yoctonear(100_000),
640            storage_usage: 1000,
641            contract: AccountContract::Local(CryptoHash::hash_bytes(&[42])),
642        };
643        let account = Account::V2(account_v2.clone());
644
645        let serialized_account = serde_json::to_string(&account).unwrap();
646        let expected_serde_repr = SerdeAccount {
647            amount: account_v2.amount,
648            locked: account_v2.locked,
649            code_hash: account_v2.contract.local_code().unwrap_or_default(),
650            storage_usage: account_v2.storage_usage,
651            version: AccountVersion::V2,
652            global_contract_hash: None,
653            global_contract_account_id: None,
654        };
655        let actual_serde_repr: SerdeAccount = serde_json::from_str(&serialized_account).unwrap();
656        assert_eq!(actual_serde_repr, expected_serde_repr);
657
658        let deserialized_account: Account = serde_json::from_str(&serialized_account).unwrap();
659        assert_eq!(deserialized_account, account);
660    }
661
662    #[test]
663    fn test_account_v2_borsh_serialization() {
664        let account_v2 = AccountV2 {
665            amount: Balance::from_yoctonear(10_000_000),
666            locked: Balance::from_yoctonear(100_000),
667            storage_usage: 1000,
668            contract: AccountContract::Global(CryptoHash::hash_bytes(&[42])),
669        };
670        let account = Account::V2(account_v2);
671        let serialized_account = borsh::to_vec(&account).unwrap();
672        let deserialized_account =
673            <Account as BorshDeserialize>::deserialize(&mut &serialized_account[..]).unwrap();
674        assert_eq!(deserialized_account, account);
675    }
676
677    #[test]
678    fn test_account_v2_serde_deserialization_fails_with_local_hash_and_global_account_id() {
679        let id = AccountId::try_from("test.near".to_string()).unwrap();
680        let code_hash = CryptoHash::hash_bytes(&[42]);
681
682        let serde_repr = create_serde_account(code_hash, None, Some(id));
683
684        let serde_string = serde_json::to_string(&serde_repr).unwrap();
685        let deserialization_attempt: Result<Account, _> = serde_json::from_str(&serde_string);
686        assert!(deserialization_attempt.is_err());
687    }
688
689    #[test]
690    fn test_account_v2_serde_deserialization_fails_with_local_and_global_hashes() {
691        let code_hash = CryptoHash::hash_bytes(&[42]);
692
693        let serde_repr = create_serde_account(code_hash, Some(code_hash), None);
694
695        let serde_string = serde_json::to_string(&serde_repr).unwrap();
696        let deserialization_attempt: Result<Account, _> = serde_json::from_str(&serde_string);
697        assert!(deserialization_attempt.is_err());
698    }
699
700    #[test]
701    fn test_account_v2_serde_deserialization_fails_if_both_types_of_global_contract_are_present() {
702        let id = AccountId::try_from("test.near".to_string()).unwrap();
703        let serde_repr = create_serde_account(
704            CryptoHash::default(),
705            Some(CryptoHash::hash_bytes(&[42])),
706            Some(id),
707        );
708
709        let serde_string = serde_json::to_string(&serde_repr).unwrap();
710        let deserialization_attempt: Result<Account, _> = serde_json::from_str(&serde_string);
711        assert!(deserialization_attempt.is_err());
712    }
713
714    #[test]
715    fn test_account_version_upgrade_behaviour() {
716        let account_v1 = AccountV1 {
717            amount: Balance::from_yoctonear(100),
718            locked: Balance::from_yoctonear(200),
719            code_hash: CryptoHash::hash_bytes(&[42]),
720            storage_usage: 300,
721        };
722        let mut account = Account::V1(account_v1);
723        let contract = AccountContract::Local(CryptoHash::hash_bytes(&[42]));
724        account.set_contract(contract);
725        assert!(matches!(account, Account::V1(_)));
726
727        let contract = AccountContract::None;
728        account.set_contract(contract);
729        assert!(matches!(account, Account::V1(_)));
730
731        let contract = AccountContract::Global(CryptoHash::hash_bytes(&[42]));
732        account.set_contract(contract);
733        assert!(matches!(account, Account::V2(_)));
734    }
735}