Contractless/src/blocks/collateral.rs

212 lines
7.3 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)] // 67 bytes
pub struct UnsignedCollateralClaimTransaction {
pub txtype: u8, // 1 byte transaction type, should be 9
pub time: u32, // 4 bytes transaction timestamp
pub contract_hash: String, // 32 bytes hash of the loan contract
pub address: String, // 22 bytes claimant short address
pub txfee: u64, // 8 bytes transaction fee
}
#[derive(Debug, Serialize, Clone)] // 733 bytes
pub struct CollateralClaimTransaction {
pub unsigned_collateral_claim: UnsignedCollateralClaimTransaction, // 67 bytes
pub signature: String, // 666 bytes signature of the transaction hash
}
impl CollateralClaimTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + 32 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedCollateralClaimTransaction {
// Create an unsigned collateral-claim transaction.
pub async fn new(
txtype: u8,
time: u32,
contract_hash: &str,
address: &str,
txfee: u64,
) -> Self {
Self {
txtype,
time,
contract_hash: contract_hash.to_string(),
address: address.to_string(),
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, 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 claimant wallet.
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok(signature)
}
}
impl CollateralClaimTransaction {
pub async fn new(
unsigned_collateral_claim: UnsignedCollateralClaimTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
// Hash and sign the unsigned transaction.
let signature = unsigned_collateral_claim.hash_and_sign(private_key).await?;
// Return the complete collateral-claim transaction.
Ok(Self {
unsigned_collateral_claim,
signature,
})
}
// Load an existing collateral-claim transaction.
pub async fn load(
unsigned_collateral_claim: UnsignedCollateralClaimTransaction,
signature: &str,
) -> Self {
Self {
unsigned_collateral_claim,
signature: signature.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed collateral-claim transaction byte layout.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_collateral_claim.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_collateral_claim.time.to_le_bytes())
.await?;
cursor
.write_all(&decode(&self.unsigned_collateral_claim.contract_hash).unwrap())
.await?;
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_collateral_claim.address)
.ok_or_else(|| tokio::io::Error::other("Invalid collateral claimant short address"))?;
cursor.write_all(&address_bytes).await?;
cursor
.write_all(&self.unsigned_collateral_claim.txfee.to_le_bytes())
.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 collateral-claim bytes.
let mut cursor = Cursor::new(bytes);
// Decode timestamp and contract hash.
let time = cursor.read_u32_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 claimant short address, fee, 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 collateral claimant short address bytes")
})?;
let txfee = cursor.read_u64_le().await?;
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature_bytes).await?;
let signature = encode(&signature_bytes);
let unsigned_collateral_claim = UnsignedCollateralClaimTransaction {
txtype,
time,
contract_hash,
address,
txfee,
};
Ok(CollateralClaimTransaction {
unsigned_collateral_claim,
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 time = self.unsigned_collateral_claim.time as i32;
let fee = i64::try_from(self.unsigned_collateral_claim.txfee)
.map_err(|_| std::io::Error::other("Collateral fee exceeds i64 mempool limit"))?;
let address = &self.unsigned_collateral_claim.address;
let contract_hash = &self.unsigned_collateral_claim.contract_hash;
let hash = &self.unsigned_collateral_claim.hash().await;
let signature = &self.signature;
// Collateral-claim 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 collateral_claim (
time,
fee,
address,
contract_hash,
hash,
signature,
original
) VALUES (
$1, $2, $3, $4, $5, $6, $7
)
"#,
&[
&time,
&fee,
address,
contract_hash,
hash,
signature,
&original_data,
],
)
.await?;
Ok(())
}
}