use crate::{
error::Result,
types::{Address, Chain},
};
use serde::{Deserialize, Serialize};
use sha3::{Digest, Keccak256};
#[derive(Debug, Clone, Default)]
pub struct TransactionBuilder {
from: Option<Address>,
to: Option<Address>,
amount: Option<u128>,
gas_limit: Option<u64>,
gas_price: Option<u64>,
data: Option<Vec<u8>>,
chain: Option<Chain>,
}
impl TransactionBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn from(mut self, address: Address) -> Self {
self.from = Some(address);
self
}
pub fn from_evm_address(self, address: &str) -> Self {
self.from(Address::evm(address))
}
pub fn from_substrate_account(self, address: &str) -> Self {
self.from(Address::substrate(address))
}
pub fn to(mut self, address: Address) -> Self {
self.to = Some(address);
self
}
pub fn to_evm_address(self, address: &str) -> Self {
self.to(Address::evm(address))
}
pub fn to_substrate_account(self, address: &str) -> Self {
self.to(Address::substrate(address))
}
pub fn amount(mut self, amount: u128) -> Self {
self.amount = Some(amount);
self
}
pub fn gas_limit(mut self, limit: u64) -> Self {
self.gas_limit = Some(limit);
self
}
pub fn with_gas_limit(self, limit: u64) -> Self {
self.gas_limit(limit)
}
pub fn gas_price(mut self, price: u64) -> Self {
self.gas_price = Some(price);
self
}
pub fn data(mut self, data: Vec<u8>) -> Self {
self.data = Some(data);
self
}
pub fn with_data(self, data: Vec<u8>) -> Self {
self.data(data)
}
pub fn chain(mut self, chain: Chain) -> Self {
self.chain = Some(chain);
self
}
pub fn build(self) -> Result<Transaction> {
let from = self
.from
.ok_or_else(|| crate::error::Error::Config("From address is required".to_string()))?;
let to = self
.to
.ok_or_else(|| crate::error::Error::Config("To address is required".to_string()))?;
let amount = self
.amount
.ok_or_else(|| crate::error::Error::Config("Amount is required".to_string()))?;
Ok(Transaction {
from,
to,
amount,
gas_limit: self.gas_limit,
gas_price: self.gas_price,
data: self.data,
chain: self.chain,
nonce: None,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
pub from: Address,
pub to: Address,
pub amount: u128,
pub gas_limit: Option<u64>,
pub gas_price: Option<u64>,
pub data: Option<Vec<u8>>,
pub chain: Option<Chain>,
pub nonce: Option<u64>,
}
impl Transaction {
pub fn builder() -> TransactionBuilder {
TransactionBuilder::new()
}
pub fn destination_chain(&self) -> Chain {
self.chain.as_ref().unwrap_or(&Chain::Polkadot).clone()
}
pub fn is_cross_chain(&self) -> bool {
match (&self.from, &self.to) {
(Address::Substrate(_), Address::Evm(_)) => true,
(Address::Evm(_), Address::Substrate(_)) => true,
(Address::Substrate(from_addr), Address::Substrate(to_addr)) => {
!Self::same_substrate_network(from_addr, to_addr)
}
(Address::Evm(_), Address::Evm(_)) => {
false
}
}
}
fn same_substrate_network(addr1: &str, addr2: &str) -> bool {
use apex_sdk_types::extract_ss58_prefix;
match (extract_ss58_prefix(addr1), extract_ss58_prefix(addr2)) {
(Some(prefix1), Some(prefix2)) => prefix1 == prefix2,
_ => true, }
}
pub fn hash(&self) -> String {
let mut hasher = Keccak256::new();
hasher.update(format!("{:?}", self).as_bytes());
format!("0x{:x}", hasher.finalize())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionResult {
pub source_tx_hash: String,
pub destination_tx_hash: Option<String>,
pub status: TransactionStatus,
pub block_number: Option<u64>,
pub gas_used: Option<u64>,
}
impl TransactionResult {
pub fn new(source_tx_hash: String) -> Self {
Self {
source_tx_hash,
destination_tx_hash: None,
status: TransactionStatus::Pending,
block_number: None,
gas_used: None,
}
}
pub fn with_status(mut self, status: TransactionStatus) -> Self {
self.status = status;
self
}
pub fn with_block_number(mut self, block_number: u64) -> Self {
self.block_number = Some(block_number);
self
}
pub fn with_gas_used(mut self, gas_used: u64) -> Self {
self.gas_used = Some(gas_used);
self
}
pub fn with_destination_tx_hash(mut self, tx_hash: String) -> Self {
self.destination_tx_hash = Some(tx_hash);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TransactionStatus {
Pending,
Success,
Failed,
Finalized,
Unknown,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transaction_builder_new() {
let builder = TransactionBuilder::new();
assert!(builder.from.is_none());
assert!(builder.to.is_none());
assert!(builder.amount.is_none());
}
#[test]
fn test_transaction_builder_default() {
let builder = TransactionBuilder::default();
assert!(builder.from.is_none());
}
#[test]
fn test_transaction_builder_missing_from() {
let result = Transaction::builder()
.to(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.amount(1000)
.build();
assert!(result.is_err());
}
#[test]
fn test_transaction_builder_missing_to() {
let result = Transaction::builder()
.from(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.amount(1000)
.build();
assert!(result.is_err());
}
#[test]
fn test_transaction_builder_missing_amount() {
let result = Transaction::builder()
.from(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.to(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.build();
assert!(result.is_err());
}
#[test]
fn test_transaction_clone() {
let tx = Transaction::builder()
.from(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.to(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.amount(1000)
.build()
.unwrap();
let _cloned = tx.clone();
}
#[test]
fn test_transaction_hash() {
let tx = Transaction::builder()
.from(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.to(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.amount(1000)
.build()
.unwrap();
let hash = tx.hash();
assert!(hash.starts_with("0x"));
assert_eq!(hash.len(), 66); }
#[test]
fn test_transaction_is_cross_chain() {
let tx = Transaction::builder()
.from(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.to(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.amount(1000)
.build()
.unwrap();
assert!(!tx.is_cross_chain());
}
#[test]
fn test_transaction_is_not_cross_chain() {
let tx = Transaction::builder()
.from(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.to(Address::evm("0x123456789abcdef123456789abcdef123456789a"))
.amount(1000)
.build()
.unwrap();
assert!(!tx.is_cross_chain()); }
#[test]
fn test_transaction_hash_determinism() {
let tx1 = Transaction::builder()
.from(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.to(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.amount(1000)
.build()
.unwrap();
let tx2 = tx1.clone();
assert_eq!(tx1.hash(), tx2.hash());
}
#[test]
fn test_transaction_hash_changes_with_nonce() {
let mut tx1 = Transaction::builder()
.from(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.to(Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"))
.amount(1000)
.build()
.unwrap();
let mut tx2 = tx1.clone();
tx1.nonce = Some(1);
tx2.nonce = Some(2);
assert_ne!(tx1.hash(), tx2.hash());
}
#[test]
fn test_transaction_result_serialization() {
let result = TransactionResult::new("0x123".to_string())
.with_status(TransactionStatus::Success)
.with_block_number(100)
.with_gas_used(21000);
let _serialized = serde_json::to_string(&result).unwrap();
assert_eq!(result.source_tx_hash, "0x123");
}
}