1use crate::error::{Error, Result};
4use apex_sdk_types::{Address, Chain, TransactionStatus};
5use serde::{Deserialize, Serialize};
6use sha3::{Digest, Keccak256};
7
8pub struct TransactionBuilder {
10 from: Option<Address>,
11 to: Option<Address>,
12 amount: Option<u128>,
13 source_chain: Option<Chain>,
14 destination_chain: Option<Chain>,
15 data: Option<Vec<u8>>,
16 gas_limit: Option<u64>,
17 nonce: Option<u64>,
18}
19
20impl TransactionBuilder {
21 pub fn new() -> Self {
23 Self {
24 from: None,
25 to: None,
26 amount: None,
27 source_chain: None,
28 destination_chain: None,
29 data: None,
30 gas_limit: None,
31 nonce: None,
32 }
33 }
34
35 pub fn from_substrate_account(mut self, address: impl Into<String>) -> Self {
41 self.from = Some(Address::substrate(address));
42 self
43 }
44
45 pub fn from_substrate_account_checked(mut self, address: impl Into<String>) -> Result<Self> {
50 self.from = Some(
51 Address::substrate_checked(address)
52 .map_err(|e| Error::InvalidAddress(e.to_string()))?,
53 );
54 Ok(self)
55 }
56
57 pub fn from_evm_address(mut self, address: impl Into<String>) -> Self {
63 self.from = Some(Address::evm(address));
64 self
65 }
66
67 pub fn from_evm_address_checked(mut self, address: impl Into<String>) -> Result<Self> {
72 self.from =
73 Some(Address::evm_checked(address).map_err(|e| Error::InvalidAddress(e.to_string()))?);
74 Ok(self)
75 }
76
77 pub fn from(mut self, address: Address) -> Self {
79 self.from = Some(address);
80 self
81 }
82
83 pub fn to_evm_address(mut self, address: impl Into<String>) -> Self {
89 self.to = Some(Address::evm(address));
90 self
91 }
92
93 pub fn to_evm_address_checked(mut self, address: impl Into<String>) -> Result<Self> {
98 self.to =
99 Some(Address::evm_checked(address).map_err(|e| Error::InvalidAddress(e.to_string()))?);
100 Ok(self)
101 }
102
103 pub fn to_substrate_account(mut self, address: impl Into<String>) -> Self {
109 self.to = Some(Address::substrate(address));
110 self
111 }
112
113 pub fn to_substrate_account_checked(mut self, address: impl Into<String>) -> Result<Self> {
118 self.to = Some(
119 Address::substrate_checked(address)
120 .map_err(|e| Error::InvalidAddress(e.to_string()))?,
121 );
122 Ok(self)
123 }
124
125 pub fn to(mut self, address: Address) -> Self {
127 self.to = Some(address);
128 self
129 }
130
131 pub fn amount(mut self, amount: u128) -> Self {
133 self.amount = Some(amount);
134 self
135 }
136
137 pub fn on_chain(mut self, chain: Chain) -> Self {
139 self.source_chain = Some(chain);
140 self
141 }
142
143 pub fn with_data(mut self, data: Vec<u8>) -> Self {
145 self.data = Some(data);
146 self
147 }
148
149 pub fn with_gas_limit(mut self, limit: u64) -> Self {
151 self.gas_limit = Some(limit);
152 self
153 }
154
155 pub fn with_nonce(mut self, nonce: u64) -> Self {
157 self.nonce = Some(nonce);
158 self
159 }
160
161 pub fn build(self) -> Result<Transaction> {
163 let from = self
164 .from
165 .ok_or_else(|| Error::transaction("Sender address required"))?;
166 let to = self
167 .to
168 .ok_or_else(|| Error::transaction("Recipient address required"))?;
169 let amount = self
170 .amount
171 .ok_or_else(|| Error::transaction("Amount required"))?;
172
173 let source_chain = self.source_chain.unwrap_or(match &from {
175 Address::Substrate(_) => Chain::Polkadot,
176 Address::Evm(_) => Chain::Ethereum,
177 });
178
179 let destination_chain = self.destination_chain.unwrap_or(match &to {
180 Address::Substrate(_) => Chain::Polkadot,
181 Address::Evm(_) => Chain::Ethereum,
182 });
183
184 Ok(Transaction {
185 from,
186 to,
187 amount,
188 source_chain,
189 destination_chain,
190 data: self.data,
191 gas_limit: self.gas_limit,
192 nonce: self.nonce,
193 })
194 }
195}
196
197impl Default for TransactionBuilder {
198 fn default() -> Self {
199 Self::new()
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct Transaction {
206 pub from: Address,
208 pub to: Address,
210 pub amount: u128,
212 pub source_chain: Chain,
214 pub destination_chain: Chain,
216 pub data: Option<Vec<u8>>,
218 pub gas_limit: Option<u64>,
220 #[serde(default)]
222 pub nonce: Option<u64>,
223}
224
225impl Transaction {
226 pub fn is_cross_chain(&self) -> bool {
228 self.source_chain != self.destination_chain
229 }
230
231 pub fn hash(&self) -> String {
243 let mut hasher = Keccak256::new();
244
245 hasher.update(b"from:");
249 hasher.update(self.from.as_str().as_bytes());
250
251 hasher.update(b"to:");
253 hasher.update(self.to.as_str().as_bytes());
254
255 hasher.update(b"amount:");
257 hasher.update(self.amount.to_le_bytes());
258
259 hasher.update(b"source_chain:");
261 hasher.update(self.source_chain.name().as_bytes());
262
263 hasher.update(b"destination_chain:");
265 hasher.update(self.destination_chain.name().as_bytes());
266
267 hasher.update(b"data:");
269 if let Some(ref data) = self.data {
270 hasher.update(b"some:");
271 hasher.update((data.len() as u64).to_le_bytes());
272 hasher.update(data);
273 } else {
274 hasher.update(b"none");
275 }
276
277 hasher.update(b"gas_limit:");
279 if let Some(gas_limit) = self.gas_limit {
280 hasher.update(b"some:");
281 hasher.update(gas_limit.to_le_bytes());
282 } else {
283 hasher.update(b"none");
284 }
285
286 hasher.update(b"nonce:");
288 if let Some(nonce) = self.nonce {
289 hasher.update(b"some:");
290 hasher.update(nonce.to_le_bytes());
291 } else {
292 hasher.update(b"none");
293 }
294
295 let result = hasher.finalize();
296 format!("0x{}", hex::encode(result))
297 }
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct TransactionResult {
303 pub source_tx_hash: String,
305 pub destination_tx_hash: Option<String>,
307 pub status: TransactionStatus,
309 pub block_number: Option<u64>,
311 pub gas_used: Option<u64>,
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_transaction_builder_new() {
321 let builder = TransactionBuilder::new();
322 assert!(builder.from.is_none());
323 assert!(builder.to.is_none());
324 assert!(builder.amount.is_none());
325 }
326
327 #[test]
328 fn test_transaction_builder_default() {
329 let builder = TransactionBuilder::default();
330 assert!(builder.from.is_none());
331 assert!(builder.to.is_none());
332 }
333
334 #[test]
335 fn test_transaction_builder_evm_to_evm() {
336 let tx = TransactionBuilder::new()
337 .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
338 .to_evm_address("0x1234567890123456789012345678901234567890")
339 .amount(1000)
340 .build();
341
342 assert!(tx.is_ok());
343 let tx = tx.unwrap();
344 assert_eq!(tx.amount, 1000);
345 assert!(!tx.is_cross_chain());
346 assert_eq!(tx.source_chain, Chain::Ethereum);
347 assert_eq!(tx.destination_chain, Chain::Ethereum);
348 }
349
350 #[test]
351 fn test_transaction_builder_substrate_to_evm() {
352 let tx = TransactionBuilder::new()
353 .from_substrate_account("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY")
354 .to_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
355 .amount(500)
356 .build();
357
358 assert!(tx.is_ok());
359 let tx = tx.unwrap();
360 assert!(tx.is_cross_chain());
361 assert_eq!(tx.source_chain, Chain::Polkadot);
362 assert_eq!(tx.destination_chain, Chain::Ethereum);
363 }
364
365 #[test]
366 fn test_transaction_builder_substrate_to_substrate() {
367 let tx = TransactionBuilder::new()
368 .from_substrate_account("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY")
369 .to_substrate_account("5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty")
370 .amount(2000)
371 .build();
372
373 assert!(tx.is_ok());
374 let tx = tx.unwrap();
375 assert!(!tx.is_cross_chain());
376 assert_eq!(tx.source_chain, Chain::Polkadot);
377 assert_eq!(tx.destination_chain, Chain::Polkadot);
378 }
379
380 #[test]
381 fn test_transaction_builder_with_explicit_chain() {
382 let tx = TransactionBuilder::new()
383 .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
384 .to_evm_address("0x1234567890123456789012345678901234567890")
385 .amount(1000)
386 .on_chain(Chain::Polygon)
387 .build();
388
389 assert!(tx.is_ok());
390 let tx = tx.unwrap();
391 assert_eq!(tx.source_chain, Chain::Polygon);
392 }
393
394 #[test]
395 fn test_transaction_builder_missing_from() {
396 let result = TransactionBuilder::new()
397 .to_evm_address("0x1234567890123456789012345678901234567890")
398 .amount(100)
399 .build();
400
401 assert!(result.is_err());
402 match result {
403 Err(Error::Transaction(msg, _)) => {
404 assert!(msg.contains("Sender address required"));
405 }
406 _ => panic!("Expected Transaction error"),
407 }
408 }
409
410 #[test]
411 fn test_transaction_builder_missing_to() {
412 let result = TransactionBuilder::new()
413 .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
414 .amount(100)
415 .build();
416
417 assert!(result.is_err());
418 match result {
419 Err(Error::Transaction(msg, _)) => {
420 assert!(msg.contains("Recipient address required"));
421 }
422 _ => panic!("Expected Transaction error"),
423 }
424 }
425
426 #[test]
427 fn test_transaction_builder_missing_amount() {
428 let result = TransactionBuilder::new()
429 .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
430 .to_evm_address("0x1234567890123456789012345678901234567890")
431 .build();
432
433 assert!(result.is_err());
434 match result {
435 Err(Error::Transaction(msg, _)) => {
436 assert!(msg.contains("Amount required"));
437 }
438 _ => panic!("Expected Transaction error"),
439 }
440 }
441
442 #[test]
443 fn test_transaction_with_data() {
444 let tx = TransactionBuilder::new()
445 .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
446 .to_evm_address("0x1234567890123456789012345678901234567890")
447 .amount(1000)
448 .with_data(vec![1, 2, 3, 4])
449 .with_gas_limit(21000)
450 .build();
451
452 assert!(tx.is_ok());
453 let tx = tx.unwrap();
454 assert_eq!(tx.data, Some(vec![1, 2, 3, 4]));
455 assert_eq!(tx.gas_limit, Some(21000));
456 }
457
458 #[test]
459 fn test_transaction_with_empty_data() {
460 let tx = TransactionBuilder::new()
461 .from_evm_address("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7")
462 .to_evm_address("0x1234567890123456789012345678901234567890")
463 .amount(1000)
464 .with_data(vec![])
465 .build();
466
467 assert!(tx.is_ok());
468 let tx = tx.unwrap();
469 assert_eq!(tx.data, Some(vec![]));
470 }
471
472 #[test]
473 fn test_transaction_is_cross_chain() {
474 let tx = Transaction {
475 from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
476 to: Address::substrate("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"),
477 amount: 1000,
478 source_chain: Chain::Ethereum,
479 destination_chain: Chain::Polkadot,
480 data: None,
481 gas_limit: None,
482 nonce: None,
483 };
484
485 assert!(tx.is_cross_chain());
486 }
487
488 #[test]
489 fn test_transaction_is_not_cross_chain() {
490 let tx = Transaction {
491 from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
492 to: Address::evm("0x1234567890123456789012345678901234567890"),
493 amount: 1000,
494 source_chain: Chain::Ethereum,
495 destination_chain: Chain::Ethereum,
496 data: None,
497 gas_limit: None,
498 nonce: None,
499 };
500
501 assert!(!tx.is_cross_chain());
502 }
503
504 #[test]
505 fn test_transaction_hash() {
506 let tx = Transaction {
507 from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
508 to: Address::evm("0x1234567890123456789012345678901234567890"),
509 amount: 1000,
510 source_chain: Chain::Ethereum,
511 destination_chain: Chain::Ethereum,
512 data: None,
513 gas_limit: None,
514 nonce: None,
515 };
516
517 let hash = tx.hash();
518 assert!(hash.starts_with("0x"));
519 assert_eq!(hash.len(), 66); }
521
522 #[test]
523 fn test_transaction_hash_determinism() {
524 let tx1 = Transaction {
526 from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
527 to: Address::evm("0x1234567890123456789012345678901234567890"),
528 amount: 1000,
529 source_chain: Chain::Ethereum,
530 destination_chain: Chain::Ethereum,
531 data: Some(vec![1, 2, 3, 4]),
532 gas_limit: Some(21000),
533 nonce: Some(42),
534 };
535
536 let tx2 = Transaction {
537 from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
538 to: Address::evm("0x1234567890123456789012345678901234567890"),
539 amount: 1000,
540 source_chain: Chain::Ethereum,
541 destination_chain: Chain::Ethereum,
542 data: Some(vec![1, 2, 3, 4]),
543 gas_limit: Some(21000),
544 nonce: Some(42),
545 };
546
547 assert_eq!(tx1.hash(), tx2.hash());
549 }
550
551 #[test]
552 fn test_transaction_hash_changes_with_nonce() {
553 let tx1 = Transaction {
554 from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
555 to: Address::evm("0x1234567890123456789012345678901234567890"),
556 amount: 1000,
557 source_chain: Chain::Ethereum,
558 destination_chain: Chain::Ethereum,
559 data: None,
560 gas_limit: None,
561 nonce: Some(1),
562 };
563
564 let tx2 = Transaction {
565 from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
566 to: Address::evm("0x1234567890123456789012345678901234567890"),
567 amount: 1000,
568 source_chain: Chain::Ethereum,
569 destination_chain: Chain::Ethereum,
570 data: None,
571 gas_limit: None,
572 nonce: Some(2),
573 };
574
575 assert_ne!(tx1.hash(), tx2.hash());
577 }
578
579 #[test]
580 fn test_transaction_hash_none_vs_some_data() {
581 let tx_none = Transaction {
582 from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
583 to: Address::evm("0x1234567890123456789012345678901234567890"),
584 amount: 1000,
585 source_chain: Chain::Ethereum,
586 destination_chain: Chain::Ethereum,
587 data: None,
588 gas_limit: None,
589 nonce: None,
590 };
591
592 let tx_empty = Transaction {
593 from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
594 to: Address::evm("0x1234567890123456789012345678901234567890"),
595 amount: 1000,
596 source_chain: Chain::Ethereum,
597 destination_chain: Chain::Ethereum,
598 data: Some(vec![]),
599 gas_limit: None,
600 nonce: None,
601 };
602
603 assert_ne!(tx_none.hash(), tx_empty.hash());
605 }
606
607 #[test]
608 fn test_transaction_clone() {
609 let tx = Transaction {
610 from: Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"),
611 to: Address::evm("0x1234567890123456789012345678901234567890"),
612 amount: 1000,
613 source_chain: Chain::Ethereum,
614 destination_chain: Chain::Ethereum,
615 data: Some(vec![1, 2, 3]),
616 gas_limit: Some(21000),
617 nonce: Some(5),
618 };
619
620 let cloned = tx.clone();
621 assert_eq!(tx.amount, cloned.amount);
622 assert_eq!(tx.data, cloned.data);
623 assert_eq!(tx.gas_limit, cloned.gas_limit);
624 assert_eq!(tx.nonce, cloned.nonce);
625 }
626
627 #[test]
628 fn test_transaction_result_serialization() {
629 let result = TransactionResult {
630 source_tx_hash: "0xabc123".to_string(),
631 destination_tx_hash: Some("0xdef456".to_string()),
632 status: TransactionStatus::Confirmed {
633 block_hash: "0xblock123".to_string(),
634 block_number: Some(12345),
635 },
636 block_number: Some(12345),
637 gas_used: Some(21000),
638 };
639
640 let json = serde_json::to_string(&result).unwrap();
641 let deserialized: TransactionResult = serde_json::from_str(&json).unwrap();
642
643 assert_eq!(result.source_tx_hash, deserialized.source_tx_hash);
644 assert_eq!(result.destination_tx_hash, deserialized.destination_tx_hash);
645 assert_eq!(result.block_number, deserialized.block_number);
646 assert_eq!(result.gas_used, deserialized.gas_used);
647 }
648}