apex_sdk/
transaction.rs

1//! Transaction building and execution
2
3use crate::error::{Error, Result};
4use apex_sdk_types::{Address, Chain, TransactionStatus};
5use serde::{Deserialize, Serialize};
6
7/// Transaction builder for creating cross-chain transactions
8pub struct TransactionBuilder {
9    from: Option<Address>,
10    to: Option<Address>,
11    amount: Option<u128>,
12    source_chain: Option<Chain>,
13    destination_chain: Option<Chain>,
14    data: Option<Vec<u8>>,
15    gas_limit: Option<u64>,
16}
17
18impl TransactionBuilder {
19    /// Create a new transaction builder
20    pub fn new() -> Self {
21        Self {
22            from: None,
23            to: None,
24            amount: None,
25            source_chain: None,
26            destination_chain: None,
27            data: None,
28            gas_limit: None,
29        }
30    }
31
32    /// Set the sender address (Substrate)
33    pub fn from_substrate_account(mut self, address: impl Into<String>) -> Self {
34        self.from = Some(Address::substrate(address));
35        self
36    }
37
38    /// Set the sender address (EVM)
39    pub fn from_evm_address(mut self, address: impl Into<String>) -> Self {
40        self.from = Some(Address::evm(address));
41        self
42    }
43
44    /// Set the sender address
45    pub fn from(mut self, address: Address) -> Self {
46        self.from = Some(address);
47        self
48    }
49
50    /// Set the recipient address (EVM)
51    pub fn to_evm_address(mut self, address: impl Into<String>) -> Self {
52        self.to = Some(Address::evm(address));
53        self
54    }
55
56    /// Set the recipient address (Substrate)
57    pub fn to_substrate_account(mut self, address: impl Into<String>) -> Self {
58        self.to = Some(Address::substrate(address));
59        self
60    }
61
62    /// Set the recipient address
63    pub fn to(mut self, address: Address) -> Self {
64        self.to = Some(address);
65        self
66    }
67
68    /// Set the transfer amount
69    pub fn amount(mut self, amount: u128) -> Self {
70        self.amount = Some(amount);
71        self
72    }
73
74    /// Set the source chain
75    pub fn on_chain(mut self, chain: Chain) -> Self {
76        self.source_chain = Some(chain);
77        self
78    }
79
80    /// Set transaction data/payload
81    pub fn with_data(mut self, data: Vec<u8>) -> Self {
82        self.data = Some(data);
83        self
84    }
85
86    /// Set gas limit
87    pub fn with_gas_limit(mut self, limit: u64) -> Self {
88        self.gas_limit = Some(limit);
89        self
90    }
91
92    /// Build the transaction
93    #[allow(clippy::result_large_err)]
94    pub fn build(self) -> Result<Transaction> {
95        let from = self
96            .from
97            .ok_or_else(|| Error::Transaction("Sender address required".to_string()))?;
98        let to = self
99            .to
100            .ok_or_else(|| Error::Transaction("Recipient address required".to_string()))?;
101        let amount = self
102            .amount
103            .ok_or_else(|| Error::Transaction("Amount required".to_string()))?;
104
105        // Determine source and destination chains based on addresses if not specified
106        let source_chain = self.source_chain.unwrap_or(match &from {
107            Address::Substrate(_) => Chain::Polkadot,
108            Address::Evm(_) => Chain::Ethereum,
109        });
110
111        let destination_chain = self.destination_chain.unwrap_or(match &to {
112            Address::Substrate(_) => Chain::Polkadot,
113            Address::Evm(_) => Chain::Ethereum,
114        });
115
116        Ok(Transaction {
117            from,
118            to,
119            amount,
120            source_chain,
121            destination_chain,
122            data: self.data,
123            gas_limit: self.gas_limit,
124        })
125    }
126}
127
128impl Default for TransactionBuilder {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134/// Represents a blockchain transaction
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct Transaction {
137    /// Sender address
138    pub from: Address,
139    /// Recipient address
140    pub to: Address,
141    /// Amount to transfer
142    pub amount: u128,
143    /// Source blockchain
144    pub source_chain: Chain,
145    /// Destination blockchain
146    pub destination_chain: Chain,
147    /// Transaction data/payload
148    pub data: Option<Vec<u8>>,
149    /// Gas limit
150    pub gas_limit: Option<u64>,
151}
152
153impl Transaction {
154    /// Check if this is a cross-chain transaction
155    pub fn is_cross_chain(&self) -> bool {
156        self.source_chain != self.destination_chain
157    }
158
159    /// Get transaction hash (placeholder for actual implementation)
160    pub fn hash(&self) -> String {
161        // Simple hash based on sender/receiver addresses
162        let data = format!("{}{}{}", self.from.as_str(), self.to.as_str(), self.amount);
163        format!("0x{}", hex::encode(&data.as_bytes()[..32.min(data.len())]))
164    }
165}
166
167/// Transaction execution result
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct TransactionResult {
170    /// Transaction hash on source chain
171    pub source_tx_hash: String,
172    /// Transaction hash on destination chain (for cross-chain)
173    pub destination_tx_hash: Option<String>,
174    /// Transaction status
175    pub status: TransactionStatus,
176    /// Block number where transaction was included
177    pub block_number: Option<u64>,
178    /// Gas used
179    pub gas_used: Option<u64>,
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_transaction_builder_new() {
188        let builder = TransactionBuilder::new();
189        assert!(builder.from.is_none());
190        assert!(builder.to.is_none());
191        assert!(builder.amount.is_none());
192    }
193
194    #[test]
195    fn test_transaction_builder_default() {
196        let builder = TransactionBuilder::default();
197        assert!(builder.from.is_none());
198        assert!(builder.to.is_none());
199    }
200
201    #[test]
202    fn test_transaction_builder_evm_to_evm() {
203        let tx = TransactionBuilder::new()
204            .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
205            .to_evm_address("0x1234567890123456789012345678901234567890")
206            .amount(1000)
207            .build();
208
209        assert!(tx.is_ok());
210        let tx = tx.unwrap();
211        assert_eq!(tx.amount, 1000);
212        assert!(!tx.is_cross_chain());
213        assert_eq!(tx.source_chain, Chain::Ethereum);
214        assert_eq!(tx.destination_chain, Chain::Ethereum);
215    }
216
217    #[test]
218    fn test_transaction_builder_substrate_to_evm() {
219        let tx = TransactionBuilder::new()
220            .from_substrate_account("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY")
221            .to_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
222            .amount(500)
223            .build();
224
225        assert!(tx.is_ok());
226        let tx = tx.unwrap();
227        assert!(tx.is_cross_chain());
228        assert_eq!(tx.source_chain, Chain::Polkadot);
229        assert_eq!(tx.destination_chain, Chain::Ethereum);
230    }
231
232    #[test]
233    fn test_transaction_builder_substrate_to_substrate() {
234        let tx = TransactionBuilder::new()
235            .from_substrate_account("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY")
236            .to_substrate_account("5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty")
237            .amount(2000)
238            .build();
239
240        assert!(tx.is_ok());
241        let tx = tx.unwrap();
242        assert!(!tx.is_cross_chain());
243        assert_eq!(tx.source_chain, Chain::Polkadot);
244        assert_eq!(tx.destination_chain, Chain::Polkadot);
245    }
246
247    #[test]
248    fn test_transaction_builder_with_explicit_chain() {
249        let tx = TransactionBuilder::new()
250            .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
251            .to_evm_address("0x1234567890123456789012345678901234567890")
252            .amount(1000)
253            .on_chain(Chain::Polygon)
254            .build();
255
256        assert!(tx.is_ok());
257        let tx = tx.unwrap();
258        assert_eq!(tx.source_chain, Chain::Polygon);
259    }
260
261    #[test]
262    fn test_transaction_builder_missing_from() {
263        let result = TransactionBuilder::new()
264            .to_evm_address("0x1234567890123456789012345678901234567890")
265            .amount(100)
266            .build();
267
268        assert!(result.is_err());
269        match result {
270            Err(Error::Transaction(msg)) => {
271                assert!(msg.contains("Sender address required"));
272            }
273            _ => panic!("Expected Transaction error"),
274        }
275    }
276
277    #[test]
278    fn test_transaction_builder_missing_to() {
279        let result = TransactionBuilder::new()
280            .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
281            .amount(100)
282            .build();
283
284        assert!(result.is_err());
285        match result {
286            Err(Error::Transaction(msg)) => {
287                assert!(msg.contains("Recipient address required"));
288            }
289            _ => panic!("Expected Transaction error"),
290        }
291    }
292
293    #[test]
294    fn test_transaction_builder_missing_amount() {
295        let result = TransactionBuilder::new()
296            .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
297            .to_evm_address("0x1234567890123456789012345678901234567890")
298            .build();
299
300        assert!(result.is_err());
301        match result {
302            Err(Error::Transaction(msg)) => {
303                assert!(msg.contains("Amount required"));
304            }
305            _ => panic!("Expected Transaction error"),
306        }
307    }
308
309    #[test]
310    fn test_transaction_with_data() {
311        let tx = TransactionBuilder::new()
312            .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
313            .to_evm_address("0x1234567890123456789012345678901234567890")
314            .amount(1000)
315            .with_data(vec![1, 2, 3, 4])
316            .with_gas_limit(21000)
317            .build();
318
319        assert!(tx.is_ok());
320        let tx = tx.unwrap();
321        assert_eq!(tx.data, Some(vec![1, 2, 3, 4]));
322        assert_eq!(tx.gas_limit, Some(21000));
323    }
324
325    #[test]
326    fn test_transaction_with_empty_data() {
327        let tx = TransactionBuilder::new()
328            .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
329            .to_evm_address("0x1234567890123456789012345678901234567890")
330            .amount(1000)
331            .with_data(vec![])
332            .build();
333
334        assert!(tx.is_ok());
335        let tx = tx.unwrap();
336        assert_eq!(tx.data, Some(vec![]));
337    }
338
339    #[test]
340    fn test_transaction_is_cross_chain() {
341        let tx = Transaction {
342            from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
343            to: Address::substrate("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"),
344            amount: 1000,
345            source_chain: Chain::Ethereum,
346            destination_chain: Chain::Polkadot,
347            data: None,
348            gas_limit: None,
349        };
350
351        assert!(tx.is_cross_chain());
352    }
353
354    #[test]
355    fn test_transaction_is_not_cross_chain() {
356        let tx = Transaction {
357            from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
358            to: Address::evm("0x1234567890123456789012345678901234567890"),
359            amount: 1000,
360            source_chain: Chain::Ethereum,
361            destination_chain: Chain::Ethereum,
362            data: None,
363            gas_limit: None,
364        };
365
366        assert!(!tx.is_cross_chain());
367    }
368
369    #[test]
370    fn test_transaction_hash() {
371        let tx = Transaction {
372            from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
373            to: Address::evm("0x1234567890123456789012345678901234567890"),
374            amount: 1000,
375            source_chain: Chain::Ethereum,
376            destination_chain: Chain::Ethereum,
377            data: None,
378            gas_limit: None,
379        };
380
381        let hash = tx.hash();
382        assert!(hash.starts_with("0x"));
383        assert!(!hash.is_empty());
384    }
385
386    #[test]
387    fn test_transaction_clone() {
388        let tx = Transaction {
389            from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
390            to: Address::evm("0x1234567890123456789012345678901234567890"),
391            amount: 1000,
392            source_chain: Chain::Ethereum,
393            destination_chain: Chain::Ethereum,
394            data: Some(vec![1, 2, 3]),
395            gas_limit: Some(21000),
396        };
397
398        let cloned = tx.clone();
399        assert_eq!(tx.amount, cloned.amount);
400        assert_eq!(tx.data, cloned.data);
401        assert_eq!(tx.gas_limit, cloned.gas_limit);
402    }
403
404    #[test]
405    fn test_transaction_result_serialization() {
406        let result = TransactionResult {
407            source_tx_hash: "0xabc123".to_string(),
408            destination_tx_hash: Some("0xdef456".to_string()),
409            status: TransactionStatus::Confirmed {
410                block_number: 12345,
411                confirmations: 3,
412            },
413            block_number: Some(12345),
414            gas_used: Some(21000),
415        };
416
417        let json = serde_json::to_string(&result).unwrap();
418        let deserialized: TransactionResult = serde_json::from_str(&json).unwrap();
419
420        assert_eq!(result.source_tx_hash, deserialized.source_tx_hash);
421        assert_eq!(result.destination_tx_hash, deserialized.destination_tx_hash);
422        assert_eq!(result.block_number, deserialized.block_number);
423        assert_eq!(result.gas_used, deserialized.gas_used);
424    }
425}