1use std::collections::BTreeMap;
4
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use borsh::{BorshDeserialize, BorshSerialize};
7use serde::{Deserialize, Serialize};
8use sha3::{Digest, Keccak256};
9
10use super::{AccountId, CryptoHash, Gas, NearToken, PublicKey, Signature};
11
12pub const DELEGATE_ACTION_PREFIX: u32 = 1_073_742_190;
19
20#[derive(
24 Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
25)]
26pub struct GasKeyInfo {
27 pub balance: NearToken,
29 pub num_nonces: u16,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
39pub enum AccessKeyPermission {
40 FunctionCall(FunctionCallPermission),
42 FullAccess,
44 GasKeyFunctionCall(GasKeyInfo, FunctionCallPermission),
46 GasKeyFullAccess(GasKeyInfo),
48}
49
50impl AccessKeyPermission {
51 pub fn function_call(
53 receiver_id: AccountId,
54 method_names: Vec<String>,
55 allowance: Option<NearToken>,
56 ) -> Self {
57 Self::FunctionCall(FunctionCallPermission {
58 allowance,
59 receiver_id,
60 method_names,
61 })
62 }
63
64 pub fn full_access() -> Self {
66 Self::FullAccess
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
72pub struct FunctionCallPermission {
73 pub allowance: Option<NearToken>,
75 pub receiver_id: AccountId,
77 pub method_names: Vec<String>,
79}
80
81#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
83pub struct AccessKey {
84 pub nonce: u64,
86 pub permission: AccessKeyPermission,
88}
89
90impl AccessKey {
91 pub fn full_access() -> Self {
93 Self {
94 nonce: 0,
95 permission: AccessKeyPermission::FullAccess,
96 }
97 }
98
99 pub fn function_call(
101 receiver_id: AccountId,
102 method_names: Vec<String>,
103 allowance: Option<NearToken>,
104 ) -> Self {
105 Self {
106 nonce: 0,
107 permission: AccessKeyPermission::function_call(receiver_id, method_names, allowance),
108 }
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
121pub enum Action {
122 CreateAccount(CreateAccountAction),
124 DeployContract(DeployContractAction),
126 FunctionCall(FunctionCallAction),
128 Transfer(TransferAction),
130 Stake(StakeAction),
132 AddKey(AddKeyAction),
134 DeleteKey(DeleteKeyAction),
136 DeleteAccount(DeleteAccountAction),
138 Delegate(Box<SignedDelegateAction>),
140 DeployGlobalContract(DeployGlobalContractAction),
142 UseGlobalContract(UseGlobalContractAction),
144 DeterministicStateInit(DeterministicStateInitAction),
146 TransferToGasKey(TransferToGasKeyAction),
148 WithdrawFromGasKey(WithdrawFromGasKeyAction),
150}
151
152#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
154pub struct CreateAccountAction;
155
156#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
158pub struct DeployContractAction {
159 pub code: Vec<u8>,
161}
162
163#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
165pub struct FunctionCallAction {
166 pub method_name: String,
168 pub args: Vec<u8>,
170 pub gas: Gas,
172 pub deposit: NearToken,
174}
175
176#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
178pub struct TransferAction {
179 pub deposit: NearToken,
181}
182
183#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
185pub struct StakeAction {
186 pub stake: NearToken,
188 pub public_key: PublicKey,
190}
191
192#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
194pub struct AddKeyAction {
195 pub public_key: PublicKey,
197 pub access_key: AccessKey,
199}
200
201#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
203pub struct DeleteKeyAction {
204 pub public_key: PublicKey,
206}
207
208#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
210pub struct DeleteAccountAction {
211 pub beneficiary_id: AccountId,
213}
214
215#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
224pub enum GlobalContractIdentifier {
225 CodeHash(CryptoHash),
228 AccountId(AccountId),
231}
232
233#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
237#[repr(u8)]
238pub enum GlobalContractDeployMode {
239 CodeHash,
242 AccountId,
245}
246
247#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
253pub struct DeployGlobalContractAction {
254 pub code: Vec<u8>,
256 pub deploy_mode: GlobalContractDeployMode,
258}
259
260#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
265pub struct UseGlobalContractAction {
266 pub contract_identifier: GlobalContractIdentifier,
268}
269
270#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
278#[repr(u8)]
279pub enum DeterministicAccountStateInit {
280 V1(DeterministicAccountStateInitV1),
282}
283
284#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
286pub struct DeterministicAccountStateInitV1 {
287 pub code: GlobalContractIdentifier,
289 pub data: BTreeMap<Vec<u8>, Vec<u8>>,
292}
293
294#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
299pub struct DeterministicStateInitAction {
300 pub state_init: DeterministicAccountStateInit,
302 pub deposit: NearToken,
304}
305
306impl DeterministicAccountStateInit {
307 pub fn derive_account_id(&self) -> AccountId {
331 let serialized = borsh::to_vec(self).expect("StateInit serialization should not fail");
333
334 let hash = Keccak256::digest(&serialized);
336
337 let suffix = &hash[12..32];
339
340 let account_str = format!("0s{}", hex::encode(suffix));
342
343 AccountId::new_unchecked(&account_str)
345 }
346}
347
348impl DeterministicStateInitAction {
349 pub fn derive_account_id(&self) -> AccountId {
353 self.state_init.derive_account_id()
354 }
355}
356
357#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
359pub struct TransferToGasKeyAction {
360 pub public_key: PublicKey,
362 pub deposit: NearToken,
364}
365
366#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
368pub struct WithdrawFromGasKeyAction {
369 pub public_key: PublicKey,
371 pub amount: NearToken,
373}
374
375#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
377pub struct DelegateAction {
378 pub sender_id: AccountId,
380 pub receiver_id: AccountId,
382 pub actions: Vec<NonDelegateAction>,
384 pub nonce: u64,
386 pub max_block_height: u64,
388 pub public_key: PublicKey,
390}
391
392#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
394pub struct SignedDelegateAction {
395 pub delegate_action: DelegateAction,
397 pub signature: super::Signature,
399}
400
401#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
410pub struct NonDelegateAction(Action);
411
412impl Action {
414 pub fn create_account() -> Self {
416 Self::CreateAccount(CreateAccountAction)
417 }
418
419 pub fn deploy_contract(code: Vec<u8>) -> Self {
421 Self::DeployContract(DeployContractAction { code })
422 }
423
424 pub fn function_call(
426 method_name: impl Into<String>,
427 args: Vec<u8>,
428 gas: Gas,
429 deposit: NearToken,
430 ) -> Self {
431 Self::FunctionCall(FunctionCallAction {
432 method_name: method_name.into(),
433 args,
434 gas,
435 deposit,
436 })
437 }
438
439 pub fn transfer(deposit: NearToken) -> Self {
441 Self::Transfer(TransferAction { deposit })
442 }
443
444 pub fn stake(stake: NearToken, public_key: PublicKey) -> Self {
446 Self::Stake(StakeAction { stake, public_key })
447 }
448
449 pub fn add_full_access_key(public_key: PublicKey) -> Self {
451 Self::AddKey(AddKeyAction {
452 public_key,
453 access_key: AccessKey::full_access(),
454 })
455 }
456
457 pub fn add_function_call_key(
459 public_key: PublicKey,
460 receiver_id: AccountId,
461 method_names: Vec<String>,
462 allowance: Option<NearToken>,
463 ) -> Self {
464 Self::AddKey(AddKeyAction {
465 public_key,
466 access_key: AccessKey::function_call(receiver_id, method_names, allowance),
467 })
468 }
469
470 pub fn delete_key(public_key: PublicKey) -> Self {
472 Self::DeleteKey(DeleteKeyAction { public_key })
473 }
474
475 pub fn delete_account(beneficiary_id: AccountId) -> Self {
477 Self::DeleteAccount(DeleteAccountAction { beneficiary_id })
478 }
479
480 pub fn delegate(signed_delegate: SignedDelegateAction) -> Self {
482 Self::Delegate(Box::new(signed_delegate))
483 }
484
485 pub fn publish_contract(code: Vec<u8>, by_hash: bool) -> Self {
512 Self::DeployGlobalContract(DeployGlobalContractAction {
513 code,
514 deploy_mode: if by_hash {
515 GlobalContractDeployMode::CodeHash
516 } else {
517 GlobalContractDeployMode::AccountId
518 },
519 })
520 }
521
522 pub fn deploy_from_hash(code_hash: CryptoHash) -> Self {
526 Self::UseGlobalContract(UseGlobalContractAction {
527 contract_identifier: GlobalContractIdentifier::CodeHash(code_hash),
528 })
529 }
530
531 pub fn deploy_from_account(account_id: AccountId) -> Self {
536 Self::UseGlobalContract(UseGlobalContractAction {
537 contract_identifier: GlobalContractIdentifier::AccountId(account_id),
538 })
539 }
540
541 pub fn state_init(state_init: DeterministicAccountStateInit, deposit: NearToken) -> Self {
546 Self::DeterministicStateInit(DeterministicStateInitAction {
547 state_init,
548 deposit,
549 })
550 }
551
552 pub fn state_init_by_hash(
554 code_hash: CryptoHash,
555 data: BTreeMap<Vec<u8>, Vec<u8>>,
556 deposit: NearToken,
557 ) -> Self {
558 Self::DeterministicStateInit(DeterministicStateInitAction {
559 state_init: DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
560 code: GlobalContractIdentifier::CodeHash(code_hash),
561 data,
562 }),
563 deposit,
564 })
565 }
566
567 pub fn state_init_by_account(
569 account_id: AccountId,
570 data: BTreeMap<Vec<u8>, Vec<u8>>,
571 deposit: NearToken,
572 ) -> Self {
573 Self::DeterministicStateInit(DeterministicStateInitAction {
574 state_init: DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
575 code: GlobalContractIdentifier::AccountId(account_id),
576 data,
577 }),
578 deposit,
579 })
580 }
581
582 pub fn transfer_to_gas_key(public_key: PublicKey, deposit: NearToken) -> Self {
584 Self::TransferToGasKey(TransferToGasKeyAction {
585 public_key,
586 deposit,
587 })
588 }
589
590 pub fn withdraw_from_gas_key(public_key: PublicKey, amount: NearToken) -> Self {
592 Self::WithdrawFromGasKey(WithdrawFromGasKeyAction { public_key, amount })
593 }
594}
595
596impl DelegateAction {
597 pub fn serialize_for_signing(&self) -> Vec<u8> {
610 let prefix_bytes = DELEGATE_ACTION_PREFIX.to_le_bytes();
611 let action_bytes =
612 borsh::to_vec(self).expect("delegate action serialization should never fail");
613
614 let mut result = Vec::with_capacity(prefix_bytes.len() + action_bytes.len());
615 result.extend_from_slice(&prefix_bytes);
616 result.extend_from_slice(&action_bytes);
617 result
618 }
619
620 pub fn get_hash(&self) -> CryptoHash {
622 let bytes = self.serialize_for_signing();
623 CryptoHash::hash(&bytes)
624 }
625
626 pub fn sign(self, signature: Signature) -> SignedDelegateAction {
628 SignedDelegateAction {
629 delegate_action: self,
630 signature,
631 }
632 }
633}
634
635impl SignedDelegateAction {
636 pub fn to_bytes(&self) -> Vec<u8> {
638 borsh::to_vec(self).expect("signed delegate action serialization should never fail")
639 }
640
641 pub fn to_base64(&self) -> String {
645 STANDARD.encode(self.to_bytes())
646 }
647
648 pub fn from_bytes(bytes: &[u8]) -> Result<Self, borsh::io::Error> {
650 borsh::from_slice(bytes)
651 }
652
653 pub fn from_base64(s: &str) -> Result<Self, DecodeError> {
655 let bytes = STANDARD.decode(s).map_err(DecodeError::Base64)?;
656 Self::from_bytes(&bytes).map_err(DecodeError::Borsh)
657 }
658
659 pub fn sender_id(&self) -> &AccountId {
661 &self.delegate_action.sender_id
662 }
663
664 pub fn receiver_id(&self) -> &AccountId {
666 &self.delegate_action.receiver_id
667 }
668}
669
670#[derive(Debug, thiserror::Error)]
672pub enum DecodeError {
673 #[error("base64 decode error: {0}")]
675 Base64(#[from] base64::DecodeError),
676 #[error("borsh decode error: {0}")]
678 Borsh(#[from] borsh::io::Error),
679}
680
681impl NonDelegateAction {
682 pub fn from_action(action: Action) -> Option<Self> {
684 if matches!(action, Action::Delegate(_)) {
685 None
686 } else {
687 Some(Self(action))
688 }
689 }
690
691 pub fn inner(&self) -> &Action {
693 &self.0
694 }
695
696 pub fn into_inner(self) -> Action {
698 self.0
699 }
700}
701
702impl From<NonDelegateAction> for Action {
703 fn from(action: NonDelegateAction) -> Self {
704 action.0
705 }
706}
707
708impl TryFrom<Action> for NonDelegateAction {
709 type Error = ();
710
711 fn try_from(action: Action) -> Result<Self, Self::Error> {
712 Self::from_action(action).ok_or(())
713 }
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719 use crate::types::{Gas, NearToken, SecretKey};
720
721 fn create_test_delegate_action() -> DelegateAction {
722 let sender_id: AccountId = "alice.testnet".parse().unwrap();
723 let receiver_id: AccountId = "bob.testnet".parse().unwrap();
724 let public_key: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
725 .parse()
726 .unwrap();
727
728 DelegateAction {
729 sender_id,
730 receiver_id,
731 actions: vec![
732 NonDelegateAction::from_action(Action::Transfer(TransferAction {
733 deposit: NearToken::from_near(1),
734 }))
735 .unwrap(),
736 ],
737 nonce: 1,
738 max_block_height: 1000,
739 public_key,
740 }
741 }
742
743 #[test]
744 fn test_delegate_action_prefix() {
745 assert_eq!(DELEGATE_ACTION_PREFIX, 1073742190);
747 assert_eq!(DELEGATE_ACTION_PREFIX, (1 << 30) + 366);
748 }
749
750 #[test]
751 fn test_delegate_action_serialize_for_signing() {
752 let delegate_action = create_test_delegate_action();
753 let bytes = delegate_action.serialize_for_signing();
754
755 let prefix_bytes = &bytes[0..4];
757 let prefix = u32::from_le_bytes(prefix_bytes.try_into().unwrap());
758 assert_eq!(prefix, DELEGATE_ACTION_PREFIX);
759
760 let action_bytes = &bytes[4..];
762 let expected_action_bytes = borsh::to_vec(&delegate_action).unwrap();
763 assert_eq!(action_bytes, expected_action_bytes.as_slice());
764 }
765
766 #[test]
767 fn test_delegate_action_get_hash() {
768 let delegate_action = create_test_delegate_action();
769 let hash = delegate_action.get_hash();
770
771 let bytes = delegate_action.serialize_for_signing();
773 let expected_hash = CryptoHash::hash(&bytes);
774 assert_eq!(hash, expected_hash);
775 }
776
777 #[test]
778 fn test_signed_delegate_action_roundtrip_bytes() {
779 let delegate_action = create_test_delegate_action();
780 let secret_key = SecretKey::generate_ed25519();
781 let hash = delegate_action.get_hash();
782 let signature = secret_key.sign(hash.as_bytes());
783 let signed = delegate_action.sign(signature);
784
785 let bytes = signed.to_bytes();
787 let decoded = SignedDelegateAction::from_bytes(&bytes).unwrap();
788
789 assert_eq!(decoded.sender_id().as_str(), signed.sender_id().as_str());
790 assert_eq!(
791 decoded.receiver_id().as_str(),
792 signed.receiver_id().as_str()
793 );
794 assert_eq!(decoded.delegate_action.nonce, signed.delegate_action.nonce);
795 assert_eq!(
796 decoded.delegate_action.max_block_height,
797 signed.delegate_action.max_block_height
798 );
799 }
800
801 #[test]
802 fn test_signed_delegate_action_roundtrip_base64() {
803 let delegate_action = create_test_delegate_action();
804 let secret_key = SecretKey::generate_ed25519();
805 let hash = delegate_action.get_hash();
806 let signature = secret_key.sign(hash.as_bytes());
807 let signed = delegate_action.sign(signature);
808
809 let base64 = signed.to_base64();
811 let decoded = SignedDelegateAction::from_base64(&base64).unwrap();
812
813 assert_eq!(decoded.sender_id().as_str(), signed.sender_id().as_str());
814 assert_eq!(
815 decoded.receiver_id().as_str(),
816 signed.receiver_id().as_str()
817 );
818 }
819
820 #[test]
821 fn test_signed_delegate_action_accessors() {
822 let delegate_action = create_test_delegate_action();
823 let secret_key = SecretKey::generate_ed25519();
824 let hash = delegate_action.get_hash();
825 let signature = secret_key.sign(hash.as_bytes());
826 let signed = delegate_action.sign(signature);
827
828 assert_eq!(signed.sender_id().as_str(), "alice.testnet");
829 assert_eq!(signed.receiver_id().as_str(), "bob.testnet");
830 }
831
832 #[test]
833 fn test_non_delegate_action_from_action() {
834 let transfer = Action::Transfer(TransferAction {
836 deposit: NearToken::from_near(1),
837 });
838 assert!(NonDelegateAction::from_action(transfer).is_some());
839
840 let call = Action::FunctionCall(FunctionCallAction {
842 method_name: "test".to_string(),
843 args: vec![],
844 gas: Gas::default(),
845 deposit: NearToken::ZERO,
846 });
847 assert!(NonDelegateAction::from_action(call).is_some());
848
849 let delegate_action = create_test_delegate_action();
851 let secret_key = SecretKey::generate_ed25519();
852 let hash = delegate_action.get_hash();
853 let signature = secret_key.sign(hash.as_bytes());
854 let signed = delegate_action.sign(signature);
855 let delegate = Action::delegate(signed);
856 assert!(NonDelegateAction::from_action(delegate).is_none());
857 }
858
859 #[test]
860 fn test_decode_error_display() {
861 let base64_err = DecodeError::Base64(base64::DecodeError::InvalidLength(5));
863 assert!(format!("{}", base64_err).contains("base64"));
864
865 }
867
868 #[test]
873 fn test_action_discriminants() {
874 let create_account = Action::create_account();
881 let bytes = borsh::to_vec(&create_account).unwrap();
882 assert_eq!(bytes[0], 0, "CreateAccount should have discriminant 0");
883
884 let deploy = Action::deploy_contract(vec![1, 2, 3]);
885 let bytes = borsh::to_vec(&deploy).unwrap();
886 assert_eq!(bytes[0], 1, "DeployContract should have discriminant 1");
887
888 let transfer = Action::transfer(NearToken::from_near(1));
889 let bytes = borsh::to_vec(&transfer).unwrap();
890 assert_eq!(bytes[0], 3, "Transfer should have discriminant 3");
891
892 let publish = Action::publish_contract(vec![1, 2, 3], false);
894 let bytes = borsh::to_vec(&publish).unwrap();
895 assert_eq!(
896 bytes[0], 9,
897 "DeployGlobalContract should have discriminant 9"
898 );
899
900 let code_hash = CryptoHash::hash(&[1, 2, 3]);
902 let use_global = Action::deploy_from_hash(code_hash);
903 let bytes = borsh::to_vec(&use_global).unwrap();
904 assert_eq!(
905 bytes[0], 10,
906 "UseGlobalContract should have discriminant 10"
907 );
908
909 let state_init =
911 Action::state_init_by_hash(code_hash, BTreeMap::new(), NearToken::from_near(1));
912 let bytes = borsh::to_vec(&state_init).unwrap();
913 assert_eq!(
914 bytes[0], 11,
915 "DeterministicStateInit should have discriminant 11"
916 );
917
918 let pk: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
920 .parse()
921 .unwrap();
922 let transfer_gas = Action::transfer_to_gas_key(pk.clone(), NearToken::from_near(1));
923 let bytes = borsh::to_vec(&transfer_gas).unwrap();
924 assert_eq!(bytes[0], 12, "TransferToGasKey should have discriminant 12");
925
926 let withdraw_gas = Action::withdraw_from_gas_key(pk, NearToken::from_near(1));
928 let bytes = borsh::to_vec(&withdraw_gas).unwrap();
929 assert_eq!(
930 bytes[0], 13,
931 "WithdrawFromGasKey should have discriminant 13"
932 );
933 }
934
935 #[test]
936 fn test_global_contract_deploy_mode_serialization() {
937 let by_hash = GlobalContractDeployMode::CodeHash;
939 let bytes = borsh::to_vec(&by_hash).unwrap();
940 assert_eq!(bytes, vec![0], "CodeHash mode should serialize to 0");
941
942 let by_account = GlobalContractDeployMode::AccountId;
943 let bytes = borsh::to_vec(&by_account).unwrap();
944 assert_eq!(bytes, vec![1], "AccountId mode should serialize to 1");
945 }
946
947 #[test]
948 fn test_global_contract_identifier_serialization() {
949 let hash = CryptoHash::hash(&[1, 2, 3]);
951 let by_hash = GlobalContractIdentifier::CodeHash(hash);
952 let bytes = borsh::to_vec(&by_hash).unwrap();
953 assert_eq!(
954 bytes[0], 0,
955 "CodeHash identifier should have discriminant 0"
956 );
957 assert_eq!(
958 bytes.len(),
959 1 + 32,
960 "Should be 1 byte discriminant + 32 byte hash"
961 );
962
963 let account_id: AccountId = "test.near".parse().unwrap();
964 let by_account = GlobalContractIdentifier::AccountId(account_id);
965 let bytes = borsh::to_vec(&by_account).unwrap();
966 assert_eq!(
967 bytes[0], 1,
968 "AccountId identifier should have discriminant 1"
969 );
970 }
971
972 #[test]
973 fn test_deploy_global_contract_action_roundtrip() {
974 let code = vec![0, 97, 115, 109]; let action = DeployGlobalContractAction {
976 code: code.clone(),
977 deploy_mode: GlobalContractDeployMode::CodeHash,
978 };
979
980 let bytes = borsh::to_vec(&action).unwrap();
981 let decoded: DeployGlobalContractAction = borsh::from_slice(&bytes).unwrap();
982
983 assert_eq!(decoded.code, code);
984 assert_eq!(decoded.deploy_mode, GlobalContractDeployMode::CodeHash);
985 }
986
987 #[test]
988 fn test_use_global_contract_action_roundtrip() {
989 let hash = CryptoHash::hash(&[1, 2, 3, 4]);
990 let action = UseGlobalContractAction {
991 contract_identifier: GlobalContractIdentifier::CodeHash(hash),
992 };
993
994 let bytes = borsh::to_vec(&action).unwrap();
995 let decoded: UseGlobalContractAction = borsh::from_slice(&bytes).unwrap();
996
997 assert_eq!(
998 decoded.contract_identifier,
999 GlobalContractIdentifier::CodeHash(hash)
1000 );
1001 }
1002
1003 #[test]
1004 fn test_deterministic_state_init_roundtrip() {
1005 let hash = CryptoHash::hash(&[1, 2, 3, 4]);
1006 let mut data = BTreeMap::new();
1007 data.insert(b"key1".to_vec(), b"value1".to_vec());
1008 data.insert(b"key2".to_vec(), b"value2".to_vec());
1009
1010 let action = DeterministicStateInitAction {
1011 state_init: DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1012 code: GlobalContractIdentifier::CodeHash(hash),
1013 data: data.clone(),
1014 }),
1015 deposit: NearToken::from_near(5),
1016 };
1017
1018 let bytes = borsh::to_vec(&action).unwrap();
1019 let decoded: DeterministicStateInitAction = borsh::from_slice(&bytes).unwrap();
1020
1021 assert_eq!(decoded.deposit, NearToken::from_near(5));
1022 let DeterministicAccountStateInit::V1(v1) = decoded.state_init;
1023 assert_eq!(v1.code, GlobalContractIdentifier::CodeHash(hash));
1024 assert_eq!(v1.data, data);
1025 }
1026
1027 #[test]
1028 fn test_action_helper_constructors() {
1029 let code = vec![1, 2, 3];
1031 let action = Action::publish_contract(code.clone(), true);
1032 if let Action::DeployGlobalContract(inner) = action {
1033 assert_eq!(inner.code, code);
1034 assert_eq!(inner.deploy_mode, GlobalContractDeployMode::CodeHash);
1035 } else {
1036 panic!("Expected DeployGlobalContract");
1037 }
1038
1039 let action = Action::publish_contract(code.clone(), false);
1040 if let Action::DeployGlobalContract(inner) = action {
1041 assert_eq!(inner.deploy_mode, GlobalContractDeployMode::AccountId);
1042 } else {
1043 panic!("Expected DeployGlobalContract");
1044 }
1045
1046 let hash = CryptoHash::hash(&code);
1048 let action = Action::deploy_from_hash(hash);
1049 if let Action::UseGlobalContract(inner) = action {
1050 assert_eq!(
1051 inner.contract_identifier,
1052 GlobalContractIdentifier::CodeHash(hash)
1053 );
1054 } else {
1055 panic!("Expected UseGlobalContract");
1056 }
1057
1058 let account_id: AccountId = "publisher.near".parse().unwrap();
1060 let action = Action::deploy_from_account(account_id.clone());
1061 if let Action::UseGlobalContract(inner) = action {
1062 assert_eq!(
1063 inner.contract_identifier,
1064 GlobalContractIdentifier::AccountId(account_id)
1065 );
1066 } else {
1067 panic!("Expected UseGlobalContract");
1068 }
1069 }
1070
1071 #[test]
1072 fn test_derive_account_id_format() {
1073 let state_init = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1075 code: GlobalContractIdentifier::CodeHash(CryptoHash::default()),
1076 data: BTreeMap::new(),
1077 });
1078
1079 let account_id = state_init.derive_account_id();
1080 let account_str = account_id.as_str();
1081
1082 assert!(
1084 account_str.starts_with("0s"),
1085 "Derived account should start with '0s', got: {}",
1086 account_str
1087 );
1088
1089 assert_eq!(
1091 account_str.len(),
1092 42,
1093 "Derived account should be 42 chars, got: {}",
1094 account_str.len()
1095 );
1096
1097 let hex_part = &account_str[2..];
1099 assert!(
1100 hex_part
1101 .chars()
1102 .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
1103 "Hex part should be lowercase hex, got: {}",
1104 hex_part
1105 );
1106 }
1107
1108 #[test]
1109 fn test_derive_account_id_deterministic() {
1110 let state_init1 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1112 code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
1113 data: BTreeMap::new(),
1114 });
1115
1116 let state_init2 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1117 code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
1118 data: BTreeMap::new(),
1119 });
1120
1121 assert_eq!(
1122 state_init1.derive_account_id(),
1123 state_init2.derive_account_id(),
1124 "Same input should produce same account ID"
1125 );
1126 }
1127
1128 #[test]
1129 fn test_derive_account_id_different_inputs() {
1130 let state_init1 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1132 code: GlobalContractIdentifier::AccountId("publisher1.near".parse().unwrap()),
1133 data: BTreeMap::new(),
1134 });
1135
1136 let state_init2 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1137 code: GlobalContractIdentifier::AccountId("publisher2.near".parse().unwrap()),
1138 data: BTreeMap::new(),
1139 });
1140
1141 assert_ne!(
1142 state_init1.derive_account_id(),
1143 state_init2.derive_account_id(),
1144 "Different code references should produce different account IDs"
1145 );
1146 }
1147
1148 #[test]
1149 fn test_access_key_permission_discriminants() {
1150 let fc = AccessKeyPermission::FunctionCall(FunctionCallPermission {
1151 allowance: None,
1152 receiver_id: "test.near".parse().unwrap(),
1153 method_names: vec![],
1154 });
1155 let bytes = borsh::to_vec(&fc).unwrap();
1156 assert_eq!(bytes[0], 0, "FunctionCall should have discriminant 0");
1157
1158 let fa = AccessKeyPermission::FullAccess;
1159 let bytes = borsh::to_vec(&fa).unwrap();
1160 assert_eq!(bytes[0], 1, "FullAccess should have discriminant 1");
1161
1162 let gkfc = AccessKeyPermission::GasKeyFunctionCall(
1163 GasKeyInfo {
1164 balance: NearToken::from_near(1),
1165 num_nonces: 5,
1166 },
1167 FunctionCallPermission {
1168 allowance: None,
1169 receiver_id: "test.near".parse().unwrap(),
1170 method_names: vec![],
1171 },
1172 );
1173 let bytes = borsh::to_vec(&gkfc).unwrap();
1174 assert_eq!(bytes[0], 2, "GasKeyFunctionCall should have discriminant 2");
1175
1176 let gkfa = AccessKeyPermission::GasKeyFullAccess(GasKeyInfo {
1177 balance: NearToken::from_near(1),
1178 num_nonces: 5,
1179 });
1180 let bytes = borsh::to_vec(&gkfa).unwrap();
1181 assert_eq!(bytes[0], 3, "GasKeyFullAccess should have discriminant 3");
1182 }
1183
1184 #[test]
1185 fn test_derive_account_id_different_data() {
1186 let mut data = BTreeMap::new();
1188 data.insert(b"key".to_vec(), b"value".to_vec());
1189
1190 let state_init1 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1191 code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
1192 data: BTreeMap::new(),
1193 });
1194
1195 let state_init2 = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
1196 code: GlobalContractIdentifier::AccountId("publisher.near".parse().unwrap()),
1197 data,
1198 });
1199
1200 assert_ne!(
1201 state_init1.derive_account_id(),
1202 state_init2.derive_account_id(),
1203 "Different data should produce different account IDs"
1204 );
1205 }
1206}