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    /// Complete this transaction with an externally-produced signature.
67    ///
68    /// Use this for hardware wallet, MPC, or HSM signing workflows where you
69    /// sign the transaction hash externally and then reconstruct the signed transaction.
70    ///
71    /// # Example
72    ///
73    /// ```rust,no_run
74    /// # use near_kit::*;
75    /// # fn example(tx: Transaction, sig_bytes: [u8; 64]) {
76    /// let hash = tx.get_hash();
77    /// // sign hash externally...
78    /// let signature = Signature::ed25519_from_bytes(sig_bytes);
79    /// let signed = tx.complete(signature);
80    /// # }
81    /// ```
82    pub fn complete(self, signature: Signature) -> SignedTransaction {
83        SignedTransaction {
84            transaction: self,
85            signature,
86        }
87    }
88}
89
90/// A signed transaction ready to be sent.
91#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
92pub struct SignedTransaction {
93    /// The unsigned transaction.
94    pub transaction: Transaction,
95    /// The signature.
96    pub signature: Signature,
97}
98
99impl SignedTransaction {
100    /// Get the hash of the signed transaction (transaction hash).
101    pub fn get_hash(&self) -> CryptoHash {
102        self.transaction.get_hash()
103    }
104
105    /// Serialize to bytes for RPC submission.
106    pub fn to_bytes(&self) -> Vec<u8> {
107        borsh::to_vec(self).expect("signed transaction serialization should never fail")
108    }
109
110    /// Serialize to base64 for RPC submission.
111    pub fn to_base64(&self) -> String {
112        use base64::{Engine as _, engine::general_purpose::STANDARD};
113        STANDARD.encode(self.to_bytes())
114    }
115
116    /// Deserialize from bytes.
117    ///
118    /// Use this to reconstruct a signed transaction that was serialized with [`to_bytes`](Self::to_bytes).
119    ///
120    /// # Example
121    ///
122    /// ```rust,ignore
123    /// use near_kit::SignedTransaction;
124    /// let bytes: Vec<u8> = /* received from offline signer */;
125    /// let signed_tx = SignedTransaction::from_bytes(&bytes)?;
126    /// ```
127    pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::error::Error> {
128        borsh::from_slice(bytes).map_err(|e| {
129            crate::error::Error::InvalidTransaction(format!(
130                "Failed to deserialize signed transaction: {}",
131                e
132            ))
133        })
134    }
135
136    /// Deserialize from base64.
137    ///
138    /// Use this to reconstruct a signed transaction that was serialized with [`to_base64`](Self::to_base64).
139    ///
140    /// # Example
141    ///
142    /// ```rust,no_run
143    /// # use near_kit::SignedTransaction;
144    /// let base64_str = "AgAAAGFsaWNlLnRlc3RuZXQ...";
145    /// let signed_tx = SignedTransaction::from_base64(base64_str)?;
146    /// # Ok::<(), near_kit::Error>(())
147    /// ```
148    pub fn from_base64(s: &str) -> Result<Self, crate::error::Error> {
149        use base64::{Engine as _, engine::general_purpose::STANDARD};
150        let bytes = STANDARD.decode(s).map_err(|e| {
151            crate::error::Error::InvalidTransaction(format!("Invalid base64: {}", e))
152        })?;
153        Self::from_bytes(&bytes)
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_transaction_hash() {
163        let secret = SecretKey::generate_ed25519();
164        let public = secret.public_key();
165
166        let tx = Transaction::new(
167            "alice.testnet".parse().unwrap(),
168            public,
169            1,
170            "bob.testnet".parse().unwrap(),
171            CryptoHash::ZERO,
172            vec![],
173        );
174
175        let hash = tx.get_hash();
176        assert!(!hash.is_zero());
177    }
178
179    #[test]
180    fn test_sign_transaction() {
181        let secret = SecretKey::generate_ed25519();
182        let public = secret.public_key();
183
184        let tx = Transaction::new(
185            "alice.testnet".parse().unwrap(),
186            public,
187            1,
188            "bob.testnet".parse().unwrap(),
189            CryptoHash::ZERO,
190            vec![],
191        );
192
193        let signed = tx.sign(&secret);
194        assert!(!signed.to_bytes().is_empty());
195    }
196
197    #[test]
198    fn test_complete_matches_sign() {
199        let secret = SecretKey::generate_ed25519();
200        let public = secret.public_key();
201
202        let tx1 = Transaction::new(
203            "alice.testnet".parse().unwrap(),
204            public.clone(),
205            1,
206            "bob.testnet".parse().unwrap(),
207            CryptoHash::ZERO,
208            vec![],
209        );
210
211        let tx2 = tx1.clone();
212
213        // Sign via the normal path
214        let signed_normal = tx1.sign(&secret);
215
216        // Sign manually via complete()
217        let hash = tx2.get_hash();
218        let signature = secret.sign(hash.as_bytes());
219        let signed_complete = tx2.complete(signature);
220
221        assert_eq!(signed_normal.get_hash(), signed_complete.get_hash());
222        assert_eq!(signed_normal.signature, signed_complete.signature);
223        assert_eq!(signed_normal.to_bytes(), signed_complete.to_bytes());
224    }
225}