use crate::common::binary_conversions::binary_to_string; use crate::common::skein::skein_256_hash_data; use crate::records::memory::mempool::db_client; 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)] // 122 bytes pub struct UnsignedLoanContractTransaction { pub txtype: u8, // 1 byte transaction type, should be 7 pub timestamp: u32, // 4 bytes contract creation timestamp pub loan_coin: String, // 15 bytes coin or token being loaned pub loan_amount: u64, // 8 bytes amount being loaned pub lender: String, // 22 bytes lender short address pub collateral: String, // 15 bytes token or NFT used as collateral pub collateral_amount: u64, // 8 bytes collateral token amount or 1 for NFT collateral pub borrower: String, // 22 bytes borrower short address pub payment_period: String, // 1 byte d, w, or m for days, weeks, or months pub payment_number: u8, // 1 byte total number of payments pub payment_amount: u64, // 8 bytes amount due for each payment pub grace_period: u8, // 1 byte missed payments before collateral claim is allowed pub max_late_value: u64, // 8 bytes max overdue value before collateral claim is allowed pub txfee: u64, // 8 bytes transaction fee paid by the lender } #[derive(Debug, Serialize, Clone)] // 1486 bytes pub struct LoanContractTransaction { pub unsigned_loan_contract: UnsignedLoanContractTransaction, // 122 bytes pub hash: String, // 32 bytes The hash of the transaction pub signature1: String, // 666 bytes The signature of lender for the hash pub signature2: String, // 666 bytes The signature of borrower for the hash } impl LoanContractTransaction { pub const BYTE_LENGTH: usize = 1 + 4 + 15 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 15 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 1 + 1 + 8 + 1 + 8 + 8 + 32 + Wallet::SIGNATURE_LENGTH + Wallet::SIGNATURE_LENGTH; } impl UnsignedLoanContractTransaction { // Create an unsigned loan-contract transaction. #[allow(clippy::too_many_arguments)] pub async fn new( txtype: u8, timestamp: u32, loan_coin: &str, loan_amount: u64, lender: &str, collateral: &str, collateral_amount: u64, borrower: &str, payment_period: &str, payment_number: u8, payment_amount: u64, grace_period: u8, max_late_value: u64, txfee: u64, ) -> Self { Self { txtype, timestamp, loan_coin: loan_coin.to_string(), loan_amount, lender: lender.to_string(), collateral: collateral.to_string(), collateral_amount, borrower: borrower.to_string(), payment_period: payment_period.to_string(), payment_number, payment_amount, grace_period, max_late_value, 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 current signer wallet. let signature = Wallet::sign_transaction(&hash, private_key).await; Ok((hash, signature)) } } impl LoanContractTransaction { // Add the next required signature to a loan-contract transaction. pub async fn new( &self, unsigned_loan_contract: UnsignedLoanContractTransaction, private_key: &str, ) -> Result> { // Hash and sign the unsigned transaction. let (hash, signature) = unsigned_loan_contract.hash_and_sign(private_key).await?; // The first signer fills signature1, and the second signer fills signature2. let mut signature1 = "".to_string(); let mut signature2 = "".to_string(); if self.signature1.is_empty() { signature1 = signature; } else { signature2 = signature; } // Return the loan contract with the newest signature included. Ok(Self { unsigned_loan_contract, hash, signature1, signature2, }) } // Load an existing loan-contract transaction. pub async fn load( unsigned_loan_contract: UnsignedLoanContractTransaction, hash: &str, signature1: &str, signature2: &str, ) -> Self { Self { unsigned_loan_contract, hash: hash.to_string(), signature1: signature1.to_string(), signature2: signature2.to_string(), } } pub async fn to_bytes(&self) -> tokio::io::Result> { // Serialize into the fixed loan-contract transaction byte layout. let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH); let mut cursor = Cursor::new(&mut buffer); cursor .write_all(&self.unsigned_loan_contract.txtype.to_le_bytes()) .await?; cursor .write_all(&self.unsigned_loan_contract.timestamp.to_le_bytes()) .await?; cursor .write_all(self.unsigned_loan_contract.loan_coin.as_bytes()) .await?; cursor .write_all(&self.unsigned_loan_contract.loan_amount.to_le_bytes()) .await?; let lender_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.lender) .ok_or_else(|| tokio::io::Error::other("Invalid lender short address"))?; cursor.write_all(&lender_bytes).await?; cursor .write_all(self.unsigned_loan_contract.collateral.as_bytes()) .await?; cursor .write_all(&self.unsigned_loan_contract.collateral_amount.to_le_bytes()) .await?; let borrower_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.borrower) .ok_or_else(|| tokio::io::Error::other("Invalid borrower short address"))?; cursor.write_all(&borrower_bytes).await?; cursor .write_all(self.unsigned_loan_contract.payment_period.as_bytes()) .await?; cursor .write_all(&self.unsigned_loan_contract.payment_number.to_le_bytes()) .await?; cursor .write_all(&self.unsigned_loan_contract.payment_amount.to_le_bytes()) .await?; cursor .write_all(&self.unsigned_loan_contract.grace_period.to_le_bytes()) .await?; cursor .write_all(&self.unsigned_loan_contract.max_late_value.to_le_bytes()) .await?; cursor .write_all(&self.unsigned_loan_contract.txfee.to_le_bytes()) .await?; cursor.write_all(&decode(&self.hash).unwrap()).await?; cursor.write_all(&decode(&self.signature1).unwrap()).await?; cursor.write_all(&decode(&self.signature2).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-contract bytes. let mut cursor = Cursor::new(bytes); // Decode timestamp and loan terms. let timestamp = cursor.read_u32_le().await?; let mut loan_coin_bytes = vec![0; 15]; cursor.read_exact(&mut loan_coin_bytes).await?; let loan_coin = binary_to_string(loan_coin_bytes); let loan_amount = cursor.read_u64_le().await?; // Decode lender short address. let mut lender_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut lender_bytes).await?; let lender = Wallet::bytes_to_short_address(&lender_bytes) .ok_or_else(|| tokio::io::Error::other("Invalid lender short address bytes"))?; // Decode collateral asset and amount. let mut collateral_bytes = vec![0; 15]; cursor.read_exact(&mut collateral_bytes).await?; let collateral = binary_to_string(collateral_bytes); let collateral_amount = cursor.read_u64_le().await?; // Decode borrower short address. let mut borrower_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut borrower_bytes).await?; let borrower = Wallet::bytes_to_short_address(&borrower_bytes) .ok_or_else(|| tokio::io::Error::other("Invalid borrower short address bytes"))?; // Decode payment schedule and late-payment rules. let mut payment_period_bytes = vec![0; 1]; cursor.read_exact(&mut payment_period_bytes).await?; let payment_period = binary_to_string(payment_period_bytes); let payment_number = cursor.read_u8().await?; let payment_amount = cursor.read_u64_le().await?; let grace_period = cursor.read_u8().await?; let max_late_value = cursor.read_u64_le().await?; // Decode fee, txid hash, and both signatures. 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 signature1_bytes = vec![0; Wallet::SIGNATURE_LENGTH]; cursor.read_exact(&mut signature1_bytes).await?; let signature1 = encode(&signature1_bytes); let mut signature2_bytes = vec![0; Wallet::SIGNATURE_LENGTH]; cursor.read_exact(&mut signature2_bytes).await?; let signature2 = encode(&signature2_bytes); // Rebuild the unsigned loan-contract payload. let unsigned_loan_contract = UnsignedLoanContractTransaction { txtype, timestamp, loan_coin, loan_amount, lender, collateral, collateral_amount, borrower, payment_period, payment_number, payment_amount, grace_period, max_late_value, txfee, }; // Wrap the rebuilt unsigned payload, txid hash, and signatures. Ok(LoanContractTransaction { unsigned_loan_contract, hash, signature1, signature2, }) } pub async fn add_to_memory(&self) -> Result<(), Box> { // Store original bytes so the mempool can rebuild the exact // transaction submitted by the wallets. let original_data = self.to_bytes().await?; // PostgreSQL uses signed integer types for these persisted fields. let fee = i64::try_from(self.unsigned_loan_contract.txfee) .map_err(|_| std::io::Error::other("Loan contract fee exceeds i64 mempool limit"))?; let time = self.unsigned_loan_contract.timestamp as i32; let loan_amount = i64::try_from(self.unsigned_loan_contract.loan_amount) .map_err(|_| std::io::Error::other("Loan amount exceeds i64 mempool limit"))?; let collateral_amount = i64::try_from(self.unsigned_loan_contract.collateral_amount) .map_err(|_| std::io::Error::other("Collateral amount exceeds i64 mempool limit"))?; let loan_coin = &self.unsigned_loan_contract.loan_coin; let lender = &self.unsigned_loan_contract.lender; let collateral = &self.unsigned_loan_contract.collateral; let borrower = &self.unsigned_loan_contract.borrower; let txid = &self.hash; let hash = self.unsigned_loan_contract.hash().await; let signature1 = &self.signature1; let signature2 = &self.signature2; // Loan contracts remain in the mempool table until mined or removed. let client_handle = db_client().await?; let client = client_handle.as_ref(); client .execute( r#" INSERT INTO loan_contract ( fee, time, loan_coin, loan_amount, lender, collateral, collateral_amount, borrower, txid, hash, signature1, signature2, original ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 ) "#, &[ &fee, &time, loan_coin, &loan_amount, lender, collateral, &collateral_amount, borrower, txid, &hash, signature1, signature2, &original_data, ], ) .await?; Ok(()) } }