near_primitives_core/
account.rs

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