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