1use crate::hashing::HashingBehavior;
2use crate::utils::decode_encode_muxed_account::encode_muxed_account_to_address;
3use std::collections::hash_map::ValuesMut;
4use std::error::Error;
5use std::fmt;
6use std::str::FromStr;
7use stellar_strkey::ed25519::PublicKey;
8use stellar_xdr::curr::LedgerKey;
9use xdr::DecoratedSignature;
10use xdr::Limits;
11use xdr::SorobanTransactionData;
12
13use crate::account::Account;
14use crate::hashing::Sha256Hasher;
15use crate::keypair::Keypair;
16use crate::keypair::KeypairBehavior;
17use crate::xdr;
18use crate::xdr::ReadXdr;
19use crate::xdr::WriteXdr;
20
21#[derive(Debug, Clone)]
22pub struct Transaction {
23 pub network_passphrase: String,
26 pub signatures: Vec<DecoratedSignature>,
27 pub fee: u32,
28 pub envelope_type: xdr::EnvelopeType,
29 pub memo: Option<xdr::Memo>,
30 pub sequence: Option<String>,
31 pub source: Option<String>,
32 pub time_bounds: Option<xdr::TimeBounds>,
33 pub ledger_bounds: Option<xdr::LedgerBounds>,
34 pub min_account_sequence: Option<String>,
35 pub min_account_sequence_age: Option<u32>,
36 pub min_account_sequence_ledger_gap: Option<u32>,
37 pub extra_signers: Option<Vec<xdr::AccountId>>,
38 pub operations: Option<Vec<xdr::Operation>>,
39 pub hash: Option<[u8; 32]>,
40 pub soroban_data: Option<SorobanTransactionData>,
41}
42
43pub trait TransactionBehavior {
45 fn signature_base(&self) -> Vec<u8>;
46 fn hash(&self) -> [u8; 32];
47 fn sign(&mut self, keypairs: &[Keypair]);
48 fn to_envelope(&self) -> Result<xdr::TransactionEnvelope, Box<dyn Error>>;
49 fn from_xdr_envelope(xdr: &str, network: &str) -> Self;
50 }
52
53impl Transaction {
54 fn to_tx(&self) -> xdr::Transaction {
55 match self.envelope_type {
56 xdr::EnvelopeType::TxV0 => xdr::Transaction {
57 source_account: xdr::MuxedAccount::from_str(
58 &self.source.clone().expect("No account"),
59 )
60 .expect("Invalid account"),
61 fee: self.fee,
62 seq_num: xdr::SequenceNumber(
63 self.sequence
64 .clone()
65 .expect("No sequence number")
66 .parse::<i64>()
67 .expect("Invalid sequence number"),
68 ),
69 cond: match &self.time_bounds {
70 None => xdr::Preconditions::None,
71 Some(time_bounds) => xdr::Preconditions::Time(time_bounds.clone()),
72 },
73 memo: self.memo.clone().unwrap_or(xdr::Memo::None),
74 operations: self
75 .operations
76 .clone()
77 .unwrap_or_default()
78 .try_into()
79 .expect("Invalid operations"),
80 ext: xdr::TransactionExt::V0,
81 },
82 xdr::EnvelopeType::Tx => xdr::Transaction {
83 source_account: xdr::MuxedAccount::from_str(
84 &self.source.clone().expect("No account"),
85 )
86 .expect("Invalid account"),
87 fee: self.fee,
88 seq_num: xdr::SequenceNumber(
89 self.sequence
90 .clone()
91 .expect("No sequence number")
92 .parse()
93 .expect("Invalid sequence number"),
94 ),
95 cond: match &self.time_bounds {
96 None => xdr::Preconditions::None,
97 Some(time_bounds) => xdr::Preconditions::Time(time_bounds.clone()),
98 },
99 memo: self.memo.clone().unwrap_or(xdr::Memo::None),
100 operations: self
101 .operations
102 .clone()
103 .unwrap_or_default()
104 .try_into()
105 .expect("Invalid operations"),
106 ext: if let Some(data) = self.soroban_data.clone() {
107 xdr::TransactionExt::V1(data)
108 } else {
109 xdr::TransactionExt::V0
110 },
111 },
112 _ => panic!("Transaction must have either tx or tx_v0 set"),
113 }
114 }
115}
116
117impl TransactionBehavior for Transaction {
118 fn signature_base(&self) -> Vec<u8> {
119 let tagged_tx = xdr::TransactionSignaturePayloadTaggedTransaction::Tx(self.to_tx());
120 let tx_sig = xdr::TransactionSignaturePayload {
121 network_id: xdr::Hash(Sha256Hasher::hash(self.network_passphrase.as_bytes())),
122 tagged_transaction: tagged_tx,
123 };
124
125 tx_sig.to_xdr(Limits::none()).unwrap()
126 }
127
128 fn hash(&self) -> [u8; 32] {
129 Sha256Hasher::hash(self.signature_base())
130 }
131
132 fn sign(&mut self, keypairs: &[Keypair]) {
133 let tx_hash: [u8; 32] = self.hash();
134 for kp in keypairs {
135 let sig = kp.sign_decorated(&tx_hash);
136 self.signatures.push(sig);
137 }
138
139 self.hash = Some(tx_hash);
140 }
141
142 fn to_envelope(&self) -> Result<xdr::TransactionEnvelope, Box<dyn Error>> {
143 let raw_tx = self.to_tx().to_xdr_base64(xdr::Limits::none()).unwrap();
144
145 let mut signatures =
146 xdr::VecM::<DecoratedSignature, 20>::try_from(self.signatures.clone()).unwrap(); let envelope = match self.envelope_type {
149 xdr::EnvelopeType::TxV0 => {
150 let transaction_v0 = xdr::TransactionV0Envelope {
151 tx: xdr::TransactionV0::from_xdr_base64(&raw_tx, xdr::Limits::none()).unwrap(), signatures,
153 };
154 xdr::TransactionEnvelope::TxV0(transaction_v0)
155 }
156
157 xdr::EnvelopeType::Tx => {
158 let transaction_v1 = xdr::TransactionV1Envelope {
159 tx: xdr::Transaction::from_xdr_base64(&raw_tx, xdr::Limits::none()).unwrap(), signatures,
161 };
162 xdr::TransactionEnvelope::Tx(transaction_v1)
163 }
164 _ => {
165 return Err(format!(
166 "Invalid TransactionEnvelope: expected an envelopeTypeTxV0 or envelopeTypeTx but received an {:?}.",
167 self.envelope_type
168 )
169 .into());
170 }
171 };
172
173 Ok(envelope)
174 }
175
176 fn from_xdr_envelope(xdr: &str, network: &str) -> Self {
177 let tx_env = xdr::TransactionEnvelope::from_xdr_base64(xdr, Limits::none()).unwrap();
178 let envelope_type = tx_env.discriminant();
179
180 match tx_env {
181 xdr::TransactionEnvelope::TxV0(tx_v0_env) => Self {
182 network_passphrase: network.to_owned(),
185 signatures: tx_v0_env.signatures.to_vec(),
186 fee: tx_v0_env.tx.fee,
187 envelope_type,
188 memo: Some(tx_v0_env.tx.memo),
189 sequence: Some(tx_v0_env.tx.seq_num.0.to_string()),
190 source: Some(
191 stellar_strkey::Strkey::PublicKeyEd25519(PublicKey(
192 tx_v0_env.tx.source_account_ed25519.0,
193 ))
194 .to_string(),
195 ),
196 time_bounds: tx_v0_env.tx.time_bounds,
197 ledger_bounds: None,
198 min_account_sequence: None,
199 min_account_sequence_age: None,
200 min_account_sequence_ledger_gap: None,
201 extra_signers: None,
202 operations: Some(tx_v0_env.tx.operations.to_vec()),
203 hash: None,
204 soroban_data: None,
205 },
206 xdr::TransactionEnvelope::Tx(tx_env) => {
207 let mut time_bounds = None;
208 let mut ledger_bounds = None;
209 let mut min_account_sequence = None;
210 let mut min_account_sequence_age = None;
211 let mut min_account_sequence_ledger_gap = None;
212 let mut extra_signers = None;
213
214 match tx_env.tx.cond.clone() {
215 xdr::Preconditions::Time(tb) => {
216 time_bounds = Some(tb);
217 }
218 xdr::Preconditions::V2(v2) => {
219 time_bounds = v2.time_bounds;
220 ledger_bounds = v2.ledger_bounds;
221 min_account_sequence = v2
222 .min_seq_num
223 .map(|seq| seq.to_xdr_base64(Limits::none()).unwrap());
224 min_account_sequence_age = Some(v2.min_seq_age);
225 min_account_sequence_ledger_gap = Some(v2.min_seq_ledger_gap);
226 extra_signers = Some(v2.extra_signers.to_vec());
227 }
228 xdr::Preconditions::None => {}
229 }
230
231 Self {
232 network_passphrase: network.to_owned(),
235 signatures: tx_env.signatures.to_vec(),
236 fee: tx_env.tx.fee,
237 envelope_type,
238 memo: Some(tx_env.tx.memo),
239 sequence: Some(tx_env.tx.seq_num.0.to_string()),
240 source: Some(encode_muxed_account_to_address(&tx_env.tx.source_account)),
241 time_bounds,
242 ledger_bounds,
243 min_account_sequence,
244 min_account_sequence_age: None,
245 min_account_sequence_ledger_gap,
246 extra_signers: None,
247 operations: Some(tx_env.tx.operations.to_vec()),
248 hash: None,
249 soroban_data: None,
250 }
251 }
252 _ => panic!("Invalid envelope type"),
253 }
254 }
255}
256
257impl fmt::Display for Transaction {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 writeln!(f, "Transaction {{")?;
260
261 writeln!(f, " Network: {}", self.network_passphrase)?;
263
264 if let Some(source) = &self.source {
266 writeln!(f, " Source Account: {}", source)?;
267 }
268
269 writeln!(f, " Fee: {}", self.fee)?;
271
272 if let Some(sequence) = &self.sequence {
274 writeln!(f, " Sequence Number: {}", sequence)?;
275 }
276
277 if let Some(memo) = &self.memo {
279 write!(f, " Memo: ")?;
280 match memo {
281 xdr::Memo::Text(text) => writeln!(f, "TEXT: {:?}", text)?,
282 xdr::Memo::Id(id) => writeln!(f, "ID: {}", id)?,
283 xdr::Memo::Hash(hash) => writeln!(f, "HASH: {:?}", hash)?,
284 xdr::Memo::Return(ret) => writeln!(f, "RETURN: {:?}", ret)?,
285 xdr::Memo::None => writeln!(f, "NONE")?,
286 }
287 }
288
289 if let Some(time_bounds) = &self.time_bounds {
291 writeln!(f, " Time Bounds: {{")?;
292 writeln!(f, " Min Time: {:?}", time_bounds.min_time)?;
293 writeln!(f, " Max Time: {:?}", time_bounds.max_time)?;
294 writeln!(f, " }}")?;
295 }
296
297 if let Some(ledger_bounds) = &self.ledger_bounds {
299 writeln!(f, " Ledger Bounds: {{")?;
300 writeln!(f, " Min Ledger: {}", ledger_bounds.min_ledger)?;
301 writeln!(f, " Max Ledger: {}", ledger_bounds.max_ledger)?;
302 writeln!(f, " }}")?;
303 }
304
305 if let Some(min_seq) = &self.min_account_sequence {
307 writeln!(f, " Min Account Sequence: {}", min_seq)?;
308 }
309
310 if let Some(age) = &self.min_account_sequence_age {
312 writeln!(f, " Min Account Sequence Age: {}", age)?;
313 }
314
315 if let Some(gap) = &self.min_account_sequence_ledger_gap {
317 writeln!(f, " Min Account Sequence Ledger Gap: {}", gap)?;
318 }
319
320 if let Some(operations) = &self.operations {
322 writeln!(f, " Operations: [")?;
323 for (i, op) in operations.iter().enumerate() {
324 writeln!(f, " {}. {:?}", i + 1, op)?;
325 }
326 writeln!(f, " ]")?;
327 }
328
329 writeln!(f, " Signatures: [")?;
331 for (i, sig) in self.signatures.iter().enumerate() {
332 writeln!(
333 f,
334 " {}. Hint: {:?}, Signature: {:?}",
335 i + 1,
336 sig.hint,
337 sig.signature
338 )?;
339 }
340 writeln!(f, " ]")?;
341
342 if let Some(hash) = &self.hash {
344 writeln!(f, " Hash: {:?}", hash)?;
345 }
346
347 if let Some(soroban_data) = &self.soroban_data {
349 writeln!(f, " Soroban Data: {:?}", soroban_data)?;
350 }
351
352 write!(f, "}}")
353 }
354}
355
356#[cfg(test)]
357mod tests {
358
359 use core::panic;
360 use keypair::KeypairBehavior;
361 use std::{cell::RefCell, rc::Rc};
362
363 use sha2::digest::crypto_common::Key;
364 use xdr::Limits;
365
366 use super::*;
367 use crate::{
368 account::{Account, AccountBehavior},
369 asset::{Asset, AssetBehavior},
370 keypair::{self, Keypair},
371 network::{NetworkPassphrase, Networks},
372 operation::{self, Operation},
373 transaction::TransactionBehavior,
374 transaction_builder::{TransactionBuilder, TransactionBuilderBehavior, TIMEOUT_INFINITE},
375 };
376
377 #[test]
378 fn constructs_transaction_object_from_transaction_envelope() {
379 let source = Rc::new(RefCell::new(
380 Account::new(
381 "GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB",
382 "20",
383 )
384 .unwrap(),
385 ));
386
387 let destination = "GAAOFCNYV2OQUMVONXH2DOOQNNLJO7WRQ7E4INEZ7VH7JNG7IKBQAK5D";
388 let asset = Asset::native();
389 let amount = 2000 * operation::ONE;
390
391 let mut builder = TransactionBuilder::new(source.clone(), Networks::testnet(), None)
392 .fee(100_u32)
393 .add_operation(
394 Operation::new()
395 .payment(destination, &asset, amount)
396 .unwrap(),
397 )
398 .add_memo("Happy birthday!")
399 .set_timeout(TIMEOUT_INFINITE)
400 .unwrap()
401 .build();
402
403 let destination = "GDJJRRMBK4IWLEPJGIE6SXD2LP7REGZODU7WDC3I2D6MR37F4XSHBKX2";
406 let signer = Keypair::master(Some(Networks::testnet())).unwrap();
407 let mut tx = TransactionBuilder::new(source, Networks::testnet(), None)
408 .fee(100_u32)
409 .add_operation(
410 Operation::new()
411 .create_account(destination, 10 * operation::ONE)
412 .unwrap(),
413 )
414 .build();
415
416 tx.sign(&[signer.clone()]);
417 let sig = &tx.signatures[0].signature.0;
418 let verified = signer.verify(&tx.hash(), sig);
419 assert!(verified);
420 }
421
422 #[test]
423 fn can_successfully_decode_envelope() {
424 let xdr = "AAAAAPQQv+uPYrlCDnjgPyPRgIjB6T8Zb8ANmL8YGAXC2IAgAAAAZAAIteYAAAAHAAAAAAAAAAAAAAABAAAAAAAAAAMAAAAAAAAAAUVVUgAAAAAAUtYuFczBLlsXyEp3q8BbTBpEGINWahqkFbnTPd93YUUAAAAXSHboAAAAABEAACcQAAAAAAAAAKIAAAAAAAAAAcLYgCAAAABAo2tU6n0Bb7bbbpaXacVeaTVbxNMBtnrrXVk2QAOje2Flllk/ORlmQdFU/9c8z43eWh1RNMpI3PscY+yDCnJPBQ==";
426
427 let tx_env = xdr::TransactionEnvelope::from_xdr_base64(xdr, Limits::none()).unwrap();
429
430 let tx = match tx_env {
431 xdr::TransactionEnvelope::TxV0(transaction_v0_envelope) => transaction_v0_envelope.tx,
432 _ => panic!("fff"),
433 };
434
435 let source_account = tx.source_account_ed25519;
436 assert_eq!(source_account.0.len(), 32);
437 }
438
439 #[test]
440 fn calculates_correct_hash_with_non_utf8_strings() {
441 let xdr = "AAAAAAtjwtJadppTmm0NtAU99BFxXXfzPO1N/SqR43Z8aXqXAAAAZAAIj6YAAAACAAAAAAAAAAEAAAAB0QAAAAAAAAEAAAAAAAAAAQAAAADLa6390PDAqg3qDLpshQxS+uVw3ytSgKRirQcInPWt1QAAAAAAAAAAA1Z+AAAAAAAAAAABfGl6lwAAAEBC655+8Izq54MIZrXTVF/E1ycHgQWpVcBD+LFkuOjjJd995u/7wM8sFqQqambL0/ME2FTOtxMO65B9i3eAIu4P";
442 let tx = Transaction::from_xdr_envelope(xdr, Networks::public());
443
444 println!("Transaction {}", tx);
445 assert_eq!(
446 hex::encode(tx.hash()),
447 "a84d534b3742ad89413bdbf259e02fa4c5d039123769e9bcc63616f723a2bcd5"
448 );
449 }
450}