Contractless/src/blocks/loan_payment.rs

250 lines
8.9 KiB
Rust

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)] // 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<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 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<Self, Box<dyn std::error::Error>> {
// 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<Vec<u8>> {
// 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<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-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<dyn std::error::Error + Send + Sync>> {
// 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_handle = db_client().await?;
let client = client_handle.as_ref();
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(())
}
}