stellar_baselib/
transaction.rs

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 tx: Option<xdr::Transaction>,
24    //pub tx_v0: Option<xdr::TransactionV0>,
25    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
43// Define a trait for Transaction behavior
44pub 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    //TODO: XDR Conversion, Proper From and To
51}
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(); // Make a copy of the signatures
147
148        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(), // Make a copy of tx
152                    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(), // Make a copy of tx
160                    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                //tx: None,
183                //tx_v0: Some(tx_v0_env.tx.clone()),
184                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                    //tx: Some(tx_env.clone().tx),
233                    //tx_v0: None,
234                    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        // Network information
262        writeln!(f, "  Network: {}", self.network_passphrase)?;
263
264        // Source account
265        if let Some(source) = &self.source {
266            writeln!(f, "  Source Account: {}", source)?;
267        }
268
269        // Fee
270        writeln!(f, "  Fee: {}", self.fee)?;
271
272        // Sequence number
273        if let Some(sequence) = &self.sequence {
274            writeln!(f, "  Sequence Number: {}", sequence)?;
275        }
276
277        // Memo
278        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        // Time bounds
290        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        // Ledger bounds
298        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        // Min account sequence
306        if let Some(min_seq) = &self.min_account_sequence {
307            writeln!(f, "  Min Account Sequence: {}", min_seq)?;
308        }
309
310        // Min account sequence age
311        if let Some(age) = &self.min_account_sequence_age {
312            writeln!(f, "  Min Account Sequence Age: {}", age)?;
313        }
314
315        // Min account sequence ledger gap
316        if let Some(gap) = &self.min_account_sequence_ledger_gap {
317            writeln!(f, "  Min Account Sequence Ledger Gap: {}", gap)?;
318        }
319
320        // Operations
321        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        // Signatures
330        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        // Transaction hash
343        if let Some(hash) = &self.hash {
344            writeln!(f, "  Hash: {:?}", hash)?;
345        }
346
347        // Soroban data
348        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        //TODO: Tests still coming in for Envelope
404
405        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        // from https://github.com/stellar/js-stellar-sdk/issues/73
425        let xdr = "AAAAAPQQv+uPYrlCDnjgPyPRgIjB6T8Zb8ANmL8YGAXC2IAgAAAAZAAIteYAAAAHAAAAAAAAAAAAAAABAAAAAAAAAAMAAAAAAAAAAUVVUgAAAAAAUtYuFczBLlsXyEp3q8BbTBpEGINWahqkFbnTPd93YUUAAAAXSHboAAAAABEAACcQAAAAAAAAAKIAAAAAAAAAAcLYgCAAAABAo2tU6n0Bb7bbbpaXacVeaTVbxNMBtnrrXVk2QAOje2Flllk/ORlmQdFU/9c8z43eWh1RNMpI3PscY+yDCnJPBQ==";
426
427        // Decode base64 XDR
428        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}