375 lines
14 KiB
Rust
375 lines
14 KiB
Rust
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<dyn std::error::Error>> {
|
|
// 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<Self, Box<dyn std::error::Error>> {
|
|
// 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<Vec<u8>> {
|
|
// 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<Self> {
|
|
// 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<dyn std::error::Error + Send + Sync>> {
|
|
// 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(())
|
|
}
|
|
}
|