use crate::common::skein::skein_256_hash_data; use crate::records::memory::mempool::DB; use crate::to_string; use crate::wallets::structures::Wallet; use crate::Cursor; use crate::Serialize; use crate::{decode, encode}; use crate::{AsyncReadExt, AsyncWriteExt}; #[derive(Debug, Serialize, Clone)] // 83 bytes pub struct UnsignedContractPaymentTransaction { pub txtype: u8, // 1 byte transaction type, should be 8 pub timestamp: u32, // 4 bytes payment timestamp pub payback_amount: u64, // 8 bytes amount of loaned token to pay back pub contract_hash: String, // 32 bytes hash of the loan contract pub address: String, // 22 bytes payer short address pub tip: u64, // 8 bytes miner tip paid in the loan token pub txfee: u64, // 8 bytes transaction fee } #[derive(Debug, Serialize, Clone)] // 781 bytes pub struct ContractPaymentTransaction { pub unsigned_contract_payment: UnsignedContractPaymentTransaction, // 83 bytes pub hash: String, // 32 bytes The hash of the transaction pub signature: String, // 666 bytes The signature of the transaction hash } impl ContractPaymentTransaction { pub const BYTE_LENGTH: usize = 1 + 4 + 8 + 32 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + 8 + 32 + Wallet::SIGNATURE_LENGTH; } impl UnsignedContractPaymentTransaction { // Create an unsigned loan-payment transaction. pub async fn new( txtype: u8, timestamp: u32, payback_amount: u64, contract_hash: &str, address: &str, tip: u64, txfee: u64, ) -> Self { Self { txtype, timestamp, payback_amount, contract_hash: contract_hash.to_string(), address: address.to_string(), tip, txfee, } } // Hash without signing for verification. pub async fn hash(&self) -> String { let serialized = to_string(self).unwrap(); skein_256_hash_data(&serialized) } // Hash the unsigned transaction and sign it. pub async fn hash_and_sign( &self, private_key: &str, ) -> Result<(String, String), Box> { // Serialize the unsigned transaction before hashing. let serialized = to_string(self)?; // Hash the serialized unsigned payload. let hash = skein_256_hash_data(&serialized); // Sign the transaction hash with the payer wallet. let signature = Wallet::sign_transaction(&hash, private_key).await; Ok((hash, signature)) } } impl ContractPaymentTransaction { pub async fn new( unsigned_contract_payment: UnsignedContractPaymentTransaction, private_key: &str, ) -> Result> { // Hash and sign the unsigned transaction. let (hash, signature) = unsigned_contract_payment.hash_and_sign(private_key).await?; // Return the complete loan-payment transaction with its txid hash. Ok(Self { unsigned_contract_payment, hash, signature, }) } // Load an existing loan-payment transaction. pub async fn load( unsigned_contract_payment: UnsignedContractPaymentTransaction, hash: &str, signature: &str, ) -> Self { Self { unsigned_contract_payment, hash: hash.to_string(), signature: signature.to_string(), } } pub async fn to_bytes(&self) -> tokio::io::Result> { // Serialize into the fixed loan-payment transaction byte layout. let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH); let mut cursor = Cursor::new(&mut buffer); cursor .write_all(&self.unsigned_contract_payment.txtype.to_le_bytes()) .await?; cursor .write_all(&self.unsigned_contract_payment.timestamp.to_le_bytes()) .await?; cursor .write_all(&self.unsigned_contract_payment.payback_amount.to_le_bytes()) .await?; cursor .write_all(&decode(&self.unsigned_contract_payment.contract_hash).unwrap()) .await?; let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_contract_payment.address) .ok_or_else(|| { tokio::io::Error::other("Invalid payer short address") })?; cursor.write_all(&address_bytes).await?; cursor .write_all(&self.unsigned_contract_payment.tip.to_le_bytes()) .await?; cursor .write_all(&self.unsigned_contract_payment.txfee.to_le_bytes()) .await?; cursor.write_all(&decode(&self.hash).unwrap()).await?; cursor.write_all(&decode(&self.signature).unwrap()).await?; Ok(buffer) } pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result { // The block parser already consumed the transaction type byte. if bytes.len() != Self::BYTE_LENGTH - 1 { return Err(tokio::io::Error::other("Invalid Byte Count", )); } // Read the remaining fixed-width loan-payment bytes. let mut cursor = Cursor::new(bytes); // Decode timestamp, payment amount, and contract hash. let timestamp = cursor.read_u32_le().await?; let payback_amount = cursor.read_u64_le().await?; let mut contract_hash_bytes = vec![0; 32]; cursor.read_exact(&mut contract_hash_bytes).await?; let contract_hash = encode(&contract_hash_bytes); // Decode payer short address, miner tip, fee, txid hash, and signature. let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut address_bytes).await?; let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| { tokio::io::Error::other("Invalid payer short address bytes", ) })?; let tip = cursor.read_u64_le().await?; let txfee = cursor.read_u64_le().await?; let mut hash_bytes = vec![0; 32]; cursor.read_exact(&mut hash_bytes).await?; let hash = encode(&hash_bytes); let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH]; cursor.read_exact(&mut signature_bytes).await?; let signature = encode(&signature_bytes); let unsigned_contract_payment = UnsignedContractPaymentTransaction { txtype, timestamp, payback_amount, contract_hash, address, tip, txfee, }; Ok(ContractPaymentTransaction { unsigned_contract_payment, hash, signature, }) } pub async fn add_to_memory(&self) -> Result<(), Box> { // Store original bytes so the mempool can rebuild the exact // transaction submitted by the wallet. let original_data = self.to_bytes().await?; // PostgreSQL uses signed integer types for these persisted fields. let fee = i64::try_from(self.unsigned_contract_payment.txfee) .map_err(|_| std::io::Error::other("Loan payment fee exceeds i64 mempool limit"))?; let time = self.unsigned_contract_payment.timestamp as i32; let payback_amount = i64::try_from(self.unsigned_contract_payment.payback_amount) .map_err(|_| std::io::Error::other("Loan payment amount exceeds i64 mempool limit"))?; let tip = i64::try_from(self.unsigned_contract_payment.tip) .map_err(|_| std::io::Error::other("Loan payment tip exceeds i64 mempool limit"))?; let contract_hash = &self.unsigned_contract_payment.contract_hash; let address = &self.unsigned_contract_payment.address; let txid = &self.hash; let hash = &self.unsigned_contract_payment.hash().await; let signature = &self.signature; // Loan-payment transactions remain in the mempool table until mined or removed. let client = DB.get().expect("DB not initialized"); client .execute( r#" INSERT INTO loan_payment ( fee, time, payback_amount, contract_hash, address, tip, txid, hash, signature, original ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 ) "#, &[ &fee, &time, &payback_amount, contract_hash, address, &tip, txid, hash, signature, &original_data, ], ) .await?; Ok(()) } }