1use std::collections::BTreeMap;
4
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use borsh::{BorshDeserialize, BorshSerialize};
7use serde::{Deserialize, Serialize};
8use serde_with::base64::Base64;
9use serde_with::serde_as;
10use sha3::{Digest, Keccak256};
11
12use super::{AccountId, CryptoHash, Gas, NearToken, PublicKey, Signature, TryIntoAccountId};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum PublishMode {
19 Updatable,
22 Immutable,
25}
26
27pub trait GlobalContractRef {
37 fn into_identifier(self) -> GlobalContractIdentifier;
38}
39
40impl GlobalContractRef for CryptoHash {
41 fn into_identifier(self) -> GlobalContractIdentifier {
42 GlobalContractIdentifier::CodeHash(self)
43 }
44}
45
46impl GlobalContractRef for AccountId {
47 fn into_identifier(self) -> GlobalContractIdentifier {
48 GlobalContractIdentifier::AccountId(self)
49 }
50}
51
52impl GlobalContractRef for &AccountId {
53 fn into_identifier(self) -> GlobalContractIdentifier {
54 GlobalContractIdentifier::AccountId(self.clone())
55 }
56}
57
58impl GlobalContractRef for &str {
59 fn into_identifier(self) -> GlobalContractIdentifier {
60 let account_id: AccountId = self.try_into_account_id().expect("invalid account ID");
61 GlobalContractIdentifier::AccountId(account_id)
62 }
63}
64
65impl GlobalContractRef for String {
66 fn into_identifier(self) -> GlobalContractIdentifier {
67 let account_id: AccountId = self.try_into_account_id().expect("invalid account ID");
68 GlobalContractIdentifier::AccountId(account_id)
69 }
70}
71
72impl GlobalContractRef for &String {
73 fn into_identifier(self) -> GlobalContractIdentifier {
74 let account_id: AccountId = self
75 .as_str()
76 .try_into_account_id()
77 .expect("invalid account ID");
78 GlobalContractIdentifier::AccountId(account_id)
79 }
80}
81
82pub const DELEGATE_ACTION_PREFIX: u32 = 1_073_742_190;
89
90#[derive(
94 Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
95)]
96pub struct GasKeyInfo {
97 pub balance: NearToken,
99 pub num_nonces: u16,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
109pub enum AccessKeyPermission {
110 FunctionCall(FunctionCallPermission),
112 FullAccess,
114 GasKeyFunctionCall(GasKeyInfo, FunctionCallPermission),
116 GasKeyFullAccess(GasKeyInfo),
118}
119
120impl AccessKeyPermission {
121 pub fn function_call(
123 receiver_id: AccountId,
124 method_names: Vec<String>,
125 allowance: Option<NearToken>,
126 ) -> Self {
127 Self::FunctionCall(FunctionCallPermission {
128 allowance,
129 receiver_id,
130 method_names,
131 })
132 }
133
134 pub fn full_access() -> Self {
136 Self::FullAccess
137 }
138}
139
140#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
142pub struct FunctionCallPermission {
143 pub allowance: Option<NearToken>,
145 pub receiver_id: AccountId,
147 pub method_names: Vec<String>,
149}
150
151#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
153pub struct AccessKey {
154 pub nonce: u64,
156 pub permission: AccessKeyPermission,
158}
159
160impl AccessKey {
161 pub fn full_access() -> Self {
163 Self {
164 nonce: 0,
165 permission: AccessKeyPermission::FullAccess,
166 }
167 }
168
169 pub fn function_call(
171 receiver_id: AccountId,
172 method_names: Vec<String>,
173 allowance: Option<NearToken>,
174 ) -> Self {
175 Self {
176 nonce: 0,
177 permission: AccessKeyPermission::function_call(receiver_id, method_names, allowance),
178 }
179 }
180}
181
182#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
191pub enum Action {
192 CreateAccount(CreateAccountAction),
194 DeployContract(DeployContractAction),
196 FunctionCall(FunctionCallAction),
198 Transfer(TransferAction),
200 Stake(StakeAction),
202 AddKey(AddKeyAction),
204 DeleteKey(DeleteKeyAction),
206 DeleteAccount(DeleteAccountAction),
208 Delegate(Box<SignedDelegateAction>),
210 DeployGlobalContract(DeployGlobalContractAction),
212 UseGlobalContract(UseGlobalContractAction),
214 DeterministicStateInit(DeterministicStateInitAction),
216 TransferToGasKey(TransferToGasKeyAction),
218 WithdrawFromGasKey(WithdrawFromGasKeyAction),
220}
221
222#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
224pub struct CreateAccountAction;
225
226#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
228pub struct DeployContractAction {
229 pub code: Vec<u8>,
231}
232
233#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
235pub struct FunctionCallAction {
236 pub method_name: String,
238 pub args: Vec<u8>,
240 pub gas: Gas,
242 pub deposit: NearToken,
244}
245
246#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
248pub struct TransferAction {
249 pub deposit: NearToken,
251}
252
253#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
255pub struct StakeAction {
256 pub stake: NearToken,
258 pub public_key: PublicKey,
260}
261
262#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
264pub struct AddKeyAction {
265 pub public_key: PublicKey,
267 pub access_key: AccessKey,
269}
270
271#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
273pub struct DeleteKeyAction {
274 pub public_key: PublicKey,
276}
277
278#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
280pub struct DeleteAccountAction {
281 pub beneficiary_id: AccountId,
283}
284
285#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
294pub enum GlobalContractIdentifier {
295 #[serde(rename = "hash")]
298 CodeHash(CryptoHash),
299 #[serde(rename = "account_id")]
302 AccountId(AccountId),
303}
304
305#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
309#[repr(u8)]
310pub enum GlobalContractDeployMode {
311 CodeHash,
314 AccountId,
317}
318
319#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
325pub struct DeployGlobalContractAction {
326 pub code: Vec<u8>,
328 pub deploy_mode: GlobalContractDeployMode,
330}
331
332#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
337pub struct UseGlobalContractAction {
338 pub contract_identifier: GlobalContractIdentifier,
340}
341
342#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
350#[repr(u8)]
351pub enum DeterministicAccountStateInit {
352 V1(DeterministicAccountStateInitV1),
354}
355
356#[serde_as]
358#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
359pub struct DeterministicAccountStateInitV1 {
360 pub code: GlobalContractIdentifier,
362 #[serde_as(as = "BTreeMap<Base64, Base64>")]
365 pub data: BTreeMap<Vec<u8>, Vec<u8>>,
366}
367
368#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
373pub struct DeterministicStateInitAction {
374 pub state_init: DeterministicAccountStateInit,
376 pub deposit: NearToken,
378}
379
380impl DeterministicAccountStateInit {
381 pub fn by_hash(code_hash: CryptoHash, data: BTreeMap<Vec<u8>, Vec<u8>>) -> Self {
383 Self::V1(DeterministicAccountStateInitV1 {
384 code: GlobalContractIdentifier::CodeHash(code_hash),
385 data,
386 })
387 }
388
389 pub fn by_publisher(publisher_id: AccountId, data: BTreeMap<Vec<u8>, Vec<u8>>) -> Self {
391 Self::V1(DeterministicAccountStateInitV1 {
392 code: GlobalContractIdentifier::AccountId(publisher_id),
393 data,
394 })
395 }
396
397 pub fn derive_account_id(&self) -> AccountId {
418 let serialized = borsh::to_vec(self).expect("StateInit serialization should not fail");
420
421 let hash = Keccak256::digest(&serialized);
423
424 let suffix = &hash[12..32];
426
427 let account_str = format!("0s{}", hex::encode(suffix));
429
430 account_str
432 .parse()
433 .expect("deterministic account ID should always be valid")
434 }
435}
436
437impl DeterministicStateInitAction {
438 pub fn derive_account_id(&self) -> AccountId {
442 self.state_init.derive_account_id()
443 }
444}
445
446#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
448pub struct TransferToGasKeyAction {
449 pub public_key: PublicKey,
451 pub deposit: NearToken,
453}
454
455#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
457pub struct WithdrawFromGasKeyAction {
458 pub public_key: PublicKey,
460 pub amount: NearToken,
462}
463
464#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
466pub struct DelegateAction {
467 pub sender_id: AccountId,
469 pub receiver_id: AccountId,
471 pub actions: Vec<NonDelegateAction>,
473 pub nonce: u64,
475 pub max_block_height: u64,
477 pub public_key: PublicKey,
479}
480
481#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
483pub struct SignedDelegateAction {
484 pub delegate_action: DelegateAction,
486 pub signature: super::Signature,
488}
489
490#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
499pub struct NonDelegateAction(Action);
500
501impl Action {
503 pub fn create_account() -> Self {
505 Self::CreateAccount(CreateAccountAction)
506 }
507
508 pub fn deploy_contract(code: Vec<u8>) -> Self {
510 Self::DeployContract(DeployContractAction { code })
511 }
512
513 pub fn function_call(
515 method_name: impl Into<String>,
516 args: Vec<u8>,
517 gas: Gas,
518 deposit: NearToken,
519 ) -> Self {
520 Self::FunctionCall(FunctionCallAction {
521 method_name: method_name.into(),
522 args,
523 gas,
524 deposit,
525 })
526 }
527
528 pub fn transfer(deposit: NearToken) -> Self {
530 Self::Transfer(TransferAction { deposit })
531 }
532
533 pub fn stake(stake: NearToken, public_key: PublicKey) -> Self {
535 Self::Stake(StakeAction { stake, public_key })
536 }
537
538 pub fn add_full_access_key(public_key: PublicKey) -> Self {
540 Self::AddKey(AddKeyAction {
541 public_key,
542 access_key: AccessKey::full_access(),
543 })
544 }
545
546 pub fn add_function_call_key(
548 public_key: PublicKey,
549 receiver_id: AccountId,
550 method_names: Vec<String>,
551 allowance: Option<NearToken>,
552 ) -> Self {
553 Self::AddKey(AddKeyAction {
554 public_key,
555 access_key: AccessKey::function_call(receiver_id, method_names, allowance),
556 })
557 }
558
559 pub fn delete_key(public_key: PublicKey) -> Self {
561 Self::DeleteKey(DeleteKeyAction { public_key })
562 }
563
564 pub fn delete_account(beneficiary_id: AccountId) -> Self {
566 Self::DeleteAccount(DeleteAccountAction { beneficiary_id })
567 }
568
569 pub fn delegate(signed_delegate: SignedDelegateAction) -> Self {
571 Self::Delegate(Box::new(signed_delegate))
572 }
573
574 pub fn publish(code: Vec<u8>, mode: PublishMode) -> Self {
600 Self::DeployGlobalContract(DeployGlobalContractAction {
601 code,
602 deploy_mode: match mode {
603 PublishMode::Updatable => GlobalContractDeployMode::AccountId,
604 PublishMode::Immutable => GlobalContractDeployMode::CodeHash,
605 },
606 })
607 }
608
609 pub fn deploy_from_hash(code_hash: CryptoHash) -> Self {
613 Self::UseGlobalContract(UseGlobalContractAction {
614 contract_identifier: GlobalContractIdentifier::CodeHash(code_hash),
615 })
616 }
617
618 pub fn deploy_from_account(account_id: AccountId) -> Self {
623 Self::UseGlobalContract(UseGlobalContractAction {
624 contract_identifier: GlobalContractIdentifier::AccountId(account_id),
625 })
626 }
627
628 pub fn state_init(state_init: DeterministicAccountStateInit, deposit: NearToken) -> Self {
633 Self::DeterministicStateInit(DeterministicStateInitAction {
634 state_init,
635 deposit,
636 })
637 }
638
639 pub fn transfer_to_gas_key(public_key: PublicKey, deposit: NearToken) -> Self {
641 Self::TransferToGasKey(TransferToGasKeyAction {
642 public_key,
643 deposit,
644 })
645 }
646
647 pub fn withdraw_from_gas_key(public_key: PublicKey, amount: NearToken) -> Self {
649 Self::WithdrawFromGasKey(WithdrawFromGasKeyAction { public_key, amount })
650 }
651}
652
653impl DelegateAction {
654 pub fn serialize_for_signing(&self) -> Vec<u8> {
667 let prefix_bytes = DELEGATE_ACTION_PREFIX.to_le_bytes();
668 let action_bytes =
669 borsh::to_vec(self).expect("delegate action serialization should never fail");
670
671 let mut result = Vec::with_capacity(prefix_bytes.len() + action_bytes.len());
672 result.extend_from_slice(&prefix_bytes);
673 result.extend_from_slice(&action_bytes);
674 result
675 }
676
677 pub fn get_hash(&self) -> CryptoHash {
679 let bytes = self.serialize_for_signing();
680 CryptoHash::hash(&bytes)
681 }
682
683 pub fn sign(self, signature: Signature) -> SignedDelegateAction {
685 SignedDelegateAction {
686 delegate_action: self,
687 signature,
688 }
689 }
690}
691
692impl SignedDelegateAction {
693 pub fn to_bytes(&self) -> Vec<u8> {
695 borsh::to_vec(self).expect("signed delegate action serialization should never fail")
696 }
697
698 pub fn to_base64(&self) -> String {
702 STANDARD.encode(self.to_bytes())
703 }
704
705 pub fn from_bytes(bytes: &[u8]) -> Result<Self, borsh::io::Error> {
707 borsh::from_slice(bytes)
708 }
709
710 pub fn from_base64(s: &str) -> Result<Self, DecodeError> {
712 let bytes = STANDARD.decode(s).map_err(DecodeError::Base64)?;
713 Self::from_bytes(&bytes).map_err(DecodeError::Borsh)
714 }
715
716 pub fn sender_id(&self) -> &AccountId {
718 &self.delegate_action.sender_id
719 }
720
721 pub fn receiver_id(&self) -> &AccountId {
723 &self.delegate_action.receiver_id
724 }
725}
726
727#[derive(Debug, thiserror::Error)]
729pub enum DecodeError {
730 #[error("base64 decode error: {0}")]
732 Base64(#[from] base64::DecodeError),
733 #[error("borsh decode error: {0}")]
735 Borsh(#[from] borsh::io::Error),
736}
737
738impl NonDelegateAction {
739 pub fn from_action(action: Action) -> Option<Self> {
741 if matches!(action, Action::Delegate(_)) {
742 None
743 } else {
744 Some(Self(action))
745 }
746 }
747
748 pub fn inner(&self) -> &Action {
750 &self.0
751 }
752
753 pub fn into_inner(self) -> Action {
755 self.0
756 }
757}
758
759impl From<NonDelegateAction> for Action {
760 fn from(action: NonDelegateAction) -> Self {
761 action.0
762 }
763}
764
765impl TryFrom<Action> for NonDelegateAction {
766 type Error = ();
767
768 fn try_from(action: Action) -> Result<Self, Self::Error> {
769 Self::from_action(action).ok_or(())
770 }
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776 use crate::types::{Gas, NearToken, SecretKey};
777
778 fn create_test_delegate_action() -> DelegateAction {
779 let sender_id: AccountId = "alice.testnet".parse().unwrap();
780 let receiver_id: AccountId = "bob.testnet".parse().unwrap();
781 let public_key: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
782 .parse()
783 .unwrap();
784
785 DelegateAction {
786 sender_id,
787 receiver_id,
788 actions: vec![
789 NonDelegateAction::from_action(Action::Transfer(TransferAction {
790 deposit: NearToken::from_near(1),
791 }))
792 .unwrap(),
793 ],
794 nonce: 1,
795 max_block_height: 1000,
796 public_key,
797 }
798 }
799
800 #[test]
801 fn test_delegate_action_prefix() {
802 assert_eq!(DELEGATE_ACTION_PREFIX, 1073742190);
804 assert_eq!(DELEGATE_ACTION_PREFIX, (1 << 30) + 366);
805 }
806
807 #[test]
808 fn test_delegate_action_serialize_for_signing() {
809 let delegate_action = create_test_delegate_action();
810 let bytes = delegate_action.serialize_for_signing();
811
812 let prefix_bytes = &bytes[0..4];
814 let prefix = u32::from_le_bytes(prefix_bytes.try_into().unwrap());
815 assert_eq!(prefix, DELEGATE_ACTION_PREFIX);
816
817 let action_bytes = &bytes[4..];
819 let expected_action_bytes = borsh::to_vec(&delegate_action).unwrap();
820 assert_eq!(action_bytes, expected_action_bytes.as_slice());
821 }
822
823 #[test]
824 fn test_delegate_action_get_hash() {
825 let delegate_action = create_test_delegate_action();
826 let hash = delegate_action.get_hash();
827
828 let bytes = delegate_action.serialize_for_signing();
830 let expected_hash = CryptoHash::hash(&bytes);
831 assert_eq!(hash, expected_hash);
832 }
833
834 #[test]
835 fn test_signed_delegate_action_roundtrip_bytes() {
836 let delegate_action = create_test_delegate_action();
837 let secret_key = SecretKey::generate_ed25519();
838 let hash = delegate_action.get_hash();
839 let signature = secret_key.sign(hash.as_bytes());
840 let signed = delegate_action.sign(signature);
841
842 let bytes = signed.to_bytes();
844 let decoded = SignedDelegateAction::from_bytes(&bytes).unwrap();
845
846 assert_eq!(decoded.sender_id().as_str(), signed.sender_id().as_str());
847 assert_eq!(
848 decoded.receiver_id().as_str(),
849 signed.receiver_id().as_str()
850 );
851 assert_eq!(decoded.delegate_action.nonce, signed.delegate_action.nonce);
852 assert_eq!(
853 decoded.delegate_action.max_block_height,
854 signed.delegate_action.max_block_height
855 );
856 }
857
858 #[test]
859 fn test_signed_delegate_action_roundtrip_base64() {
860 let delegate_action = create_test_delegate_action();
861 let secret_key = SecretKey::generate_ed25519();
862 let hash = delegate_action.get_hash();
863 let signature = secret_key.sign(hash.as_bytes());
864 let signed = delegate_action.sign(signature);
865
866 let base64 = signed.to_base64();
868 let decoded = SignedDelegateAction::from_base64(&base64).unwrap();
869
870 assert_eq!(decoded.sender_id().as_str(), signed.sender_id().as_str());
871 assert_eq!(
872 decoded.receiver_id().as_str(),
873 signed.receiver_id().as_str()
874 );
875 }
876
877 #[test]
878 fn test_signed_delegate_action_accessors() {
879 let delegate_action = create_test_delegate_action();
880 let secret_key = SecretKey::generate_ed25519();
881 let hash = delegate_action.get_hash();
882 let signature = secret_key.sign(hash.as_bytes());
883 let signed = delegate_action.sign(signature);
884
885 assert_eq!(signed.sender_id().as_str(), "alice.testnet");
886 assert_eq!(signed.receiver_id().as_str(), "bob.testnet");
887 }
888
889 #[test]
890 fn test_non_delegate_action_from_action() {
891 let transfer = Action::Transfer(TransferAction {
893 deposit: NearToken::from_near(1),
894 });
895 assert!(NonDelegateAction::from_action(transfer).is_some());
896
897 let call = Action::FunctionCall(FunctionCallAction {
899 method_name: "test".to_string(),
900 args: vec![],
901 gas: Gas::default(),
902 deposit: NearToken::ZERO,
903 });
904 assert!(NonDelegateAction::from_action(call).is_some());
905
906 let delegate_action = create_test_delegate_action();
908 let secret_key = SecretKey::generate_ed25519();
909 let hash = delegate_action.get_hash();
910 let signature = secret_key.sign(hash.as_bytes());
911 let signed = delegate_action.sign(signature);
912 let delegate = Action::delegate(signed);
913 assert!(NonDelegateAction::from_action(delegate).is_none());
914 }
915
916 #[test]
917 fn test_decode_error_display() {
918 let base64_err = DecodeError::Base64(base64::DecodeError::InvalidLength(5));
920 assert!(format!("{}", base64_err).contains("base64"));
921
922 }
924
925 #[test]
930 fn test_action_discriminants() {
931 let create_account = Action::create_account();
938 let bytes = borsh::to_vec(&create_account).unwrap();
939 assert_eq!(bytes[0], 0, "CreateAccount should have discriminant 0");
940
941 let deploy = Action::deploy_contract(vec![1, 2, 3]);
942 let bytes = borsh::to_vec(&deploy).unwrap();
943 assert_eq!(bytes[0], 1, "DeployContract should have discriminant 1");
944
945 let transfer = Action::transfer(NearToken::from_near(1));
946 let bytes = borsh::to_vec(&transfer).unwrap();
947 assert_eq!(bytes[0], 3, "Transfer should have discriminant 3");
948
949 let publish = Action::publish(vec![1, 2, 3], PublishMode::Updatable);
951 let bytes = borsh::to_vec(&publish).unwrap();
952 assert_eq!(
953 bytes[0], 9,
954 "DeployGlobalContract should have discriminant 9"
955 );
956
957 let code_hash = CryptoHash::hash(&[1, 2, 3]);
959 let use_global = Action::deploy_from_hash(code_hash);
960 let bytes = borsh::to_vec(&use_global).unwrap();
961 assert_eq!(
962 bytes[0], 10,
963 "UseGlobalContract should have discriminant 10"
964 );
965
966 let state_init = Action::state_init(
968 DeterministicAccountStateInit::by_hash(code_hash, BTreeMap::new()),
969 NearToken::from_near(1),
970 );
971 let bytes = borsh::to_vec(&state_init).unwrap();
972 assert_eq!(
973 bytes[0], 11,
974 "DeterministicStateInit should have discriminant 11"
975 );
976
977 let pk: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
979 .parse()
980 .unwrap();
981 let transfer_gas = Action::transfer_to_gas_key(pk.clone(), NearToken::from_near(1));
982 let bytes = borsh::to_vec(&transfer_gas).unwrap();
983 assert_eq!(bytes[0], 12, "TransferToGasKey should have discriminant 12");
984
985 let withdraw_gas = Action::withdraw_from_gas_key(pk, NearToken::from_near(1));
987 let bytes = borsh::to_vec(&withdraw_gas).unwrap();
988 assert_eq!(
989 bytes[0], 13,
990 "WithdrawFromGasKey should have discriminant 13"
991 );
992 }
993
994 #[test]
995 fn test_global_contract_deploy_mode_serialization() {
996 let by_hash = GlobalContractDeployMode::CodeHash;
998 let bytes = borsh::to_vec(&by_hash).unwrap();
999 assert_eq!(bytes, vec![0], "CodeHash mode should serialize to 0");
1000
1001 let by_account = GlobalContractDeployMode::AccountId;
1002 let bytes = borsh::to_vec(&by_account).unwrap();
1003 assert_eq!(bytes, vec![1], "AccountId mode should serialize to 1");
1004 }
1005
1006 #[test]
1007 fn test_global_contract_identifier_serialization() {
1008 let hash = CryptoHash::hash(&[1, 2, 3]);
1010 let by_hash = GlobalContractIdentifier::CodeHash(hash);
1011 let bytes = borsh::to_vec(&by_hash).unwrap();
1012 assert_eq!(
1013 bytes[0], 0,
1014 "CodeHash identifier should have discriminant 0"
1015 );
1016 assert_eq!(
1017 bytes.len(),
1018 1 + 32,
1019 "Should be 1 byte discriminant + 32 byte hash"
1020 );
1021
1022 let account_id: AccountId = "test.near".parse().unwrap();
1023 let by_account = GlobalContractIdentifier::AccountId(account_id);
1024 let bytes = borsh::to_vec(&by_account).unwrap();
1025 assert_eq!(
1026 bytes[0], 1,
1027 "AccountId identifier should have discriminant 1"
1028 );
1029 }
1030
1031 #[test]
1032 fn test_deploy_global_contract_action_roundtrip() {
1033 let code = vec![0, 97, 115, 109]; let action = DeployGlobalContractAction {
1035 code: code.clone(),
1036 deploy_mode: GlobalContractDeployMode::CodeHash,
1037 };
1038
1039 let bytes = borsh::to_vec(&action).unwrap();
1040 let decoded: DeployGlobalContractAction = borsh::from_slice(&bytes).unwrap();
1041
1042 assert_eq!(decoded.code, code);
1043 assert_eq!(decoded.deploy_mode, GlobalContractDeployMode::CodeHash);
1044 }
1045
1046 #[test]
1047 fn test_use_global_contract_action_roundtrip() {
1048 let hash = CryptoHash::hash(&[1, 2, 3, 4]);
1049 let action = UseGlobalContractAction {
1050 contract_identifier: GlobalContractIdentifier::CodeHash(hash),
1051 };
1052
1053 let bytes = borsh::to_vec(&action).unwrap();
1054 let decoded: UseGlobalContractAction = borsh::from_slice(&bytes).unwrap();
1055
1056 assert_eq!(
1057 decoded.contract_identifier,
1058 GlobalContractIdentifier::CodeHash(hash)
1059 );
1060 }
1061
1062 #[test]
1063 fn test_deterministic_state_init_roundtrip() {
1064 let hash = CryptoHash::hash(&[1, 2, 3, 4]);
1065 let mut data = BTreeMap::new();
1066 data.insert(b"key1".to_vec(), b"value1".to_vec());
1067 data.insert(b"key2".to_vec(), b"value2".to_vec());
1068
1069 let action = DeterministicStateInitAction {
1070 state_init: DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1071 code: GlobalContractIdentifier::CodeHash(hash),
1072 data: data.clone(),
1073 }),
1074 deposit: NearToken::from_near(5),
1075 };
1076
1077 let bytes = borsh::to_vec(&action).unwrap();
1078 let decoded: DeterministicStateInitAction = borsh::from_slice(&bytes).unwrap();
1079
1080 assert_eq!(decoded.deposit, NearToken::from_near(5));
1081 let DeterministicAccountStateInit::V1(v1) = decoded.state_init;
1082 assert_eq!(v1.code, GlobalContractIdentifier::CodeHash(hash));
1083 assert_eq!(v1.data, data);
1084 }
1085
1086 #[test]
1087 fn test_action_helper_constructors() {
1088 let code = vec![1, 2, 3];
1090 let action = Action::publish(code.clone(), PublishMode::Immutable);
1091 if let Action::DeployGlobalContract(inner) = action {
1092 assert_eq!(inner.code, code);
1093 assert_eq!(inner.deploy_mode, GlobalContractDeployMode::CodeHash);
1094 } else {
1095 panic!("Expected DeployGlobalContract");
1096 }
1097
1098 let action = Action::publish(code.clone(), PublishMode::Updatable);
1099 if let Action::DeployGlobalContract(inner) = action {
1100 assert_eq!(inner.deploy_mode, GlobalContractDeployMode::AccountId);
1101 } else {
1102 panic!("Expected DeployGlobalContract");
1103 }
1104
1105 let hash = CryptoHash::hash(&code);
1107 let action = Action::deploy_from_hash(hash);
1108 if let Action::UseGlobalContract(inner) = action {
1109 assert_eq!(
1110 inner.contract_identifier,
1111 GlobalContractIdentifier::CodeHash(hash)
1112 );
1113 } else {
1114 panic!("Expected UseGlobalContract");
1115 }
1116
1117 let account_id: AccountId = "publisher.near".parse().unwrap();
1119 let action = Action::deploy_from_account(account_id.clone());
1120 if let Action::UseGlobalContract(inner) = action {
1121 assert_eq!(
1122 inner.contract_identifier,
1123 GlobalContractIdentifier::AccountId(account_id)
1124 );
1125 } else {
1126 panic!("Expected UseGlobalContract");
1127 }
1128 }
1129
1130 #[test]
1131 fn test_derive_account_id_format() {
1132 let state_init = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1134 code: GlobalContractIdentifier::CodeHash(CryptoHash::default()),
1135 data: BTreeMap::new(),
1136 });
1137
1138 let account_id = state_init.derive_account_id();
1139 let account_str = account_id.as_str();
1140
1141 assert!(
1143 account_str.starts_with("0s"),
1144 "Derived account should start with '0s', got: {}",
1145 account_str
1146 );
1147
1148 assert_eq!(
1150 account_str.len(),
1151 42,
1152 "Derived account should be 42 chars, got: {}",
1153 account_str.len()
1154 );
1155
1156 let hex_part = &account_str[2..];
1158 assert!(
1159 hex_part
1160 .chars()
1161 .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
1162 "Hex part should be lowercase hex, got: {}",
1163 hex_part
1164 );
1165 }
1166
1167 #[test]
1168 fn test_derive_account_id_deterministic() {
1169 let state_init1 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1171 code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
1172 data: BTreeMap::new(),
1173 });
1174
1175 let state_init2 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1176 code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
1177 data: BTreeMap::new(),
1178 });
1179
1180 assert_eq!(
1181 state_init1.derive_account_id(),
1182 state_init2.derive_account_id(),
1183 "Same input should produce same account ID"
1184 );
1185 }
1186
1187 #[test]
1188 fn test_derive_account_id_different_inputs() {
1189 let state_init1 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1191 code: GlobalContractIdentifier::AccountId("publisher1.near".parse().unwrap()),
1192 data: BTreeMap::new(),
1193 });
1194
1195 let state_init2 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1196 code: GlobalContractIdentifier::AccountId("publisher2.near".parse().unwrap()),
1197 data: BTreeMap::new(),
1198 });
1199
1200 assert_ne!(
1201 state_init1.derive_account_id(),
1202 state_init2.derive_account_id(),
1203 "Different code references should produce different account IDs"
1204 );
1205 }
1206
1207 #[test]
1208 fn test_access_key_permission_discriminants() {
1209 let fc = AccessKeyPermission::FunctionCall(FunctionCallPermission {
1210 allowance: None,
1211 receiver_id: "test.near".parse().unwrap(),
1212 method_names: vec![],
1213 });
1214 let bytes = borsh::to_vec(&fc).unwrap();
1215 assert_eq!(bytes[0], 0, "FunctionCall should have discriminant 0");
1216
1217 let fa = AccessKeyPermission::FullAccess;
1218 let bytes = borsh::to_vec(&fa).unwrap();
1219 assert_eq!(bytes[0], 1, "FullAccess should have discriminant 1");
1220
1221 let gkfc = AccessKeyPermission::GasKeyFunctionCall(
1222 GasKeyInfo {
1223 balance: NearToken::from_near(1),
1224 num_nonces: 5,
1225 },
1226 FunctionCallPermission {
1227 allowance: None,
1228 receiver_id: "test.near".parse().unwrap(),
1229 method_names: vec![],
1230 },
1231 );
1232 let bytes = borsh::to_vec(&gkfc).unwrap();
1233 assert_eq!(bytes[0], 2, "GasKeyFunctionCall should have discriminant 2");
1234
1235 let gkfa = AccessKeyPermission::GasKeyFullAccess(GasKeyInfo {
1236 balance: NearToken::from_near(1),
1237 num_nonces: 5,
1238 });
1239 let bytes = borsh::to_vec(&gkfa).unwrap();
1240 assert_eq!(bytes[0], 3, "GasKeyFullAccess should have discriminant 3");
1241 }
1242
1243 #[test]
1244 fn test_derive_account_id_different_data() {
1245 let mut data = BTreeMap::new();
1247 data.insert(b"key".to_vec(), b"value".to_vec());
1248
1249 let state_init1 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1250 code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
1251 data: BTreeMap::new(),
1252 });
1253
1254 let state_init2 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1255 code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
1256 data,
1257 });
1258
1259 assert_ne!(
1260 state_init1.derive_account_id(),
1261 state_init2.derive_account_id(),
1262 "Different data should produce different account IDs"
1263 );
1264 }
1265
1266 #[test]
1271 fn test_deterministic_state_init_json_roundtrip() {
1272 let hash = CryptoHash::hash(&[1, 2, 3, 4]);
1274 let mut data = BTreeMap::new();
1275 data.insert(b"key1".to_vec(), b"value1".to_vec());
1276 data.insert(b"key2".to_vec(), b"value2".to_vec());
1277
1278 let state_init = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1279 code: GlobalContractIdentifier::CodeHash(hash),
1280 data: data.clone(),
1281 });
1282
1283 let json = serde_json::to_value(&state_init).unwrap();
1285
1286 assert!(
1288 json.get("V1").is_some(),
1289 "Expected externally-tagged 'V1' key, got: {json}"
1290 );
1291 let v1 = json.get("V1").unwrap();
1292 assert!(v1.get("code").is_some(), "Expected 'code' field in V1");
1293 assert!(v1.get("data").is_some(), "Expected 'data' field in V1");
1294
1295 let data_obj = v1.get("data").unwrap().as_object().unwrap();
1297 assert!(
1299 data_obj.contains_key("a2V5MQ=="),
1300 "Expected base64-encoded key 'a2V5MQ==', got keys: {:?}",
1301 data_obj.keys().collect::<Vec<_>>()
1302 );
1303
1304 let deserialized: DeterministicAccountStateInit = serde_json::from_value(json).unwrap();
1306 let DeterministicAccountStateInit::V1(v1_decoded) = deserialized;
1307 assert_eq!(v1_decoded.code, GlobalContractIdentifier::CodeHash(hash));
1308 assert_eq!(v1_decoded.data, data);
1309 }
1310
1311 #[test]
1312 fn test_global_contract_identifier_json_roundtrip() {
1313 let hash = CryptoHash::hash(&[1, 2, 3]);
1315 let id = GlobalContractIdentifier::CodeHash(hash);
1316 let json = serde_json::to_string(&id).unwrap();
1317 let decoded: GlobalContractIdentifier = serde_json::from_str(&json).unwrap();
1318 assert_eq!(decoded, id);
1319
1320 let account_id: AccountId = "test.near".parse().unwrap();
1322 let id = GlobalContractIdentifier::AccountId(account_id);
1323 let json = serde_json::to_string(&id).unwrap();
1324 let decoded: GlobalContractIdentifier = serde_json::from_str(&json).unwrap();
1325 assert_eq!(decoded, id);
1326 }
1327
1328 #[test]
1329 fn test_deterministic_state_init_action_json_roundtrip() {
1330 let action = DeterministicStateInitAction {
1331 state_init: DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1332 code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
1333 data: BTreeMap::new(),
1334 }),
1335 deposit: NearToken::from_near(5),
1336 };
1337
1338 let json = serde_json::to_string(&action).unwrap();
1339 let decoded: DeterministicStateInitAction = serde_json::from_str(&json).unwrap();
1340 assert_eq!(decoded, action);
1341 }
1342}