1use crate::error::{Error, Result};
4use apex_sdk_types::{Address, Chain, TransactionStatus};
5use serde::{Deserialize, Serialize};
6
7pub 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 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 pub fn from_substrate_account(mut self, address: impl Into<String>) -> Self {
34 self.from = Some(Address::substrate(address));
35 self
36 }
37
38 pub fn from_evm_address(mut self, address: impl Into<String>) -> Self {
40 self.from = Some(Address::evm(address));
41 self
42 }
43
44 pub fn from(mut self, address: Address) -> Self {
46 self.from = Some(address);
47 self
48 }
49
50 pub fn to_evm_address(mut self, address: impl Into<String>) -> Self {
52 self.to = Some(Address::evm(address));
53 self
54 }
55
56 pub fn to_substrate_account(mut self, address: impl Into<String>) -> Self {
58 self.to = Some(Address::substrate(address));
59 self
60 }
61
62 pub fn to(mut self, address: Address) -> Self {
64 self.to = Some(address);
65 self
66 }
67
68 pub fn amount(mut self, amount: u128) -> Self {
70 self.amount = Some(amount);
71 self
72 }
73
74 pub fn on_chain(mut self, chain: Chain) -> Self {
76 self.source_chain = Some(chain);
77 self
78 }
79
80 pub fn with_data(mut self, data: Vec<u8>) -> Self {
82 self.data = Some(data);
83 self
84 }
85
86 pub fn with_gas_limit(mut self, limit: u64) -> Self {
88 self.gas_limit = Some(limit);
89 self
90 }
91
92 #[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 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#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct Transaction {
137 pub from: Address,
139 pub to: Address,
141 pub amount: u128,
143 pub source_chain: Chain,
145 pub destination_chain: Chain,
147 pub data: Option<Vec<u8>>,
149 pub gas_limit: Option<u64>,
151}
152
153impl Transaction {
154 pub fn is_cross_chain(&self) -> bool {
156 self.source_chain != self.destination_chain
157 }
158
159 pub fn hash(&self) -> String {
161 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#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct TransactionResult {
170 pub source_tx_hash: String,
172 pub destination_tx_hash: Option<String>,
174 pub status: TransactionStatus,
176 pub block_number: Option<u64>,
178 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}