Skip to main content

near_kit/types/
transaction.rs

1//! Transaction types.
2
3use borsh::{BorshDeserialize, BorshSerialize};
4
5use super::{AccountId, Action, CryptoHash, PublicKey, SecretKey, Signature};
6
7/// An unsigned transaction.
8#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
9pub struct Transaction {
10    /// The account that signs and pays for the transaction.
11    pub signer_id: AccountId,
12    /// The public key of the signer.
13    pub public_key: PublicKey,
14    /// Nonce for replay protection (must be greater than previous nonce).
15    pub nonce: u64,
16    /// The account that receives the transaction.
17    pub receiver_id: AccountId,
18    /// A recent block hash for transaction validity.
19    pub block_hash: CryptoHash,
20    /// The actions to execute.
21    pub actions: Vec<Action>,
22}
23
24impl Transaction {
25    /// Create a new transaction.
26    pub fn new(
27        signer_id: AccountId,
28        public_key: PublicKey,
29        nonce: u64,
30        receiver_id: AccountId,
31        block_hash: CryptoHash,
32        actions: Vec<Action>,
33    ) -> Self {
34        Self {
35            signer_id,
36            public_key,
37            nonce,
38            receiver_id,
39            block_hash,
40            actions,
41        }
42    }
43
44    /// Get the hash of this transaction (for signing).
45    pub fn get_hash(&self) -> CryptoHash {
46        let bytes = borsh::to_vec(self).expect("transaction serialization should never fail");
47        CryptoHash::hash(&bytes)
48    }
49
50    /// Get the raw bytes of this transaction (for signing).
51    pub fn get_hash_and_size(&self) -> (CryptoHash, usize) {
52        let bytes = borsh::to_vec(self).expect("transaction serialization should never fail");
53        (CryptoHash::hash(&bytes), bytes.len())
54    }
55
56    /// Sign this transaction with a secret key.
57    pub fn sign(self, signer: &SecretKey) -> SignedTransaction {
58        let hash = self.get_hash();
59        let signature = signer.sign(hash.as_bytes());
60        SignedTransaction {
61            transaction: self,
62            signature,
63        }
64    }
65}
66
67/// A signed transaction ready to be sent.
68#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
69pub struct SignedTransaction {
70    /// The unsigned transaction.
71    pub transaction: Transaction,
72    /// The signature.
73    pub signature: Signature,
74}
75
76impl SignedTransaction {
77    /// Get the hash of the signed transaction (transaction hash).
78    pub fn get_hash(&self) -> CryptoHash {
79        self.transaction.get_hash()
80    }
81
82    /// Serialize to bytes for RPC submission.
83    pub fn to_bytes(&self) -> Vec<u8> {
84        borsh::to_vec(self).expect("signed transaction serialization should never fail")
85    }
86
87    /// Serialize to base64 for RPC submission.
88    pub fn to_base64(&self) -> String {
89        use base64::{Engine as _, engine::general_purpose::STANDARD};
90        STANDARD.encode(self.to_bytes())
91    }
92
93    /// Deserialize from bytes.
94    ///
95    /// Use this to reconstruct a signed transaction that was serialized with [`to_bytes`](Self::to_bytes).
96    ///
97    /// # Example
98    ///
99    /// ```rust,ignore
100    /// use near_kit::SignedTransaction;
101    /// let bytes: Vec<u8> = /* received from offline signer */;
102    /// let signed_tx = SignedTransaction::from_bytes(&bytes)?;
103    /// ```
104    pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::error::Error> {
105        borsh::from_slice(bytes).map_err(|e| {
106            crate::error::Error::InvalidTransaction(format!(
107                "Failed to deserialize signed transaction: {}",
108                e
109            ))
110        })
111    }
112
113    /// Deserialize from base64.
114    ///
115    /// Use this to reconstruct a signed transaction that was serialized with [`to_base64`](Self::to_base64).
116    ///
117    /// # Example
118    ///
119    /// ```rust,no_run
120    /// # use near_kit::SignedTransaction;
121    /// let base64_str = "AgAAAGFsaWNlLnRlc3RuZXQ...";
122    /// let signed_tx = SignedTransaction::from_base64(base64_str)?;
123    /// # Ok::<(), near_kit::Error>(())
124    /// ```
125    pub fn from_base64(s: &str) -> Result<Self, crate::error::Error> {
126        use base64::{Engine as _, engine::general_purpose::STANDARD};
127        let bytes = STANDARD.decode(s).map_err(|e| {
128            crate::error::Error::InvalidTransaction(format!("Invalid base64: {}", e))
129        })?;
130        Self::from_bytes(&bytes)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_transaction_hash() {
140        let secret = SecretKey::generate_ed25519();
141        let public = secret.public_key();
142
143        let tx = Transaction::new(
144            "alice.testnet".parse().unwrap(),
145            public,
146            1,
147            "bob.testnet".parse().unwrap(),
148            CryptoHash::ZERO,
149            vec![],
150        );
151
152        let hash = tx.get_hash();
153        assert!(!hash.is_zero());
154    }
155
156    #[test]
157    fn test_sign_transaction() {
158        let secret = SecretKey::generate_ed25519();
159        let public = secret.public_key();
160
161        let tx = Transaction::new(
162            "alice.testnet".parse().unwrap(),
163            public,
164            1,
165            "bob.testnet".parse().unwrap(),
166            CryptoHash::ZERO,
167            vec![],
168        );
169
170        let signed = tx.sign(&secret);
171        assert!(!signed.to_bytes().is_empty());
172    }
173}