Skip to main content

near_kit/types/
action.rs

1//! Transaction action types.
2
3use 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
12/// NEP-461 prefix for delegate actions (meta-transactions).
13/// Value: 2^30 + 366 = 1073742190
14///
15/// This prefix is prepended to DelegateAction when serializing for signing,
16/// ensuring delegate action signatures are always distinguishable from
17/// regular transaction signatures.
18pub const DELEGATE_ACTION_PREFIX: u32 = 1_073_742_190;
19
20/// Gas key information.
21///
22/// Gas keys are access keys with a prepaid balance to pay for gas costs.
23#[derive(
24    Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
25)]
26pub struct GasKeyInfo {
27    /// Prepaid gas balance in yoctoNEAR.
28    pub balance: NearToken,
29    /// Number of nonces allocated for this gas key.
30    pub num_nonces: u16,
31}
32
33/// Access key permission.
34///
35/// IMPORTANT: Variant order matters for Borsh serialization!
36/// NEAR Protocol defines: 0 = FunctionCall, 1 = FullAccess,
37/// 2 = GasKeyFunctionCall, 3 = GasKeyFullAccess
38#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
39pub enum AccessKeyPermission {
40    /// Function call access with restrictions. (discriminant = 0)
41    FunctionCall(FunctionCallPermission),
42    /// Full access to the account. (discriminant = 1)
43    FullAccess,
44    /// Gas key with function call access. (discriminant = 2)
45    GasKeyFunctionCall(GasKeyInfo, FunctionCallPermission),
46    /// Gas key with full access. (discriminant = 3)
47    GasKeyFullAccess(GasKeyInfo),
48}
49
50impl AccessKeyPermission {
51    /// Create a function call permission.
52    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    /// Create a full access permission.
65    pub fn full_access() -> Self {
66        Self::FullAccess
67    }
68}
69
70/// Function call access key permission details.
71#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
72pub struct FunctionCallPermission {
73    /// Maximum amount this key can spend (None = unlimited).
74    pub allowance: Option<NearToken>,
75    /// Contract that can be called.
76    pub receiver_id: AccountId,
77    /// Methods that can be called (empty = all methods).
78    pub method_names: Vec<String>,
79}
80
81/// Access key attached to an account.
82#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
83pub struct AccessKey {
84    /// Nonce for replay protection.
85    pub nonce: u64,
86    /// Permission level.
87    pub permission: AccessKeyPermission,
88}
89
90impl AccessKey {
91    /// Create a full access key.
92    pub fn full_access() -> Self {
93        Self {
94            nonce: 0,
95            permission: AccessKeyPermission::FullAccess,
96        }
97    }
98
99    /// Create a function call access key.
100    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/// A transaction action.
113///
114/// IMPORTANT: Variant order matters for Borsh serialization!
115/// The discriminants match NEAR Protocol specification:
116/// 0 = CreateAccount, 1 = DeployContract, 2 = FunctionCall, 3 = Transfer,
117/// 4 = Stake, 5 = AddKey, 6 = DeleteKey, 7 = DeleteAccount, 8 = Delegate,
118/// 9 = DeployGlobalContract, 10 = UseGlobalContract, 11 = DeterministicStateInit,
119/// 12 = TransferToGasKey, 13 = WithdrawFromGasKey
120#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
121pub enum Action {
122    /// Create a new account. (discriminant = 0)
123    CreateAccount(CreateAccountAction),
124    /// Deploy contract code. (discriminant = 1)
125    DeployContract(DeployContractAction),
126    /// Call a contract function. (discriminant = 2)
127    FunctionCall(FunctionCallAction),
128    /// Transfer NEAR tokens. (discriminant = 3)
129    Transfer(TransferAction),
130    /// Stake NEAR for validation. (discriminant = 4)
131    Stake(StakeAction),
132    /// Add an access key. (discriminant = 5)
133    AddKey(AddKeyAction),
134    /// Delete an access key. (discriminant = 6)
135    DeleteKey(DeleteKeyAction),
136    /// Delete the account. (discriminant = 7)
137    DeleteAccount(DeleteAccountAction),
138    /// Delegate action (for meta-transactions). (discriminant = 8)
139    Delegate(Box<SignedDelegateAction>),
140    /// Publish a contract to global registry. (discriminant = 9)
141    DeployGlobalContract(DeployGlobalContractAction),
142    /// Deploy from a previously published global contract. (discriminant = 10)
143    UseGlobalContract(UseGlobalContractAction),
144    /// NEP-616: Deploy with deterministically derived account ID. (discriminant = 11)
145    DeterministicStateInit(DeterministicStateInitAction),
146    /// Transfer NEAR to a gas key. (discriminant = 12)
147    TransferToGasKey(TransferToGasKeyAction),
148    /// Withdraw NEAR from a gas key. (discriminant = 13)
149    WithdrawFromGasKey(WithdrawFromGasKeyAction),
150}
151
152/// Create a new account.
153#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
154pub struct CreateAccountAction;
155
156/// Deploy contract code.
157#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
158pub struct DeployContractAction {
159    /// WASM code to deploy.
160    pub code: Vec<u8>,
161}
162
163/// Call a contract function.
164#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
165pub struct FunctionCallAction {
166    /// Method name to call.
167    pub method_name: String,
168    /// Arguments (JSON or Borsh encoded).
169    pub args: Vec<u8>,
170    /// Gas to attach.
171    pub gas: Gas,
172    /// NEAR tokens to attach.
173    pub deposit: NearToken,
174}
175
176/// Transfer NEAR tokens.
177#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
178pub struct TransferAction {
179    /// Amount to transfer.
180    pub deposit: NearToken,
181}
182
183/// Stake NEAR for validation.
184#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
185pub struct StakeAction {
186    /// Amount to stake.
187    pub stake: NearToken,
188    /// Validator public key.
189    pub public_key: PublicKey,
190}
191
192/// Add an access key.
193#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
194pub struct AddKeyAction {
195    /// Public key to add.
196    pub public_key: PublicKey,
197    /// Access key details.
198    pub access_key: AccessKey,
199}
200
201/// Delete an access key.
202#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
203pub struct DeleteKeyAction {
204    /// Public key to delete.
205    pub public_key: PublicKey,
206}
207
208/// Delete the account.
209#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
210pub struct DeleteAccountAction {
211    /// Account to receive remaining balance.
212    pub beneficiary_id: AccountId,
213}
214
215// ============================================================================
216// Global Contract Actions
217// ============================================================================
218
219/// How a global contract is identified in the registry.
220///
221/// Global contracts can be referenced either by their code hash (immutable)
222/// or by the account that published them (updatable).
223#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
224pub enum GlobalContractIdentifier {
225    /// Reference by code hash (32-byte SHA-256 hash of the WASM code).
226    /// This creates an immutable reference - the contract cannot be updated.
227    CodeHash(CryptoHash),
228    /// Reference by the account ID that published the contract.
229    /// The publisher can update the contract, and all users will get the new version.
230    AccountId(AccountId),
231}
232
233/// Deploy mode for global contracts.
234///
235/// Determines how the contract will be identified in the global registry.
236#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
237#[repr(u8)]
238pub enum GlobalContractDeployMode {
239    /// Contract is identified by its code hash (immutable).
240    /// Other accounts reference it by the hash.
241    CodeHash,
242    /// Contract is identified by the signer's account ID (updatable).
243    /// The signer can update the contract later.
244    AccountId,
245}
246
247/// Publish a contract to the global registry.
248///
249/// Global contracts are deployed once and can be referenced by multiple accounts,
250/// saving storage costs. The contract can be identified either by its code hash
251/// (immutable) or by the publishing account (updatable).
252#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
253pub struct DeployGlobalContractAction {
254    /// The WASM code to publish.
255    pub code: Vec<u8>,
256    /// How the contract will be identified.
257    pub deploy_mode: GlobalContractDeployMode,
258}
259
260/// Deploy a contract from the global registry.
261///
262/// Instead of uploading the WASM code, this action references a previously
263/// published contract in the global registry.
264#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
265pub struct UseGlobalContractAction {
266    /// Reference to the published contract.
267    pub contract_identifier: GlobalContractIdentifier,
268}
269
270// ============================================================================
271// NEP-616 Deterministic Account Actions
272// ============================================================================
273
274/// State initialization data for NEP-616 deterministic accounts.
275///
276/// The account ID is derived from: `"0s" + hex(keccak256(borsh(state_init))[12..32])`
277#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
278#[repr(u8)]
279pub enum DeterministicAccountStateInit {
280    /// Version 1 of the state init format.
281    V1(DeterministicAccountStateInitV1),
282}
283
284/// Version 1 of deterministic account state initialization.
285#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
286pub struct DeterministicAccountStateInitV1 {
287    /// Reference to the contract code (from global registry).
288    pub code: GlobalContractIdentifier,
289    /// Initial key-value pairs to populate in the contract's storage.
290    /// Keys and values are Borsh-serialized bytes.
291    pub data: BTreeMap<Vec<u8>, Vec<u8>>,
292}
293
294/// Deploy a contract with a deterministically derived account ID (NEP-616).
295///
296/// This enables creating accounts where the account ID is derived from the
297/// contract code and initial state, making them predictable and reproducible.
298#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
299pub struct DeterministicStateInitAction {
300    /// The state initialization data.
301    pub state_init: DeterministicAccountStateInit,
302    /// Amount to attach for storage costs.
303    pub deposit: NearToken,
304}
305
306impl DeterministicAccountStateInit {
307    /// Derive the deterministic account ID from this state init.
308    ///
309    /// The account ID is derived as: `"0s" + hex(keccak256(borsh(state_init))[12..32])`
310    ///
311    /// This produces a 42-character account ID that:
312    /// - Starts with "0s" prefix (distinguishes from Ethereum implicit accounts "0x")
313    /// - Followed by 40 hex characters (20 bytes from the keccak256 hash)
314    ///
315    /// # Example
316    ///
317    /// ```rust
318    /// use near_kit::types::{DeterministicAccountStateInit, DeterministicAccountStateInitV1, GlobalContractIdentifier, CryptoHash};
319    /// use std::collections::BTreeMap;
320    ///
321    /// let state_init = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
322    ///     code: GlobalContractIdentifier::CodeHash(CryptoHash::default()),
323    ///     data: BTreeMap::new(),
324    /// });
325    ///
326    /// let account_id = state_init.derive_account_id();
327    /// assert!(account_id.as_str().starts_with("0s"));
328    /// assert_eq!(account_id.as_str().len(), 42);
329    /// ```
330    pub fn derive_account_id(&self) -> AccountId {
331        // Borsh-serialize the state init
332        let serialized = borsh::to_vec(self).expect("StateInit serialization should not fail");
333
334        // Compute keccak256 hash
335        let hash = Keccak256::digest(&serialized);
336
337        // Take last 20 bytes (indices 12-32) of the hash
338        let suffix = &hash[12..32];
339
340        // Format as "0s" + hex
341        let account_str = format!("0s{}", hex::encode(suffix));
342
343        // This is a valid deterministic account ID by construction
344        AccountId::new_unchecked(&account_str)
345    }
346}
347
348impl DeterministicStateInitAction {
349    /// Derive the deterministic account ID for this action.
350    ///
351    /// Convenience method that delegates to `DeterministicAccountStateInit::derive_account_id`.
352    pub fn derive_account_id(&self) -> AccountId {
353        self.state_init.derive_account_id()
354    }
355}
356
357/// Transfer NEAR to a gas key.
358#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
359pub struct TransferToGasKeyAction {
360    /// Public key of the gas key to fund.
361    pub public_key: PublicKey,
362    /// Amount of NEAR to transfer.
363    pub deposit: NearToken,
364}
365
366/// Withdraw NEAR from a gas key.
367#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
368pub struct WithdrawFromGasKeyAction {
369    /// Public key of the gas key to withdraw from.
370    pub public_key: PublicKey,
371    /// Amount of NEAR to withdraw.
372    pub amount: NearToken,
373}
374
375/// Delegate action for meta-transactions.
376#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
377pub struct DelegateAction {
378    /// Sender of the delegate action.
379    pub sender_id: AccountId,
380    /// Receiver of the delegate action.
381    pub receiver_id: AccountId,
382    /// Actions to delegate.
383    pub actions: Vec<NonDelegateAction>,
384    /// Nonce for replay protection.
385    pub nonce: u64,
386    /// Maximum block height for the action.
387    pub max_block_height: u64,
388    /// Public key authorizing the delegate.
389    pub public_key: PublicKey,
390}
391
392/// Signed delegate action.
393#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
394pub struct SignedDelegateAction {
395    /// The delegate action.
396    pub delegate_action: DelegateAction,
397    /// Signature over the delegate action.
398    pub signature: super::Signature,
399}
400
401/// Non-delegate action (for use within DelegateAction).
402///
403/// This is a newtype wrapper around Action that ensures the wrapped action
404/// is not a Delegate variant, since delegate actions cannot contain nested
405/// delegate actions.
406///
407/// The newtype wrapper serializes identically to the inner Action, preserving
408/// Borsh compatibility with near-primitives.
409#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
410pub struct NonDelegateAction(Action);
411
412// Helper constructors for actions
413impl Action {
414    /// Create a CreateAccount action.
415    pub fn create_account() -> Self {
416        Self::CreateAccount(CreateAccountAction)
417    }
418
419    /// Create a DeployContract action.
420    pub fn deploy_contract(code: Vec<u8>) -> Self {
421        Self::DeployContract(DeployContractAction { code })
422    }
423
424    /// Create a FunctionCall action.
425    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    /// Create a Transfer action.
440    pub fn transfer(deposit: NearToken) -> Self {
441        Self::Transfer(TransferAction { deposit })
442    }
443
444    /// Create a Stake action.
445    pub fn stake(stake: NearToken, public_key: PublicKey) -> Self {
446        Self::Stake(StakeAction { stake, public_key })
447    }
448
449    /// Create an AddKey action for full access.
450    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    /// Create an AddKey action for function call access.
458    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    /// Create a DeleteKey action.
471    pub fn delete_key(public_key: PublicKey) -> Self {
472        Self::DeleteKey(DeleteKeyAction { public_key })
473    }
474
475    /// Create a DeleteAccount action.
476    pub fn delete_account(beneficiary_id: AccountId) -> Self {
477        Self::DeleteAccount(DeleteAccountAction { beneficiary_id })
478    }
479
480    /// Create a Delegate action from a signed delegate action.
481    pub fn delegate(signed_delegate: SignedDelegateAction) -> Self {
482        Self::Delegate(Box::new(signed_delegate))
483    }
484
485    /// Publish a contract to the global registry.
486    ///
487    /// Global contracts are deployed once and can be referenced by multiple accounts,
488    /// saving storage costs.
489    ///
490    /// # Arguments
491    ///
492    /// * `code` - The WASM code to publish
493    /// * `by_hash` - If true, contract is identified by its code hash (immutable).
494    ///   If false (default), contract is identified by the signer's account ID (updatable).
495    ///
496    /// # Example
497    ///
498    /// ```rust,ignore
499    /// // Publish updatable contract (identified by your account)
500    /// near.transaction("alice.near")
501    ///     .publish_contract(wasm_code, false)
502    ///     .send()
503    ///     .await?;
504    ///
505    /// // Publish immutable contract (identified by its hash)
506    /// near.transaction("alice.near")
507    ///     .publish_contract(wasm_code, true)
508    ///     .send()
509    ///     .await?;
510    /// ```
511    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    /// Deploy a contract from the global registry by code hash.
523    ///
524    /// References a previously published immutable contract.
525    pub fn deploy_from_hash(code_hash: CryptoHash) -> Self {
526        Self::UseGlobalContract(UseGlobalContractAction {
527            contract_identifier: GlobalContractIdentifier::CodeHash(code_hash),
528        })
529    }
530
531    /// Deploy a contract from the global registry by account ID.
532    ///
533    /// References a contract published by the given account.
534    /// The contract can be updated by the publisher.
535    pub fn deploy_from_account(account_id: AccountId) -> Self {
536        Self::UseGlobalContract(UseGlobalContractAction {
537            contract_identifier: GlobalContractIdentifier::AccountId(account_id),
538        })
539    }
540
541    /// Create a NEP-616 deterministic state init action.
542    ///
543    /// The account ID is derived from the state init data:
544    /// `"0s" + hex(keccak256(borsh(state_init))[12..32])`
545    pub fn state_init(state_init: DeterministicAccountStateInit, deposit: NearToken) -> Self {
546        Self::DeterministicStateInit(DeterministicStateInitAction {
547            state_init,
548            deposit,
549        })
550    }
551
552    /// Create a NEP-616 deterministic state init action with code hash reference.
553    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    /// Create a NEP-616 deterministic state init action with account reference.
568    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    /// Transfer NEAR to a gas key.
583    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    /// Withdraw NEAR from a gas key.
591    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    /// Serialize the delegate action for signing.
598    ///
599    /// Per NEP-461, this prepends a u32 prefix (2^30 + 366) before the delegate action,
600    /// ensuring signed delegate actions are never identical to signed transactions.
601    ///
602    /// # Example
603    ///
604    /// ```rust,ignore
605    /// let bytes = delegate_action.serialize_for_signing();
606    /// let hash = CryptoHash::hash(&bytes);
607    /// let signature = signer.sign(hash.as_bytes()).await?;
608    /// ```
609    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    /// Get the hash of this delegate action (for signing).
621    pub fn get_hash(&self) -> CryptoHash {
622        let bytes = self.serialize_for_signing();
623        CryptoHash::hash(&bytes)
624    }
625
626    /// Sign this delegate action and return a SignedDelegateAction.
627    pub fn sign(self, signature: Signature) -> SignedDelegateAction {
628        SignedDelegateAction {
629            delegate_action: self,
630            signature,
631        }
632    }
633}
634
635impl SignedDelegateAction {
636    /// Encode the signed delegate action to bytes for transport.
637    pub fn to_bytes(&self) -> Vec<u8> {
638        borsh::to_vec(self).expect("signed delegate action serialization should never fail")
639    }
640
641    /// Encode the signed delegate action to base64 for transport.
642    ///
643    /// This is the most common format for sending delegate actions via HTTP/JSON.
644    pub fn to_base64(&self) -> String {
645        STANDARD.encode(self.to_bytes())
646    }
647
648    /// Decode a signed delegate action from bytes.
649    pub fn from_bytes(bytes: &[u8]) -> Result<Self, borsh::io::Error> {
650        borsh::from_slice(bytes)
651    }
652
653    /// Decode a signed delegate action from base64.
654    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    /// Get the sender account ID.
660    pub fn sender_id(&self) -> &AccountId {
661        &self.delegate_action.sender_id
662    }
663
664    /// Get the receiver account ID.
665    pub fn receiver_id(&self) -> &AccountId {
666        &self.delegate_action.receiver_id
667    }
668}
669
670/// Error decoding a signed delegate action.
671#[derive(Debug, thiserror::Error)]
672pub enum DecodeError {
673    /// Base64 decoding failed.
674    #[error("base64 decode error: {0}")]
675    Base64(#[from] base64::DecodeError),
676    /// Borsh deserialization failed.
677    #[error("borsh decode error: {0}")]
678    Borsh(#[from] borsh::io::Error),
679}
680
681impl NonDelegateAction {
682    /// Convert from an Action, returning None if it's a Delegate action.
683    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    /// Get a reference to the inner action.
692    pub fn inner(&self) -> &Action {
693        &self.0
694    }
695
696    /// Consume self and return the inner action.
697    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        // NEP-461: prefix = 2^30 + 366
746        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        // First 4 bytes should be the NEP-461 prefix in little-endian
756        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        // Rest should be borsh-serialized DelegateAction
761        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        // Hash should be SHA-256 of serialize_for_signing bytes
772        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        // Roundtrip through bytes
786        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        // Roundtrip through base64
810        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        // Transfer should convert
835        let transfer = Action::Transfer(TransferAction {
836            deposit: NearToken::from_near(1),
837        });
838        assert!(NonDelegateAction::from_action(transfer).is_some());
839
840        // FunctionCall should convert
841        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        // Delegate should NOT convert (returns None)
850        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        // Test that DecodeError has proper Display impl
862        let base64_err = DecodeError::Base64(base64::DecodeError::InvalidLength(5));
863        assert!(format!("{}", base64_err).contains("base64"));
864
865        // Borsh error is harder to construct, but we tested the variant exists
866    }
867
868    // ========================================================================
869    // Global Contract Action Tests
870    // ========================================================================
871
872    #[test]
873    fn test_action_discriminants() {
874        // Verify action discriminants match NEAR protocol specification
875        // 0 = CreateAccount, 1 = DeployContract, 2 = FunctionCall, 3 = Transfer,
876        // 4 = Stake, 5 = AddKey, 6 = DeleteKey, 7 = DeleteAccount, 8 = Delegate,
877        // 9 = DeployGlobalContract, 10 = UseGlobalContract, 11 = DeterministicStateInit,
878        // 12 = TransferToGasKey, 13 = WithdrawFromGasKey
879
880        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        // DeployGlobalContract (discriminant = 9)
893        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        // UseGlobalContract (discriminant = 10)
901        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        // DeterministicStateInit (discriminant = 11)
910        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        // TransferToGasKey (discriminant = 12)
919        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        // WithdrawFromGasKey (discriminant = 13)
927        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        // Verify deploy mode serialization
938        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        // Verify identifier serialization
950        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]; // WASM magic bytes
975        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        // Test publish_contract
1030        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        // Test deploy_from_hash
1047        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        // Test deploy_from_account
1059        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        // Test that derived account ID has the correct format
1074        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        // Should start with "0s"
1083        assert!(
1084            account_str.starts_with("0s"),
1085            "Derived account should start with '0s', got: {}",
1086            account_str
1087        );
1088
1089        // Should be exactly 42 characters: "0s" + 40 hex chars
1090        assert_eq!(
1091            account_str.len(),
1092            42,
1093            "Derived account should be 42 chars, got: {}",
1094            account_str.len()
1095        );
1096
1097        // Everything after "0s" should be valid lowercase hex
1098        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        // Same input should produce same output
1111        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        // Different code references should produce different account IDs
1131        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        // Different data should produce different account IDs
1187        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}