substrate_stellar_sdk/xdr/impls/
transaction.rs

1//! Transaction envelopes and signatures
2
3use crate::{
4    types::{
5        FeeBumpTransactionEnvelope, FeeBumpTransactionExt, FeeBumpTransactionInnerTx, Memo, MuxedAccount, Operation,
6        Preconditions, TimeBounds, Transaction, TransactionEnvelope, TransactionExt, TransactionV0, TransactionV0Ext,
7        TransactionV1Envelope,
8    },
9    xdr::compound_types::LimitedVarArray,
10    FeeBumpTransaction, IntoAmount, IntoMuxedAccountId, StellarSdkError, BASE_FEE_STROOPS,
11};
12
13// fee is not fee per operation but total fee
14// sequence_number must be 1 + current account sequence numbere
15impl Transaction {
16    pub fn new<T: IntoMuxedAccountId>(
17        source_account: T,
18        sequence_number: i64,
19        fee_per_operation: Option<u32>,
20        preconditions: Preconditions,
21        memo: Option<Memo>,
22    ) -> Result<Self, StellarSdkError> {
23        let transaction = Self {
24            source_account: source_account.into_muxed_account_id()?,
25            fee: fee_per_operation.unwrap_or(BASE_FEE_STROOPS),
26            seq_num: sequence_number,
27            cond: preconditions,
28            memo: memo.unwrap_or(Memo::MemoNone),
29            operations: LimitedVarArray::new_empty(),
30            ext: TransactionExt::V0,
31        };
32
33        Ok(transaction)
34    }
35
36    pub fn append_operation(&mut self, operation: Operation) -> Result<(), StellarSdkError> {
37        self.operations.push(operation)
38    }
39
40    // careful: this operation also multiplies the fees with the number of operations
41    pub fn into_transaction_envelope(mut self) -> TransactionEnvelope {
42        self.fee = self.fee.checked_mul(self.operations.len() as u32).unwrap_or(self.fee);
43
44        TransactionEnvelope::EnvelopeTypeTx(TransactionV1Envelope {
45            tx: self,
46            signatures: LimitedVarArray::new_empty(),
47        })
48    }
49}
50
51impl From<TransactionV0> for Transaction {
52    fn from(transaction: TransactionV0) -> Self {
53        let time_bounds = transaction.time_bounds.unwrap_or(TimeBounds::from_time_points((), ()));
54
55        Self {
56            source_account: MuxedAccount::KeyTypeEd25519(transaction.source_account_ed25519),
57            fee: transaction.fee,
58            seq_num: transaction.seq_num,
59            cond: Preconditions::PrecondTime(time_bounds),
60            memo: transaction.memo,
61            operations: transaction.operations,
62            ext: match transaction.ext {
63                TransactionV0Ext::V0 => TransactionExt::V0,
64                TransactionV0Ext::Default(default) => TransactionExt::Default(default),
65            },
66        }
67    }
68}
69
70impl FeeBumpTransaction {
71    pub fn new<T: IntoMuxedAccountId, S: IntoAmount>(
72        fee_source: T,
73        new_total_fee: S,
74        inner_transaction_envelope: TransactionEnvelope,
75    ) -> Result<Self, StellarSdkError> {
76        let v1_envelope = match inner_transaction_envelope {
77            TransactionEnvelope::EnvelopeTypeTxV0(envelope) =>
78                TransactionV1Envelope { tx: envelope.tx.into(), signatures: envelope.signatures },
79            TransactionEnvelope::EnvelopeTypeTx(envelope) => envelope,
80            TransactionEnvelope::EnvelopeTypeTxFeeBump(_) => return Err(StellarSdkError::CantWrapFeeBumpTransaction),
81            TransactionEnvelope::Default(_) => unreachable!(),
82        };
83
84        let transaction = Self {
85            fee_source: fee_source.into_muxed_account_id()?,
86            fee: new_total_fee.into_stroop_amount(false)?,
87            inner_tx: FeeBumpTransactionInnerTx::EnvelopeTypeTx(v1_envelope),
88            ext: FeeBumpTransactionExt::V0,
89        };
90
91        Ok(transaction)
92    }
93
94    pub fn into_transaction_envelope(self) -> TransactionEnvelope {
95        TransactionEnvelope::EnvelopeTypeTxFeeBump(FeeBumpTransactionEnvelope {
96            tx: self,
97            signatures: LimitedVarArray::new_empty(),
98        })
99    }
100}
101
102#[cfg(test)]
103mod test {
104    use crate::{
105        network::TEST_NETWORK, types::Preconditions, Asset, IntoSecretKey, Memo, MilliSecondEpochTime, Operation,
106        Price, SecondEpochTime, StroopAmount, TimeBounds, Transaction, TransactionEnvelope, XdrCodec,
107    };
108
109    const ACCOUNT_ID1: &str = "GDGRDTRINPF66FNC47H22NY6BNWMCD5Q4XZTVA2KG7PFZ64WHRIU62TQ";
110    const ACCOUNT_ID2: &str = "GBNKQVTFRP25TIQRODMU5GJGSXDKHCEUDN7LNMOS5PNM427LMR77NV4M";
111    const ACCOUNT_ID3: &str = "GCACWDM2VEYTXGUI3CUYLBJ453IBEPQ3XEJKA772ARAP5XDQ4NMGFZGJ";
112
113    const SIGNER1: &str = "SCVKZEONBSU3XD6OTHXGAP6BTEWHOU4RPZQZJJ5AVAGPXUZ5A4D7MU6S";
114    const SIGNER3: &str = "SDOKV37I4TI655LMEMDQFOWESJ3LK6DDFKIVTYKN4YYTSAYFIBPP7MYI";
115
116    #[test]
117    fn build_transaction1() {
118        let mut transaction = Transaction::new(
119            ACCOUNT_ID1,
120            1980190376853505,
121            Some(100),
122            Preconditions::PrecondTime(TimeBounds::from_time_points(SecondEpochTime(0), SecondEpochTime(1626258131))),
123            None,
124        )
125        .unwrap();
126
127        transaction
128            .append_operation(Operation::new_payment(ACCOUNT_ID2, Asset::native(), "123.456").unwrap())
129            .unwrap();
130
131        let expexted_base64 = b"AAAAAgAAAADNEc4oa8vvFaL\
132            nz603HgtswQ+w5fM6g0o33lz7ljxRTwAAAGQABwj5AAAAAQAAAAEA\
133            AAAAAAAAAAAAAABg7rrTAAAAAAAAAAEAAAAAAAAAAQAAAABaqFZli\
134            /XZohFw2U6ZJpXGo4iUG362sdLr2s5r62R/9gAAAAAAAAAASZXkAA\
135            AAAAAAAAAA";
136        let envelope = transaction.into_transaction_envelope();
137
138        assert_eq!(envelope, TransactionEnvelope::from_base64_xdr(expexted_base64).unwrap());
139        assert_eq!(envelope.to_base64_xdr(), expexted_base64);
140    }
141
142    #[test]
143    fn build_transaction2() {
144        let mut transaction = Transaction::new(
145            ACCOUNT_ID1,
146            1980190376853505,
147            Some(321),
148            Preconditions::PrecondTime(TimeBounds::from_time_points(SecondEpochTime(0), SecondEpochTime(0))),
149            Some(Memo::from_text_memo("Hello World!").unwrap()),
150        )
151        .unwrap();
152
153        transaction
154            .append_operation(
155                Operation::new_payment(
156                    ACCOUNT_ID2,
157                    Asset::from_asset_code("USD", ACCOUNT_ID3).unwrap(),
158                    StroopAmount(1234560000),
159                )
160                .unwrap(),
161            )
162            .unwrap();
163
164        let expexted_base64 = b"AAAAAgAAAADNEc4oa8vvFaLnz603HgtswQ+w5fM6g0o33lz7ljxRTwAAAUEABwj5AAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAxIZWxsbyBXb3JsZCEAAAABAAAAAAAAAAEAAAAAWqhWZYv12aIRcNlOmSaVxqOIlBt+trHS69rOa+tkf/YAAAABVVNEAAAAAACAKw2aqTE7mojYqYWFPO7QEj4buRKgf/oEQP7ccONYYgAAAABJleQAAAAAAAAAAAA=";
165        let envelope = transaction.into_transaction_envelope();
166
167        assert_eq!(envelope, TransactionEnvelope::from_base64_xdr(expexted_base64).unwrap());
168        assert_eq!(envelope.to_base64_xdr(), expexted_base64);
169    }
170
171    #[test]
172    fn build_transaction3() {
173        let mut transaction = Transaction::new(
174            ACCOUNT_ID1,
175            1980190376853505,
176            Some(321),
177            Preconditions::PrecondTime(TimeBounds::from_time_points(
178                SecondEpochTime(162620000),
179                MilliSecondEpochTime(1626263454_000),
180            )),
181            Some(Memo::from_text_memo("Hello World!").unwrap()),
182        )
183        .unwrap();
184
185        transaction
186            .append_operation(
187                Operation::new_payment(
188                    ACCOUNT_ID2,
189                    Asset::from_asset_code("USD", ACCOUNT_ID3).unwrap(),
190                    StroopAmount(1234560000),
191                )
192                .unwrap()
193                .set_source_account(ACCOUNT_ID3)
194                .unwrap(),
195            )
196            .unwrap();
197
198        transaction
199            .append_operation(
200                Operation::new_manage_sell_offer(
201                    Asset::from_asset_code("DOMINATION", ACCOUNT_ID2).unwrap(),
202                    Asset::native(),
203                    "152.103",
204                    Price::from_float(4.58).unwrap(),
205                    Some(123456789),
206                )
207                .unwrap(),
208            )
209            .unwrap();
210
211        let expexted_base64 = b"AAAAAgAAAADNEc4oa8vvFaLnz603HgtswQ+w5fM6g0o33lz7ljxRTwAAAoIABwj5AAAAAQAAAAEAAAAACbFiYAAAAABg7s+eAAAAAQAAAAxIZWxsbyBXb3JsZCEAAAACAAAAAQAAAACAKw2aqTE7mojYqYWFPO7QEj4buRKgf/oEQP7ccONYYgAAAAEAAAAAWqhWZYv12aIRcNlOmSaVxqOIlBt+trHS69rOa+tkf/YAAAABVVNEAAAAAACAKw2aqTE7mojYqYWFPO7QEj4buRKgf/oEQP7ccONYYgAAAABJleQAAAAAAAAAAAMAAAACRE9NSU5BVElPTgAAAAAAAFqoVmWL9dmiEXDZTpkmlcajiJQbfrax0uvazmvrZH/2AAAAAAAAAABaqRNwAAAA5QAAADIAAAAAB1vNFQAAAAAAAAAA";
212        let mut envelope = transaction.into_transaction_envelope();
213
214        assert_eq!(envelope, TransactionEnvelope::from_base64_xdr(expexted_base64).unwrap());
215        assert_eq!(envelope.to_base64_xdr(), expexted_base64);
216
217        envelope
218            .sign(&TEST_NETWORK, vec![&SIGNER1.into_secret_key().unwrap(), &SIGNER3.into_secret_key().unwrap()])
219            .unwrap();
220
221        let expexted_singed_base64 = b"AAAAAgAAAADNEc4oa8vvFaLnz603HgtswQ+w5fM6g0o33lz7ljxRTwAAAoIABwj5AAAAAQAAAAEAAAAACbFiYAAAAABg7s+eAAAAAQAAAAxIZWxsbyBXb3JsZCEAAAACAAAAAQAAAACAKw2aqTE7mojYqYWFPO7QEj4buRKgf/oEQP7ccONYYgAAAAEAAAAAWqhWZYv12aIRcNlOmSaVxqOIlBt+trHS69rOa+tkf/YAAAABVVNEAAAAAACAKw2aqTE7mojYqYWFPO7QEj4buRKgf/oEQP7ccONYYgAAAABJleQAAAAAAAAAAAMAAAACRE9NSU5BVElPTgAAAAAAAFqoVmWL9dmiEXDZTpkmlcajiJQbfrax0uvazmvrZH/2AAAAAAAAAABaqRNwAAAA5QAAADIAAAAAB1vNFQAAAAAAAAACljxRTwAAAEB0B8vODxIESpa9H9f4QkPtFHVg4Xjx2A9aTncJOkW6BW0i1AxZFgMvrzEb7nO5UnXRvCKnBmuhpvA76YivAXYGcONYYgAAAECesooI2hhhuoOcLcXB76L58vMOrFvPFqpIeG+/zzLZXz0XYU6aELdtDxLAhK8GZCIZwlXdJ/RyZF9/2YzqbPMC";
222        assert_eq!(envelope, TransactionEnvelope::from_base64_xdr(expexted_singed_base64).unwrap());
223        assert_eq!(envelope.to_base64_xdr(), expexted_singed_base64);
224    }
225}