use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Currency {
ETH,
ERC20 {
contract_address: String,
decimals: u8,
},
}
impl Currency {
pub fn erc20(contract_address: impl Into<String>, decimals: u8) -> Self {
Self::ERC20 {
contract_address: contract_address.into(),
decimals,
}
}
pub fn usdt() -> Self {
Self::ERC20 {
contract_address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
decimals: 6, }
}
pub fn usdc() -> Self {
Self::ERC20 {
contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
decimals: 6, }
}
pub fn dai() -> Self {
Self::ERC20 {
contract_address: "0x6B175474E89094C44Da98b954EedeAC495271d0F".to_string(),
decimals: 18,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentRequest {
pub amount: Decimal,
pub currency: Currency,
pub recipient_address: String,
pub required_confirmations: u64,
pub timeout_seconds: Option<u64>,
}
impl PaymentRequest {
pub fn eth(
amount: Decimal,
recipient_address: impl Into<String>,
required_confirmations: u64,
) -> Self {
Self {
amount,
currency: Currency::ETH,
recipient_address: recipient_address.into(),
required_confirmations,
timeout_seconds: None,
}
}
pub fn token(
amount: Decimal,
contract_address: impl Into<String>,
decimals: u8,
recipient_address: impl Into<String>,
required_confirmations: u64,
) -> Self {
Self {
amount,
currency: Currency::erc20(contract_address, decimals),
recipient_address: recipient_address.into(),
required_confirmations,
timeout_seconds: None,
}
}
pub fn with_timeout(mut self, timeout_seconds: u64) -> Self {
self.timeout_seconds = Some(timeout_seconds);
self
}
pub fn is_expired(&self, created_at: DateTime<Utc>) -> bool {
if let Some(timeout) = self.timeout_seconds {
let elapsed = Utc::now().signed_duration_since(created_at);
elapsed.num_seconds() as u64 >= timeout
} else {
false
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PaymentStatus {
Pending,
Detected {
confirmations: u64,
tx_hash: String,
},
Confirmed {
tx_hash: String,
confirmations: u64,
},
Failed {
reason: String,
},
Expired,
}
impl PaymentStatus {
pub fn is_finalized(&self) -> bool {
matches!(
self,
PaymentStatus::Confirmed { .. }
| PaymentStatus::Failed { .. }
| PaymentStatus::Expired
)
}
pub fn is_successful(&self) -> bool {
matches!(self, PaymentStatus::Confirmed { .. })
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Payment {
pub id: Uuid,
pub request: PaymentRequest,
pub status: PaymentStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub metadata: serde_json::Value,
}
impl Payment {
pub fn new(request: PaymentRequest) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
request,
status: PaymentStatus::Pending,
created_at: now,
updated_at: now,
metadata: serde_json::Value::Null,
}
}
pub fn update_status(&mut self, status: PaymentStatus) {
self.status = status;
self.updated_at = Utc::now();
}
pub fn is_expired(&self) -> bool {
self.request.is_expired(self.created_at)
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = metadata;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_eth_payment_request() {
let request = PaymentRequest::eth(
Decimal::from_str("0.1").unwrap(),
"0x1234567890123456789012345678901234567890",
12,
);
assert_eq!(request.currency, Currency::ETH);
assert_eq!(request.required_confirmations, 12);
}
#[test]
fn test_token_payment_request() {
let request = PaymentRequest::token(
Decimal::from(100),
"0xcontract",
18,
"0x1234567890123456789012345678901234567890",
6,
);
match request.currency {
Currency::ERC20 {
ref contract_address,
decimals,
} => {
assert_eq!(contract_address, "0xcontract");
assert_eq!(decimals, 18);
}
_ => panic!("Expected ERC20 currency"),
}
}
#[test]
fn test_payment_creation() {
let request = PaymentRequest::eth(Decimal::from(1), "0xrecipient", 12);
let payment = Payment::new(request);
assert_eq!(payment.status, PaymentStatus::Pending);
assert!(!payment.is_expired());
}
#[test]
fn test_payment_status_finalized() {
let status = PaymentStatus::Pending;
assert!(!status.is_finalized());
let status = PaymentStatus::Confirmed {
tx_hash: "0xhash".to_string(),
confirmations: 15,
};
assert!(status.is_finalized());
assert!(status.is_successful());
}
}