Contractless/src/blocks/loans.rs

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(())
}
}