rpc changes for asset reporting
This commit is contained in:
parent
fe83bd50dc
commit
61bc832ff4
|
|
@ -1,5 +1,7 @@
|
|||
use blockchain::blocks::loans::UnsignedLoanContractTransaction;
|
||||
use blockchain::common::asset_names::padded_normalized_asset_input;
|
||||
use blockchain::common::asset_names::{
|
||||
padded_loan_collateral_input_or_base, padded_normalized_asset_input,
|
||||
};
|
||||
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
|
||||
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use blockchain::common::types::LENDER_FEE;
|
||||
|
|
@ -9,7 +11,7 @@ use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
|
|||
use blockchain::wallets::structures::Wallet;
|
||||
use blockchain::File;
|
||||
use blockchain::{create_dir_all, AsyncWriteExt};
|
||||
use blockchain::{Local, LocalResult, NaiveDate, NaiveTime, TimeZone};
|
||||
use blockchain::{NaiveDate, NaiveTime, TimeZone, Utc};
|
||||
|
||||
fn normalize_asset_or_base(input: &str) -> Option<String> {
|
||||
let base_coin = block_extension_and_paths().1;
|
||||
|
|
@ -26,17 +28,11 @@ fn display_fee(value: u64) -> f64 {
|
|||
}
|
||||
|
||||
fn parse_start_date(input: &str) -> Result<u32, String> {
|
||||
// Loan start dates are entered as calendar dates and stored as local midnight timestamps.
|
||||
// Loan schedules use UTC dates so both parties and every node derive the same due dates.
|
||||
let date = NaiveDate::parse_from_str(input.trim(), "%Y-%m-%d")
|
||||
.map_err(|_| "Please enter the date in YYYY-MM-DD format.".to_string())?;
|
||||
let datetime = date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap());
|
||||
let timestamp = match Local.from_local_datetime(&datetime) {
|
||||
LocalResult::Single(dt) => dt.timestamp(),
|
||||
LocalResult::Ambiguous(dt, _) => dt.timestamp(),
|
||||
LocalResult::None => {
|
||||
return Err("Loan start date could not be resolved in local time.".to_string());
|
||||
}
|
||||
};
|
||||
let timestamp = Utc.from_utc_datetime(&datetime).timestamp();
|
||||
if timestamp < 0 || timestamp > u32::MAX as i64 {
|
||||
return Err("Loan start date is out of range.".to_string());
|
||||
}
|
||||
|
|
@ -129,8 +125,12 @@ async fn main() {
|
|||
let collateral_name =
|
||||
prompt_visible("What collateral coin, token, or NFT will you be accepting for this loan? ")
|
||||
.await;
|
||||
let Some(collateral) = padded_normalized_asset_input(collateral_name.trim()) else {
|
||||
println!("Collateral asset must normalize to 3-15 alphanumeric characters.");
|
||||
let base_coin = block_extension_and_paths().1;
|
||||
let Some(collateral) = padded_loan_collateral_input_or_base(collateral_name.trim(), &base_coin)
|
||||
else {
|
||||
println!(
|
||||
"Collateral must be the base coin, a 3-15 character asset, or a numbered NFT ending in _xxxxx."
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -46,10 +46,14 @@ async fn main() {
|
|||
prompt_visible("Please enter the coin or token you wish to transfer: ").await
|
||||
};
|
||||
let Some(coin) = normalize_asset_or_base(&coin_name) else {
|
||||
println!("Transfer asset must be the base coin or normalize to 3-15 alphanumeric characters.");
|
||||
println!(
|
||||
"Transfer asset must be the base coin or normalize to 3-15 alphanumeric characters."
|
||||
);
|
||||
return;
|
||||
};
|
||||
let is_base_coin = coin.trim().eq_ignore_ascii_case(block_extension_and_paths().1.trim());
|
||||
let is_base_coin = coin
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(block_extension_and_paths().1.trim());
|
||||
|
||||
let value_string = if args.len() > 2 {
|
||||
args[2].clone()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::common::asset_names::{loan_collateral_asset_name_or_base, LOAN_COLLATERAL_LENGTH};
|
||||
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::records::memory::mempool::{db_client, BASECOIN};
|
||||
use crate::to_string;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::Cursor;
|
||||
|
|
@ -8,14 +9,14 @@ use crate::Serialize;
|
|||
use crate::{decode, encode};
|
||||
use crate::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[derive(Debug, Serialize, Clone)] // 122 bytes
|
||||
#[derive(Debug, Serialize, Clone)] // 128 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: String, // 21 bytes token or numbered 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
|
||||
|
|
@ -26,9 +27,9 @@ pub struct UnsignedLoanContractTransaction {
|
|||
pub txfee: u64, // 8 bytes transaction fee paid by the lender
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)] // 1486 bytes
|
||||
#[derive(Debug, Serialize, Clone)] // 1492 bytes
|
||||
pub struct LoanContractTransaction {
|
||||
pub unsigned_loan_contract: UnsignedLoanContractTransaction, // 122 bytes
|
||||
pub unsigned_loan_contract: UnsignedLoanContractTransaction, // 128 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
|
||||
|
|
@ -40,7 +41,7 @@ impl LoanContractTransaction {
|
|||
+ 15
|
||||
+ 8
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ 15
|
||||
+ LOAN_COLLATERAL_LENGTH
|
||||
+ 8
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ 1
|
||||
|
|
@ -161,6 +162,14 @@ impl LoanContractTransaction {
|
|||
|
||||
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
|
||||
// Serialize into the fixed loan-contract transaction byte layout.
|
||||
if self.unsigned_loan_contract.loan_coin.len() != 15
|
||||
|| self.unsigned_loan_contract.collateral.len() != LOAN_COLLATERAL_LENGTH
|
||||
|| self.unsigned_loan_contract.payment_period.len() != 1
|
||||
{
|
||||
return Err(tokio::io::Error::other(
|
||||
"Invalid fixed-width loan transaction field",
|
||||
));
|
||||
}
|
||||
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
|
||||
let mut cursor = Cursor::new(&mut buffer);
|
||||
|
||||
|
|
@ -238,7 +247,7 @@ impl LoanContractTransaction {
|
|||
.ok_or_else(|| tokio::io::Error::other("Invalid lender short address bytes"))?;
|
||||
|
||||
// Decode collateral asset and amount.
|
||||
let mut collateral_bytes = vec![0; 15];
|
||||
let mut collateral_bytes = vec![0; LOAN_COLLATERAL_LENGTH];
|
||||
cursor.read_exact(&mut collateral_bytes).await?;
|
||||
let collateral = binary_to_string(collateral_bytes);
|
||||
|
||||
|
|
@ -318,7 +327,9 @@ impl LoanContractTransaction {
|
|||
|
||||
let loan_coin = &self.unsigned_loan_contract.loan_coin;
|
||||
let lender = &self.unsigned_loan_contract.lender;
|
||||
let collateral = &self.unsigned_loan_contract.collateral;
|
||||
let collateral =
|
||||
loan_collateral_asset_name_or_base(&self.unsigned_loan_contract.collateral, &BASECOIN)
|
||||
.ok_or_else(|| std::io::Error::other("Invalid loan collateral asset"))?;
|
||||
let borrower = &self.unsigned_loan_contract.borrower;
|
||||
let txid = &self.hash;
|
||||
let hash = self.unsigned_loan_contract.hash().await;
|
||||
|
|
@ -357,7 +368,7 @@ impl LoanContractTransaction {
|
|||
loan_coin,
|
||||
&loan_amount,
|
||||
lender,
|
||||
collateral,
|
||||
&collateral,
|
||||
&collateral_amount,
|
||||
borrower,
|
||||
txid,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use crate::common::types::COIN_LENGTH;
|
||||
|
||||
pub const LOAN_COLLATERAL_LENGTH: usize = 21;
|
||||
|
||||
pub fn normalize_asset_identifier(input: &str) -> String {
|
||||
input
|
||||
.chars()
|
||||
|
|
@ -36,3 +38,104 @@ pub fn is_canonical_padded_asset_or_base(input: &str, base_coin: &str) -> bool {
|
|||
input.trim().eq_ignore_ascii_case(base_coin.trim())
|
||||
|| is_canonical_padded_asset_identifier(input)
|
||||
}
|
||||
|
||||
pub fn loan_collateral_asset_name(input: &str) -> Option<String> {
|
||||
let trimmed = input.trim();
|
||||
if let Some((base, series)) = trimmed.rsplit_once('_') {
|
||||
if base.len() < 3
|
||||
|| base.len() > COIN_LENGTH
|
||||
|| series.is_empty()
|
||||
|| series.len() > 5
|
||||
|| !base
|
||||
.chars()
|
||||
.all(|character| character.is_ascii_alphanumeric())
|
||||
|| !series.chars().all(|character| character.is_ascii_digit())
|
||||
|| series.parse::<u32>().ok()? == 0
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let normalized_base = normalize_asset_identifier(base);
|
||||
return Some(format!("{normalized_base}_{series}"));
|
||||
}
|
||||
|
||||
padded_normalized_asset_identifier(trimmed)
|
||||
}
|
||||
|
||||
pub fn padded_loan_collateral_input(input: &str) -> Option<String> {
|
||||
let asset_name = loan_collateral_asset_name(input)?;
|
||||
if asset_name.trim_end().len() > LOAN_COLLATERAL_LENGTH {
|
||||
return None;
|
||||
}
|
||||
Some(format!(
|
||||
"{:<LOAN_COLLATERAL_LENGTH$}",
|
||||
asset_name.trim_end()
|
||||
))
|
||||
}
|
||||
|
||||
pub fn is_canonical_loan_collateral(input: &str) -> bool {
|
||||
input.len() == LOAN_COLLATERAL_LENGTH
|
||||
&& padded_loan_collateral_input(input).is_some_and(|canonical| canonical == input)
|
||||
}
|
||||
|
||||
pub fn loan_collateral_asset_name_or_base(input: &str, base_coin: &str) -> Option<String> {
|
||||
if input.trim().eq_ignore_ascii_case(base_coin.trim()) {
|
||||
return Some(base_coin.to_string());
|
||||
}
|
||||
loan_collateral_asset_name(input)
|
||||
}
|
||||
|
||||
pub fn padded_loan_collateral_input_or_base(input: &str, base_coin: &str) -> Option<String> {
|
||||
if input.trim().eq_ignore_ascii_case(base_coin.trim()) {
|
||||
return Some(format!("{:<LOAN_COLLATERAL_LENGTH$}", base_coin.trim()));
|
||||
}
|
||||
padded_loan_collateral_input(input)
|
||||
}
|
||||
|
||||
pub fn is_canonical_loan_collateral_or_base(input: &str, base_coin: &str) -> bool {
|
||||
input.len() == LOAN_COLLATERAL_LENGTH
|
||||
&& padded_loan_collateral_input_or_base(input, base_coin)
|
||||
.is_some_and(|canonical| canonical == input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
loan_collateral_asset_name, loan_collateral_asset_name_or_base,
|
||||
padded_loan_collateral_input, padded_loan_collateral_input_or_base, LOAN_COLLATERAL_LENGTH,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn normal_collateral_uses_a_21_byte_wire_field_and_15_byte_asset_key() {
|
||||
let wire = padded_loan_collateral_input("testcoin").unwrap();
|
||||
assert_eq!(wire.len(), LOAN_COLLATERAL_LENGTH);
|
||||
assert_eq!(
|
||||
loan_collateral_asset_name(&wire).unwrap(),
|
||||
"testcoin "
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbered_nft_collateral_keeps_its_series_suffix() {
|
||||
let wire = padded_loan_collateral_input("testnft_12345").unwrap();
|
||||
assert_eq!(wire.len(), LOAN_COLLATERAL_LENGTH);
|
||||
assert_eq!(loan_collateral_asset_name(&wire).unwrap(), "testnft_12345");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbered_nft_suffix_is_limited_to_five_digits() {
|
||||
assert!(padded_loan_collateral_input("testnft_123456").is_none());
|
||||
assert!(padded_loan_collateral_input("testnft_0").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_coin_collateral_preserves_the_network_symbol() {
|
||||
let base = "CLTC ";
|
||||
let wire = padded_loan_collateral_input_or_base("cltc", base).unwrap();
|
||||
assert_eq!(wire, "CLTC ");
|
||||
assert_eq!(
|
||||
loan_collateral_asset_name_or_base(&wire, base).unwrap(),
|
||||
base
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
use chrono::{Datelike, Duration, NaiveDate, TimeZone, Utc};
|
||||
|
||||
fn last_day_of_month(year: i32, month: u32) -> Result<u32, String> {
|
||||
let (next_year, next_month) = if month == 12 {
|
||||
(year + 1, 1)
|
||||
} else {
|
||||
(year, month + 1)
|
||||
};
|
||||
let next_month_start = NaiveDate::from_ymd_opt(next_year, next_month, 1)
|
||||
.ok_or_else(|| "Loan payment date is out of range.".to_string())?;
|
||||
Ok((next_month_start - Duration::days(1)).day())
|
||||
}
|
||||
|
||||
fn monthly_due_date(start: NaiveDate, payment_index: u32) -> Result<NaiveDate, String> {
|
||||
let start_month = start.year() as i64 * 12 + start.month0() as i64;
|
||||
let target_month = start_month
|
||||
.checked_add(payment_index as i64)
|
||||
.ok_or_else(|| "Loan payment month overflowed.".to_string())?;
|
||||
let year = i32::try_from(target_month.div_euclid(12))
|
||||
.map_err(|_| "Loan payment year is out of range.".to_string())?;
|
||||
let month = target_month.rem_euclid(12) as u32 + 1;
|
||||
let day = start.day().min(last_day_of_month(year, month)?);
|
||||
NaiveDate::from_ymd_opt(year, month, day)
|
||||
.ok_or_else(|| "Loan payment date is out of range.".to_string())
|
||||
}
|
||||
|
||||
fn due_date(start: NaiveDate, period: &str, payment_index: u32) -> Result<NaiveDate, String> {
|
||||
match period.trim() {
|
||||
"d" => start
|
||||
.checked_add_signed(Duration::days(payment_index as i64))
|
||||
.ok_or_else(|| "Daily loan payment date overflowed.".to_string()),
|
||||
"w" => start
|
||||
.checked_add_signed(Duration::days(payment_index as i64 * 7))
|
||||
.ok_or_else(|| "Weekly loan payment date overflowed.".to_string()),
|
||||
"m" => monthly_due_date(start, payment_index),
|
||||
_ => Err("Loan payment period must be d, w, or m.".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overdue_payment_count(
|
||||
start_timestamp: u32,
|
||||
period: &str,
|
||||
payment_number: u8,
|
||||
as_of_timestamp: u32,
|
||||
) -> Result<usize, String> {
|
||||
let start = Utc
|
||||
.timestamp_opt(start_timestamp as i64, 0)
|
||||
.single()
|
||||
.ok_or_else(|| "Invalid loan start timestamp.".to_string())?
|
||||
.date_naive();
|
||||
|
||||
let mut overdue = 0usize;
|
||||
for payment_index in 1..=payment_number as u32 {
|
||||
let due = due_date(start, period, payment_index)?;
|
||||
let overdue_at = due
|
||||
.checked_add_signed(Duration::days(1))
|
||||
.and_then(|date| date.and_hms_opt(0, 0, 0))
|
||||
.ok_or_else(|| "Loan overdue timestamp overflowed.".to_string())?
|
||||
.and_utc()
|
||||
.timestamp();
|
||||
|
||||
if as_of_timestamp as i64 >= overdue_at {
|
||||
overdue += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(overdue)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::overdue_payment_count;
|
||||
use chrono::{TimeZone, Utc};
|
||||
|
||||
fn timestamp(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> u32 {
|
||||
Utc.with_ymd_and_hms(year, month, day, hour, minute, second)
|
||||
.single()
|
||||
.unwrap()
|
||||
.timestamp() as u32
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daily_payment_is_not_overdue_until_the_following_midnight() {
|
||||
let start = timestamp(2026, 3, 15, 12, 0, 0);
|
||||
assert_eq!(
|
||||
overdue_payment_count(start, "d", 10, timestamp(2026, 3, 16, 23, 59, 59)).unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
overdue_payment_count(start, "d", 10, timestamp(2026, 3, 17, 0, 0, 0)).unwrap(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weekly_payment_uses_the_full_due_date() {
|
||||
let start = timestamp(2026, 3, 15, 0, 0, 0);
|
||||
assert_eq!(
|
||||
overdue_payment_count(start, "w", 10, timestamp(2026, 3, 22, 23, 59, 59)).unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
overdue_payment_count(start, "w", 10, timestamp(2026, 3, 23, 0, 0, 0)).unwrap(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monthly_schedule_is_anchored_to_the_original_calendar_day() {
|
||||
let start = timestamp(2026, 1, 31, 0, 0, 0);
|
||||
assert_eq!(
|
||||
overdue_payment_count(start, "m", 10, timestamp(2026, 2, 28, 23, 59, 59)).unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
overdue_payment_count(start, "m", 10, timestamp(2026, 3, 1, 0, 0, 0)).unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
overdue_payment_count(start, "m", 10, timestamp(2026, 4, 1, 0, 0, 0)).unwrap(),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monthly_schedule_handles_leap_year_february() {
|
||||
let start = timestamp(2024, 1, 31, 0, 0, 0);
|
||||
assert_eq!(
|
||||
overdue_payment_count(start, "m", 10, timestamp(2024, 2, 29, 23, 59, 59)).unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
overdue_payment_count(start, "m", 10, timestamp(2024, 3, 1, 0, 0, 0)).unwrap(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn future_start_dates_have_no_overdue_payments() {
|
||||
let start = timestamp(2026, 6, 1, 0, 0, 0);
|
||||
assert_eq!(
|
||||
overdue_payment_count(start, "d", 10, timestamp(2026, 5, 1, 0, 0, 0)).unwrap(),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
pub mod binary_conversions;
|
||||
pub mod asset_names;
|
||||
pub mod binary_conversions;
|
||||
pub mod check_genesis;
|
||||
pub mod cli_prompts;
|
||||
pub mod loan_schedule;
|
||||
pub mod network_paths_and_settings;
|
||||
pub mod network_startup;
|
||||
pub mod nft_assets;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use crate::blocks::swap::SwapTransaction;
|
|||
use crate::blocks::token::CreateTokenTransaction;
|
||||
use crate::blocks::transfer::TransferTransaction;
|
||||
use crate::blocks::vanity::VanityAddressTransaction;
|
||||
use crate::common::asset_names::loan_collateral_asset_name_or_base;
|
||||
use crate::common::nft_assets::nft_asset_name;
|
||||
use crate::common::types::Transaction;
|
||||
use crate::decode;
|
||||
|
|
@ -148,6 +149,9 @@ pub async fn restore_swap(transaction: &SwapTransaction, db: &Db) {
|
|||
|
||||
pub async fn restore_loan_creation(transaction: &LoanContractTransaction, db: &Db) {
|
||||
let loan = &transaction.unsigned_loan_contract;
|
||||
let Some(collateral) = loan_collateral_asset_name_or_base(&loan.collateral, &BASECOIN) else {
|
||||
return;
|
||||
};
|
||||
let lender_spendable = balance_checkup(
|
||||
db,
|
||||
loan.loan_amount,
|
||||
|
|
@ -156,14 +160,8 @@ pub async fn restore_loan_creation(transaction: &LoanContractTransaction, db: &D
|
|||
&loan.lender,
|
||||
)
|
||||
.await;
|
||||
let borrower_spendable = balance_checkup(
|
||||
db,
|
||||
loan.collateral_amount,
|
||||
0,
|
||||
loan.collateral.clone(),
|
||||
&loan.borrower,
|
||||
)
|
||||
.await;
|
||||
let borrower_spendable =
|
||||
balance_checkup(db, loan.collateral_amount, 0, collateral, &loan.borrower).await;
|
||||
let signatures = vec![
|
||||
transaction.signature1.clone(),
|
||||
transaction.signature2.clone(),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
use crate::blocks::collateral::CollateralClaimTransaction;
|
||||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::common::asset_names::loan_collateral_asset_name_or_base;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::decode;
|
||||
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
|
||||
use crate::records::memory::mempool::BASECOIN;
|
||||
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
|
||||
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
|
||||
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
|
||||
|
|
@ -54,7 +56,9 @@ pub async fn undo_collateral_transaction(
|
|||
}
|
||||
};
|
||||
|
||||
let collateral = loan_tx.unsigned_loan_contract.collateral;
|
||||
let collateral =
|
||||
loan_collateral_asset_name_or_base(&loan_tx.unsigned_loan_contract.collateral, &BASECOIN)
|
||||
.ok_or_else(|| "Invalid loan collateral asset.".to_string())?;
|
||||
let collateral_amount = loan_tx.unsigned_loan_contract.collateral_amount;
|
||||
let collateral_holding = format!(
|
||||
"collateral_{}",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::common::asset_names::loan_collateral_asset_name_or_base;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::decode;
|
||||
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
|
||||
use crate::records::memory::mempool::BASECOIN;
|
||||
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
|
||||
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
|
||||
use crate::sled::Db;
|
||||
|
|
@ -26,12 +28,22 @@ pub async fn undo_loan_creation_transaction(
|
|||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (txfee, loan_coin, loan_amount, lender, collateral, collateral_amount, borrower, hash) = (
|
||||
let collateral = loan_collateral_asset_name_or_base(
|
||||
&transaction.unsigned_loan_contract.collateral,
|
||||
&BASECOIN,
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
transaction
|
||||
.unsigned_loan_contract
|
||||
.collateral
|
||||
.trim()
|
||||
.to_string()
|
||||
});
|
||||
let (txfee, loan_coin, loan_amount, lender, collateral_amount, borrower, hash) = (
|
||||
&transaction.unsigned_loan_contract.txfee,
|
||||
&transaction.unsigned_loan_contract.loan_coin.clone(),
|
||||
&transaction.unsigned_loan_contract.loan_amount,
|
||||
&transaction.unsigned_loan_contract.lender.clone(),
|
||||
&transaction.unsigned_loan_contract.collateral.clone(),
|
||||
&transaction.unsigned_loan_contract.collateral_amount,
|
||||
&transaction.unsigned_loan_contract.borrower.clone(),
|
||||
&transaction.hash.clone(),
|
||||
|
|
@ -55,14 +67,14 @@ pub async fn undo_loan_creation_transaction(
|
|||
db,
|
||||
&collateral_wallet,
|
||||
*collateral_amount,
|
||||
collateral,
|
||||
&collateral,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
borrower,
|
||||
*collateral_amount,
|
||||
collateral,
|
||||
&collateral,
|
||||
operand_addition,
|
||||
);
|
||||
|
||||
|
|
@ -86,7 +98,7 @@ pub async fn undo_loan_creation_transaction(
|
|||
.contains_key(collateral.as_bytes())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let _ = remove_nft_history_entry(db, collateral, &hash_binary);
|
||||
let _ = remove_nft_history_entry(db, &collateral, &hash_binary);
|
||||
}
|
||||
if nft_tree.contains_key(loan_coin.as_bytes()).unwrap_or(false) {
|
||||
let _ = remove_nft_history_entry(db, loan_coin, &hash_binary);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use crate::blocks::marketing::MarketingTransaction;
|
|||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::decode;
|
||||
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
|
||||
use crate::records::record_chain::marketing_campaign_index::remove_marketing_campaign_transaction_index;
|
||||
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
|
||||
use crate::sled::Db;
|
||||
|
||||
|
|
@ -42,6 +43,12 @@ pub async fn undo_marketing_transaction(
|
|||
|
||||
let hash = decode(&transaction.unsigned_marketing.hash().await).unwrap();
|
||||
let _ = remove_wallet_transaction_index(db, &[advertiser, mining_receiver], &hash);
|
||||
let _ = remove_marketing_campaign_transaction_index(
|
||||
db,
|
||||
advertiser,
|
||||
transaction.unsigned_marketing.campaign,
|
||||
&hash,
|
||||
);
|
||||
|
||||
// Remove the marketing transaction lookup from the txid tree.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::common::asset_names::loan_collateral_asset_name_or_base;
|
||||
use crate::common::binary_conversions::binary_to_string;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::common::nft_assets::{nft_asset_name, nft_asset_parts};
|
||||
|
|
@ -294,10 +295,17 @@ async fn resolve_collateral_details(db: &Db, contract_hash: &str) -> Result<(Vec
|
|||
return Ok((Vec::new(), 0));
|
||||
}
|
||||
match LoanContractTransaction::from_bytes(7, &bytes[1..]).await {
|
||||
Ok(loan) => Ok((
|
||||
loan.unsigned_loan_contract.collateral.as_bytes().to_vec(),
|
||||
Ok(loan) => {
|
||||
let collateral = loan_collateral_asset_name_or_base(
|
||||
&loan.unsigned_loan_contract.collateral,
|
||||
&BASECOIN,
|
||||
)
|
||||
.unwrap_or_else(|| loan.unsigned_loan_contract.collateral.trim().to_string());
|
||||
Ok((
|
||||
collateral.into_bytes(),
|
||||
loan.unsigned_loan_contract.collateral_amount as i64,
|
||||
)),
|
||||
))
|
||||
}
|
||||
Err(_) => Ok((Vec::new(), 0)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ pub async fn setup_mempool() -> Result<()> {
|
|||
loan_coin VARCHAR(15),
|
||||
loan_amount BIGINT NOT NULL,
|
||||
lender TEXT NOT NULL,
|
||||
collateral VARCHAR(15),
|
||||
collateral VARCHAR(21),
|
||||
collateral_amount BIGINT NOT NULL,
|
||||
borrower TEXT NOT NULL,
|
||||
txid VARCHAR(64) NOT NULL,
|
||||
|
|
@ -307,6 +307,7 @@ pub async fn setup_mempool() -> Result<()> {
|
|||
ALTER TABLE swap ALTER COLUMN signature2 TYPE TEXT;
|
||||
ALTER TABLE loan_contract ALTER COLUMN lender TYPE TEXT;
|
||||
ALTER TABLE loan_contract ALTER COLUMN borrower TYPE TEXT;
|
||||
ALTER TABLE loan_contract ALTER COLUMN collateral TYPE VARCHAR(21);
|
||||
ALTER TABLE loan_contract ALTER COLUMN signature1 TYPE TEXT;
|
||||
ALTER TABLE loan_contract ALTER COLUMN signature2 TYPE TEXT;
|
||||
ALTER TABLE loan_payment ALTER COLUMN address TYPE TEXT;
|
||||
|
|
|
|||
|
|
@ -765,6 +765,8 @@ pub async fn apply_selected_transaction_math(
|
|||
} => {
|
||||
// Loan creation moves the loan asset to the borrower and locks
|
||||
// collateral under a contract-specific holding key.
|
||||
let collateral = loan_collateral_asset_name_or_base(collateral, &BASECOIN)
|
||||
.ok_or_else(|| anyhow!("Invalid loan collateral asset"))?;
|
||||
add_balance_change(db, &mut balance_changes, lender, &BASECOIN, -*fee);
|
||||
add_balance_change_bytes(
|
||||
&mut balance_changes,
|
||||
|
|
@ -778,7 +780,7 @@ pub async fn apply_selected_transaction_math(
|
|||
db,
|
||||
&mut balance_changes,
|
||||
borrower,
|
||||
collateral,
|
||||
&collateral,
|
||||
-*collateral_amount,
|
||||
);
|
||||
|
||||
|
|
@ -787,7 +789,7 @@ pub async fn apply_selected_transaction_math(
|
|||
db,
|
||||
&mut balance_changes,
|
||||
&collateral_holding,
|
||||
collateral,
|
||||
&collateral,
|
||||
*collateral_amount,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::blocks::collateral::CollateralClaimTransaction;
|
||||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::common::asset_names::loan_collateral_asset_name_or_base;
|
||||
use crate::decode;
|
||||
use crate::records::memory::mempool::BASECOIN;
|
||||
use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects};
|
||||
|
|
@ -58,7 +59,9 @@ pub async fn process_collateral(
|
|||
}
|
||||
};
|
||||
|
||||
let collateral = loan_tx.unsigned_loan_contract.collateral;
|
||||
let collateral =
|
||||
loan_collateral_asset_name_or_base(&loan_tx.unsigned_loan_contract.collateral, &BASECOIN)
|
||||
.ok_or_else(|| "Invalid loan collateral asset.".to_string())?;
|
||||
let collateral_amount = loan_tx.unsigned_loan_contract.collateral_amount;
|
||||
let collateral_holding = format!(
|
||||
"collateral_{}",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::common::asset_names::loan_collateral_asset_name_or_base;
|
||||
use crate::decode;
|
||||
use crate::records::memory::mempool::BASECOIN;
|
||||
use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects};
|
||||
|
|
@ -25,6 +26,11 @@ pub async fn process_lender(
|
|||
// the loan disbursement and collateral-holding balance changes.
|
||||
let txhash = transaction.unsigned_loan_contract.hash().await;
|
||||
let txhash_bytes = decode(&txhash).map_err(|e| format!("Error decoding hex: {e}"))?;
|
||||
let collateral = loan_collateral_asset_name_or_base(
|
||||
&transaction.unsigned_loan_contract.collateral,
|
||||
&BASECOIN,
|
||||
)
|
||||
.ok_or_else(|| "Invalid loan collateral asset.".to_string())?;
|
||||
let transaction_bytes = match transaction.to_bytes().await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => return Err(e.to_string()),
|
||||
|
|
@ -60,14 +66,14 @@ pub async fn process_lender(
|
|||
pending_effects.add_balance(
|
||||
&transaction.unsigned_loan_contract.borrower,
|
||||
transaction.unsigned_loan_contract.collateral_amount,
|
||||
&transaction.unsigned_loan_contract.collateral,
|
||||
&collateral,
|
||||
BalanceOperand::Subtraction,
|
||||
);
|
||||
let collateral_holding = format!("collateral_{txhash}");
|
||||
pending_effects.add_balance(
|
||||
&collateral_holding,
|
||||
transaction.unsigned_loan_contract.collateral_amount,
|
||||
&transaction.unsigned_loan_contract.collateral,
|
||||
&collateral,
|
||||
BalanceOperand::Addition,
|
||||
);
|
||||
|
||||
|
|
@ -91,11 +97,7 @@ pub async fn process_lender(
|
|||
pending_effects.append_tree_if_key_exists(
|
||||
"nfts",
|
||||
"nft_history",
|
||||
transaction
|
||||
.unsigned_loan_contract
|
||||
.collateral
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
collateral.as_bytes().to_vec(),
|
||||
txhash_bytes.clone(),
|
||||
);
|
||||
pending_effects.append_tree_if_key_exists(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
use crate::records::record_chain::pending_effects::PendingEffects;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
|
||||
pub const MARKETING_CAMPAIGN_INDEX_TREE: &str = "marketing_campaign_index";
|
||||
|
||||
fn marketing_campaign_index_key(
|
||||
advertiser: &str,
|
||||
campaign: u64,
|
||||
block_height: u32,
|
||||
entry_index: u32,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let advertiser_bytes = Wallet::short_address_to_bytes(advertiser)
|
||||
.ok_or_else(|| format!("Invalid marketing advertiser address: {advertiser}"))?;
|
||||
let mut key = Vec::with_capacity(Wallet::SHORT_ADDRESS_BYTES_LENGTH + 16);
|
||||
key.extend_from_slice(&advertiser_bytes);
|
||||
key.extend_from_slice(&campaign.to_be_bytes());
|
||||
key.extend_from_slice(&block_height.to_be_bytes());
|
||||
key.extend_from_slice(&entry_index.to_be_bytes());
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub fn index_marketing_campaign_transaction(
|
||||
pending_effects: &mut PendingEffects,
|
||||
advertiser: &str,
|
||||
campaign: u64,
|
||||
block_height: u32,
|
||||
entry_index: u32,
|
||||
txid: &[u8],
|
||||
) -> Result<(), String> {
|
||||
let key = marketing_campaign_index_key(advertiser, campaign, block_height, entry_index)?;
|
||||
pending_effects.set_tree(MARKETING_CAMPAIGN_INDEX_TREE, key, txid.to_vec());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn txid_block_height(db: &Db, txid: &[u8]) -> Result<Option<u32>, String> {
|
||||
let tree = db
|
||||
.open_tree("txid")
|
||||
.map_err(|err| format!("Failed to open txid tree: {err}"))?;
|
||||
let Some(value) = tree
|
||||
.get(txid)
|
||||
.map_err(|err| format!("Failed to read txid tree: {err}"))?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let value = String::from_utf8_lossy(&value);
|
||||
let Some((block_height, _entry_index)) = value.split_once(':') else {
|
||||
return Err("Invalid txid location value.".to_string());
|
||||
};
|
||||
block_height
|
||||
.parse::<u32>()
|
||||
.map(Some)
|
||||
.map_err(|err| format!("Invalid txid block height: {err}"))
|
||||
}
|
||||
|
||||
pub fn remove_marketing_campaign_transaction_index(
|
||||
db: &Db,
|
||||
advertiser: &str,
|
||||
campaign: u64,
|
||||
txid: &[u8],
|
||||
) -> Result<(), String> {
|
||||
let Some(block_height) = txid_block_height(db, txid)? else {
|
||||
return Ok(());
|
||||
};
|
||||
let advertiser_bytes = Wallet::short_address_to_bytes(advertiser)
|
||||
.ok_or_else(|| format!("Invalid marketing advertiser address: {advertiser}"))?;
|
||||
let mut prefix = Vec::with_capacity(Wallet::SHORT_ADDRESS_BYTES_LENGTH + 12);
|
||||
prefix.extend_from_slice(&advertiser_bytes);
|
||||
prefix.extend_from_slice(&campaign.to_be_bytes());
|
||||
prefix.extend_from_slice(&block_height.to_be_bytes());
|
||||
|
||||
let tree = db
|
||||
.open_tree(MARKETING_CAMPAIGN_INDEX_TREE)
|
||||
.map_err(|err| format!("Failed to open marketing campaign index: {err}"))?;
|
||||
let keys = tree
|
||||
.scan_prefix(prefix)
|
||||
.filter_map(|entry| match entry {
|
||||
Ok((key, value)) if value.as_ref() == txid => Some(key.to_vec()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for key in keys {
|
||||
tree.remove(key)
|
||||
.map_err(|err| format!("Failed to remove marketing campaign index: {err}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::blocks::marketing::MarketingTransaction;
|
||||
use crate::decode;
|
||||
use crate::records::memory::mempool::BASECOIN;
|
||||
use crate::records::record_chain::marketing_campaign_index::index_marketing_campaign_transaction;
|
||||
use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects};
|
||||
use crate::records::record_chain::wallet_tx_index::index_wallet_transaction;
|
||||
use crate::sled::Db;
|
||||
|
|
@ -59,5 +60,13 @@ pub async fn process_marketing(
|
|||
**index as u32,
|
||||
&txhash_bytes,
|
||||
)?;
|
||||
index_marketing_campaign_transaction(
|
||||
pending_effects,
|
||||
&transaction.unsigned_marketing.advertiser,
|
||||
transaction.unsigned_marketing.campaign,
|
||||
block_header_number,
|
||||
**index as u32,
|
||||
&txhash_bytes,
|
||||
)?;
|
||||
Ok(binary_data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ pub mod genesis_tx;
|
|||
pub mod header_number;
|
||||
pub mod issue_token_tx;
|
||||
pub mod lender_tx;
|
||||
pub mod marketing_campaign_index;
|
||||
pub mod marketing_tx;
|
||||
pub mod nft_provenance;
|
||||
pub mod nft_tx;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ pub const RPC_WALLET_REGISTRATION_STATUS: u8 = 43;
|
|||
pub const RPC_ADDRESS_HISTORY: u8 = 44;
|
||||
pub const RPC_VANITY_OWNER_LOOKUP: u8 = 45;
|
||||
pub const RPC_LATEST_ADDRESS_TRANSACTIONS: u8 = 46;
|
||||
pub const RPC_MARKETING_CAMPAIGN_HISTORY: u8 = 47;
|
||||
pub const RPC_TOKEN_CATALOG: u8 = 48;
|
||||
pub const RPC_REPLY: u8 = 255;
|
||||
pub const MAX_RPC_REPLY_BYTES: usize = 64 * 1024 * 1024;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
use crate::blocks::collateral::CollateralClaimTransaction;
|
||||
use crate::blocks::loan_payment::ContractPaymentTransaction;
|
||||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::common::loan_schedule::overdue_payment_count;
|
||||
use crate::encode;
|
||||
use crate::records::memory::mempool::get_pending_payments_for_contract;
|
||||
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::{DateTime, Datelike, Local, TimeZone, Utc};
|
||||
use crate::{Local, TimeZone, Utc};
|
||||
|
||||
fn format_amount(value: u64) -> f64 {
|
||||
// Contract RPC output presents coin amounts as user-facing decimal values.
|
||||
|
|
@ -21,40 +23,6 @@ fn format_local_date(timestamp: u32) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn time_passed(start_time: u32, period: &str) -> usize {
|
||||
// Contract summaries reuse the same elapsed-period logic as the
|
||||
// collateral checks to compute delinquency and payment schedule state.
|
||||
let start_datetime = Utc
|
||||
.timestamp_opt(start_time as i64, 0)
|
||||
.single()
|
||||
.expect("Invalid start timestamp");
|
||||
let current_timestamp = Utc::now().timestamp() as u32;
|
||||
let current_datetime = Utc
|
||||
.timestamp_opt(current_timestamp as i64, 0)
|
||||
.single()
|
||||
.expect("Invalid current timestamp");
|
||||
let duration = current_datetime - start_datetime;
|
||||
|
||||
match period {
|
||||
"d" => duration.num_days() as usize,
|
||||
"w" => (duration.num_days() / 7) as usize,
|
||||
"m" => calculate_months_between(start_datetime, current_datetime),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_months_between(start: DateTime<Utc>, end: DateTime<Utc>) -> usize {
|
||||
// Monthly payment periods are calendar based, not a fixed number of
|
||||
// seconds, so use year/month fields directly.
|
||||
let start_date = start.date_naive();
|
||||
let end_date = end.date_naive();
|
||||
|
||||
let years_diff = end_date.year() - start_date.year();
|
||||
let months_diff = end_date.month() as i32 - start_date.month() as i32;
|
||||
|
||||
(years_diff * 12 + months_diff) as usize
|
||||
}
|
||||
|
||||
fn payment_type_name(period: &str) -> &'static str {
|
||||
// Expand the stored compact payment-period code into display text.
|
||||
match period.trim() {
|
||||
|
|
@ -168,11 +136,18 @@ async fn build_contract_summary(hash: Vec<u8>, db: &Db) -> Result<String, String
|
|||
.sum();
|
||||
let total_to_be_paidback = loan.payment_amount * loan.payment_number as u64;
|
||||
let remaining_balance = total_to_be_paidback.saturating_sub(total_value_paid);
|
||||
let pending_value = get_pending_payments_for_contract(&contract_hash)
|
||||
.await
|
||||
.map_err(|err| format!("error: Failed to read pending loan payments: {err}"))?;
|
||||
let remaining_after_pending = remaining_balance.saturating_sub(pending_value);
|
||||
|
||||
let periods_elapsed = time_passed(loan.timestamp, &loan.payment_period);
|
||||
let payments_due = periods_elapsed
|
||||
.saturating_sub(1)
|
||||
.min(loan.payment_number as usize);
|
||||
let payments_due = overdue_payment_count(
|
||||
loan.timestamp,
|
||||
&loan.payment_period,
|
||||
loan.payment_number,
|
||||
Utc::now().timestamp() as u32,
|
||||
)
|
||||
.map_err(|err| format!("error: Failed to calculate loan schedule: {err}"))?;
|
||||
let should_have_paid = payments_due as u64 * loan.payment_amount;
|
||||
|
||||
// A contract is inactive once collateral was claimed; otherwise it is
|
||||
|
|
@ -206,24 +181,35 @@ async fn build_contract_summary(hash: Vec<u8>, db: &Db) -> Result<String, String
|
|||
.unwrap_or_default();
|
||||
|
||||
let mut output = format!(
|
||||
"contract={}\nstatus={}\ncreation_date={}\nlender={}\nborrower={}\ncoin_loaned={}\nloaned_count={:.8}\ncollateral={}\ncollateral_count={:.8}\npayment_type={}\nnumber_of_payments={}\npayment_value={:.8}\nmax_late_payments={}\nmax_late_value={:.8}\ntotal_payments_made={}\ntotal_value_paid={:.8}\nremaining_balance={:.8}\ncollateral_claimed_by={}\n",
|
||||
"contract={}\nstatus={}\ncreation_date={}\nstart_timestamp={}\nlender={}\nborrower={}\ncoin_loaned={}\nloaned_count={:.8}\nloaned_count_atomic={}\ncollateral={}\ncollateral_count={:.8}\ncollateral_count_atomic={}\npayment_type={}\nnumber_of_payments={}\npayment_value={:.8}\npayment_value_atomic={}\nmax_late_payments={}\nmax_late_value={:.8}\nmax_late_value_atomic={}\ntotal_payments_made={}\ntotal_value_paid={:.8}\ntotal_value_paid_atomic={}\npending_value={:.8}\npending_value_atomic={}\nremaining_balance={:.8}\nremaining_balance_atomic={}\nremaining_after_pending={:.8}\nremaining_after_pending_atomic={}\ncollateral_claimed_by={}\n",
|
||||
contract_hash,
|
||||
status,
|
||||
format_local_date(loan.timestamp),
|
||||
loan.timestamp,
|
||||
loan.lender.trim(),
|
||||
loan.borrower.trim(),
|
||||
loan.loan_coin.trim(),
|
||||
format_amount(loan.loan_amount),
|
||||
loan.loan_amount,
|
||||
loan.collateral.trim(),
|
||||
format_amount(loan.collateral_amount),
|
||||
loan.collateral_amount,
|
||||
payment_type_name(&loan.payment_period),
|
||||
loan.payment_number,
|
||||
format_amount(loan.payment_amount),
|
||||
loan.payment_amount,
|
||||
loan.grace_period,
|
||||
format_amount(loan.max_late_value),
|
||||
loan.max_late_value,
|
||||
payments.len(),
|
||||
format_amount(total_value_paid),
|
||||
total_value_paid,
|
||||
format_amount(pending_value),
|
||||
pending_value,
|
||||
format_amount(remaining_balance),
|
||||
remaining_balance,
|
||||
format_amount(remaining_after_pending),
|
||||
remaining_after_pending,
|
||||
collateral_claimed_by,
|
||||
);
|
||||
output.push_str(&payments_output);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,279 @@
|
|||
use crate::blocks::marketing::MarketingTransaction;
|
||||
use crate::records::memory::mempool::db_client;
|
||||
use crate::records::record_chain::marketing_campaign_index::MARKETING_CAMPAIGN_INDEX_TREE;
|
||||
use crate::rpc::commands::transaction_by_txid::transaction_bytes_with_block;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Bound;
|
||||
|
||||
const TXID_LENGTH: usize = 32;
|
||||
const MAX_MARKETING_CAMPAIGN_ROWS: usize = 10_000;
|
||||
const STATUS_PENDING: u8 = 0;
|
||||
const STATUS_MINED: u8 = 1;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MarketingCampaignRow {
|
||||
status: u8,
|
||||
txid: Vec<u8>,
|
||||
block_height: u32,
|
||||
time: u32,
|
||||
campaign: u64,
|
||||
ad_type: String,
|
||||
keyword: String,
|
||||
displayed: String,
|
||||
impression: u8,
|
||||
click: u8,
|
||||
impression_value: u16,
|
||||
click_value: u16,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MarketingCampaignTotals {
|
||||
total_records: u64,
|
||||
total_impressions: u64,
|
||||
total_clicks: u64,
|
||||
total_impression_value: u64,
|
||||
total_click_value: u64,
|
||||
}
|
||||
|
||||
impl MarketingCampaignTotals {
|
||||
fn add(&mut self, row: &MarketingCampaignRow) {
|
||||
self.total_records = self.total_records.saturating_add(1);
|
||||
self.total_impressions = self
|
||||
.total_impressions
|
||||
.saturating_add(u64::from(row.impression));
|
||||
self.total_clicks = self.total_clicks.saturating_add(u64::from(row.click));
|
||||
self.total_impression_value = self.total_impression_value.saturating_add(
|
||||
u64::from(row.impression).saturating_mul(u64::from(row.impression_value)),
|
||||
);
|
||||
self.total_click_value = self
|
||||
.total_click_value
|
||||
.saturating_add(u64::from(row.click).saturating_mul(u64::from(row.click_value)));
|
||||
}
|
||||
}
|
||||
|
||||
fn prefix_successor(prefix: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut next = prefix.to_vec();
|
||||
for index in (0..next.len()).rev() {
|
||||
if next[index] != u8::MAX {
|
||||
next[index] += 1;
|
||||
next.truncate(index + 1);
|
||||
return Some(next);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn campaign_bounds(advertiser_bytes: &[u8], campaign: u64) -> (Bound<Vec<u8>>, Bound<Vec<u8>>) {
|
||||
let mut prefix = Vec::with_capacity(Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8);
|
||||
prefix.extend_from_slice(advertiser_bytes);
|
||||
prefix.extend_from_slice(&campaign.to_be_bytes());
|
||||
let start = Bound::Included(prefix.clone());
|
||||
let end = prefix_successor(&prefix)
|
||||
.map(Bound::Excluded)
|
||||
.unwrap_or(Bound::Unbounded);
|
||||
(start, end)
|
||||
}
|
||||
|
||||
fn row_from_transaction(
|
||||
status: u8,
|
||||
txid: Vec<u8>,
|
||||
block_height: u32,
|
||||
tx: MarketingTransaction,
|
||||
) -> MarketingCampaignRow {
|
||||
let marketing = tx.unsigned_marketing;
|
||||
MarketingCampaignRow {
|
||||
status,
|
||||
txid,
|
||||
block_height,
|
||||
time: marketing.time,
|
||||
campaign: marketing.campaign,
|
||||
ad_type: marketing.ad_type,
|
||||
keyword: marketing.keyword,
|
||||
displayed: marketing.displayed,
|
||||
impression: marketing.impression,
|
||||
click: marketing.click,
|
||||
impression_value: marketing.impression_value,
|
||||
click_value: marketing.click_value,
|
||||
}
|
||||
}
|
||||
|
||||
async fn pending_rows(
|
||||
advertiser: &str,
|
||||
campaign: u64,
|
||||
) -> Result<Vec<MarketingCampaignRow>, String> {
|
||||
let client_handle = db_client()
|
||||
.await
|
||||
.map_err(|err| format!("Failed to connect to mempool database: {err}"))?;
|
||||
let client = client_handle.as_ref();
|
||||
let rows = client
|
||||
.query(
|
||||
r#"
|
||||
SELECT hash, original
|
||||
FROM marketing
|
||||
WHERE advertiser = $1 AND processed = false
|
||||
ORDER BY time DESC, id DESC
|
||||
"#,
|
||||
&[&advertiser],
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to query pending marketing records: {err}"))?;
|
||||
|
||||
let mut output = Vec::new();
|
||||
for row in rows {
|
||||
let hash: String = row.get(0);
|
||||
let original: Vec<u8> = row.get(1);
|
||||
if original.first() != Some(&5) {
|
||||
continue;
|
||||
}
|
||||
let Ok(tx) = MarketingTransaction::from_bytes(5, &original[1..]).await else {
|
||||
continue;
|
||||
};
|
||||
if tx.unsigned_marketing.campaign != campaign {
|
||||
continue;
|
||||
}
|
||||
let txid = match crate::decode(&hash) {
|
||||
Ok(bytes) if bytes.len() == TXID_LENGTH => bytes,
|
||||
_ => continue,
|
||||
};
|
||||
output.push(row_from_transaction(STATUS_PENDING, txid, 0, tx));
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
async fn mined_rows(
|
||||
db: &Db,
|
||||
advertiser_bytes: &[u8],
|
||||
campaign: u64,
|
||||
seen: &mut HashSet<Vec<u8>>,
|
||||
) -> Result<Vec<MarketingCampaignRow>, String> {
|
||||
let tree = db
|
||||
.open_tree(MARKETING_CAMPAIGN_INDEX_TREE)
|
||||
.map_err(|err| format!("Failed to open marketing campaign index: {err}"))?;
|
||||
let (start, end) = campaign_bounds(advertiser_bytes, campaign);
|
||||
let mut output = Vec::new();
|
||||
|
||||
for entry in tree.range((start, end)).rev() {
|
||||
let (_key, value) =
|
||||
entry.map_err(|err| format!("Failed to read marketing campaign index: {err}"))?;
|
||||
if value.len() != TXID_LENGTH {
|
||||
continue;
|
||||
}
|
||||
let txid = value.to_vec();
|
||||
if !seen.insert(txid.clone()) {
|
||||
continue;
|
||||
}
|
||||
let (block_height, transaction_bytes) = transaction_bytes_with_block(db, &txid).await?;
|
||||
if transaction_bytes.first() != Some(&5) {
|
||||
continue;
|
||||
}
|
||||
let tx = MarketingTransaction::from_bytes(5, &transaction_bytes[1..])
|
||||
.await
|
||||
.map_err(|err| format!("Failed to decode marketing transaction: {err}"))?;
|
||||
if tx.unsigned_marketing.campaign != campaign {
|
||||
continue;
|
||||
}
|
||||
output.push(row_from_transaction(STATUS_MINED, txid, block_height, tx));
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn encode_fixed_text(output: &mut Vec<u8>, value: &str, width: usize) {
|
||||
let bytes = value.as_bytes();
|
||||
if bytes.len() >= width {
|
||||
output.extend_from_slice(&bytes[..width]);
|
||||
} else {
|
||||
output.extend_from_slice(bytes);
|
||||
output.extend(std::iter::repeat(b' ').take(width - bytes.len()));
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_response(totals: MarketingCampaignTotals, rows: &[MarketingCampaignRow]) -> RpcResponse {
|
||||
let mut response = Vec::with_capacity(8 * 5 + 4 + rows.len().saturating_mul(202));
|
||||
response.extend_from_slice(&totals.total_records.to_le_bytes());
|
||||
response.extend_from_slice(&totals.total_impressions.to_le_bytes());
|
||||
response.extend_from_slice(&totals.total_clicks.to_le_bytes());
|
||||
response.extend_from_slice(&totals.total_impression_value.to_le_bytes());
|
||||
response.extend_from_slice(&totals.total_click_value.to_le_bytes());
|
||||
response.extend_from_slice(&(rows.len() as u32).to_le_bytes());
|
||||
|
||||
for row in rows {
|
||||
response.push(row.status);
|
||||
response.extend_from_slice(&row.txid);
|
||||
response.extend_from_slice(&row.block_height.to_le_bytes());
|
||||
response.extend_from_slice(&row.time.to_le_bytes());
|
||||
response.extend_from_slice(&row.campaign.to_le_bytes());
|
||||
encode_fixed_text(&mut response, &row.ad_type, 6);
|
||||
encode_fixed_text(&mut response, &row.keyword, 40);
|
||||
encode_fixed_text(&mut response, &row.displayed, 100);
|
||||
response.push(row.impression);
|
||||
response.push(row.click);
|
||||
response.extend_from_slice(&row.impression_value.to_le_bytes());
|
||||
response.extend_from_slice(&row.click_value.to_le_bytes());
|
||||
}
|
||||
|
||||
RpcResponse::Binary(response)
|
||||
}
|
||||
|
||||
pub async fn lookup(
|
||||
advertiser_bytes: Vec<u8>,
|
||||
campaign: u64,
|
||||
skip: u32,
|
||||
limit: u32,
|
||||
db: &Db,
|
||||
) -> RpcResponse {
|
||||
if advertiser_bytes.len() != Wallet::SHORT_ADDRESS_BYTES_LENGTH {
|
||||
return RpcResponse::Binary(b"error: Invalid advertiser address bytes".to_vec());
|
||||
}
|
||||
let Some(advertiser) = Wallet::bytes_to_short_address(&advertiser_bytes) else {
|
||||
return RpcResponse::Binary(b"error: Invalid advertiser address bytes".to_vec());
|
||||
};
|
||||
|
||||
let limit = (limit as usize).min(MAX_MARKETING_CAMPAIGN_ROWS);
|
||||
let skip = skip as usize;
|
||||
let mut seen = HashSet::new();
|
||||
let mut all_rows = Vec::new();
|
||||
|
||||
match pending_rows(&advertiser, campaign).await {
|
||||
Ok(rows) => {
|
||||
for row in rows {
|
||||
seen.insert(row.txid.clone());
|
||||
all_rows.push(row);
|
||||
}
|
||||
}
|
||||
Err(err) => return RpcResponse::Binary(format!("error: {err}").into_bytes()),
|
||||
}
|
||||
|
||||
match mined_rows(db, &advertiser_bytes, campaign, &mut seen).await {
|
||||
Ok(rows) => all_rows.extend(rows),
|
||||
Err(err) => return RpcResponse::Binary(format!("error: {err}").into_bytes()),
|
||||
}
|
||||
|
||||
all_rows.sort_by(|left, right| {
|
||||
right
|
||||
.time
|
||||
.cmp(&left.time)
|
||||
.then_with(|| right.block_height.cmp(&left.block_height))
|
||||
});
|
||||
|
||||
let mut totals = MarketingCampaignTotals::default();
|
||||
for row in &all_rows {
|
||||
totals.add(row);
|
||||
}
|
||||
|
||||
let paged_rows = if limit == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
all_rows
|
||||
.into_iter()
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
encode_response(totals, &paged_rows)
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ pub mod contract;
|
|||
pub mod difficulty;
|
||||
pub mod largest_tx_fee;
|
||||
pub mod latest_block;
|
||||
pub mod marketing_campaign_history;
|
||||
pub mod memory_by_signature;
|
||||
pub mod network_info;
|
||||
pub mod nft_list;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ use crate::blocks::loans::LoanContractTransaction;
|
|||
use crate::blocks::nft::CreateNftTransaction;
|
||||
use crate::blocks::swap::SwapTransaction;
|
||||
use crate::blocks::transfer::TransferTransaction;
|
||||
use crate::common::asset_names::loan_collateral_asset_name_or_base;
|
||||
use crate::common::binary_conversions::binary_to_string;
|
||||
use crate::common::nft_assets::{nft_asset_name, nft_asset_parts};
|
||||
use crate::decode;
|
||||
use crate::fs;
|
||||
use crate::records::balance_sheet::pathing::{balance_asset_segments, balance_root_path};
|
||||
use crate::records::balance_sheet::tokens_to_lower::strip_spaces_and_lowercase;
|
||||
use crate::records::memory::mempool::BASECOIN;
|
||||
use crate::rpc::commands::transaction_by_txid::{
|
||||
request_transaction_by_txid, request_transaction_by_txid_with_block,
|
||||
};
|
||||
|
|
@ -245,7 +247,11 @@ async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Op
|
|||
let tx = LoanContractTransaction::from_bytes(txtype, body)
|
||||
.await
|
||||
.ok()?;
|
||||
if tx.unsigned_loan_contract.collateral == asset_name {
|
||||
let collateral = loan_collateral_asset_name_or_base(
|
||||
&tx.unsigned_loan_contract.collateral,
|
||||
&BASECOIN,
|
||||
)?;
|
||||
if collateral == asset_name {
|
||||
from = wallet_field_bytes(&tx.unsigned_loan_contract.borrower);
|
||||
let (asset, series) = nft_asset_parts(&tx.unsigned_loan_contract.loan_coin);
|
||||
received_asset = padded_asset_bytes(asset.trim());
|
||||
|
|
@ -255,7 +261,7 @@ async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Op
|
|||
} else if tx.unsigned_loan_contract.loan_coin == asset_name {
|
||||
from = wallet_field_bytes(&tx.unsigned_loan_contract.lender);
|
||||
to = wallet_field_bytes(&tx.unsigned_loan_contract.borrower);
|
||||
let (asset, series) = nft_asset_parts(&tx.unsigned_loan_contract.collateral);
|
||||
let (asset, series) = nft_asset_parts(&collateral);
|
||||
received_asset = padded_asset_bytes(asset.trim());
|
||||
received_series = series;
|
||||
received_value = tx.unsigned_loan_contract.collateral_amount;
|
||||
|
|
@ -295,7 +301,11 @@ async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Op
|
|||
let contract = LoanContractTransaction::from_bytes(7, &contract_bytes)
|
||||
.await
|
||||
.ok()?;
|
||||
if contract.unsigned_loan_contract.collateral != asset_name {
|
||||
let collateral = loan_collateral_asset_name_or_base(
|
||||
&contract.unsigned_loan_contract.collateral,
|
||||
&BASECOIN,
|
||||
)?;
|
||||
if collateral != asset_name {
|
||||
return None;
|
||||
}
|
||||
to = wallet_field_bytes(&tx.unsigned_collateral_claim.address);
|
||||
|
|
|
|||
|
|
@ -295,3 +295,157 @@ pub async fn lookup_token_details(db: &Db, token_name: String) -> RpcResponse {
|
|||
|
||||
RpcResponse::Binary(response)
|
||||
}
|
||||
|
||||
pub async fn token_catalog(db: &Db) -> RpcResponse {
|
||||
let tokens_tree = match db.open_tree("tokens") {
|
||||
Ok(tree) => tree,
|
||||
Err(err) => {
|
||||
return RpcResponse::Binary(
|
||||
format!("error: Failed to open tokens tree: {err}").into_bytes(),
|
||||
)
|
||||
}
|
||||
};
|
||||
let origins_tree = match db.open_tree("token_origins") {
|
||||
Ok(tree) => tree,
|
||||
Err(err) => {
|
||||
return RpcResponse::Binary(
|
||||
format!("error: Failed to open token origins tree: {err}").into_bytes(),
|
||||
)
|
||||
}
|
||||
};
|
||||
let limits_tree = match db.open_tree("token_limits") {
|
||||
Ok(tree) => tree,
|
||||
Err(err) => {
|
||||
return RpcResponse::Binary(
|
||||
format!("error: Failed to open token limits tree: {err}").into_bytes(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let mut rows = Vec::new();
|
||||
|
||||
for entry in origins_tree.iter() {
|
||||
let (ticker_key, origin_value) = match entry {
|
||||
Ok(pair) => pair,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let ticker = binary_to_string(ticker_key.to_vec());
|
||||
let normalized_ticker = strip_spaces_and_lowercase(&ticker);
|
||||
if normalized_ticker.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let origin_hash = binary_to_string(origin_value.to_vec());
|
||||
let origin_hash_bytes = match decode(&origin_hash) {
|
||||
Ok(bytes) if bytes.len() == 32 => bytes,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let RpcResponse::Binary(tx_bytes) =
|
||||
request_transaction_by_txid(db, origin_hash_bytes.clone()).await;
|
||||
if tx_bytes.is_empty() || tx_bytes[0] != 3 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let contract = match CreateTokenTransaction::from_bytes(tx_bytes[0], &tx_bytes[1..]).await {
|
||||
Ok(tx) => tx,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let creator_bytes =
|
||||
match Wallet::short_address_to_bytes(&contract.unsigned_create_token.creator) {
|
||||
Some(bytes) => bytes,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let current_supply = tokens_tree
|
||||
.get(ticker_key.as_ref())
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|value| parse_token_supply(value.as_ref()))
|
||||
.unwrap_or(0);
|
||||
let hard_limit = limits_tree
|
||||
.get(ticker_key.as_ref())
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|bytes| bytes.first().copied())
|
||||
.unwrap_or(contract.unsigned_create_token.hard_limit);
|
||||
|
||||
let (_, burned_hashes) = collect_token_history(db, &ticker, &origin_hash_bytes).await;
|
||||
let mut total_burned = 0_u64;
|
||||
for hash in burned_hashes {
|
||||
let RpcResponse::Binary(burn_bytes) =
|
||||
request_transaction_by_txid(db, hash.to_vec()).await;
|
||||
if burn_bytes.is_empty() || burn_bytes[0] != 10 {
|
||||
continue;
|
||||
}
|
||||
let Ok(burn) =
|
||||
crate::blocks::burn::BurnTransaction::from_bytes(burn_bytes[0], &burn_bytes[1..])
|
||||
.await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if burn.unsigned_burn.nft_series == 0
|
||||
&& strip_spaces_and_lowercase(&burn.unsigned_burn.coin) == normalized_ticker
|
||||
{
|
||||
total_burned = total_burned.saturating_add(burn.unsigned_burn.value);
|
||||
}
|
||||
}
|
||||
|
||||
let mut padded_ticker = ticker.as_bytes().to_vec();
|
||||
if padded_ticker.len() < 15 {
|
||||
padded_ticker.resize(15, b' ');
|
||||
}
|
||||
if padded_ticker.len() != 15
|
||||
|| creator_bytes.len() != Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
|| origin_hash_bytes.len() != 32
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.push((
|
||||
padded_ticker,
|
||||
origin_hash_bytes,
|
||||
creator_bytes,
|
||||
contract.unsigned_create_token.time,
|
||||
contract.unsigned_create_token.number,
|
||||
current_supply,
|
||||
total_burned,
|
||||
count_wallets_holding(&normalized_ticker) as u32,
|
||||
hard_limit,
|
||||
));
|
||||
}
|
||||
|
||||
rows.sort_by(|left, right| {
|
||||
binary_to_string(left.0.clone())
|
||||
.to_ascii_lowercase()
|
||||
.cmp(&binary_to_string(right.0.clone()).to_ascii_lowercase())
|
||||
});
|
||||
|
||||
let mut response = Vec::with_capacity(4 + rows.len() * 102);
|
||||
response.extend_from_slice(&(rows.len() as u32).to_le_bytes());
|
||||
for (
|
||||
ticker,
|
||||
origin_hash,
|
||||
creator,
|
||||
created_timestamp,
|
||||
initial_supply,
|
||||
current_supply,
|
||||
total_burned,
|
||||
holder_count,
|
||||
hard_limit,
|
||||
) in rows
|
||||
{
|
||||
response.extend_from_slice(&ticker);
|
||||
response.extend_from_slice(&origin_hash);
|
||||
response.extend_from_slice(&creator);
|
||||
response.extend_from_slice(&created_timestamp.to_le_bytes());
|
||||
response.extend_from_slice(&initial_supply.to_le_bytes());
|
||||
response.extend_from_slice(¤t_supply.to_le_bytes());
|
||||
response.extend_from_slice(&total_burned.to_le_bytes());
|
||||
response.extend_from_slice(&holder_count.to_le_bytes());
|
||||
response.push(hard_limit);
|
||||
}
|
||||
|
||||
RpcResponse::Binary(response)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ use crate::sled::Db;
|
|||
use crate::torrent::torrenting_system::get_nodes::get_nodes_from_memory;
|
||||
use crate::Arc;
|
||||
use crate::Mutex;
|
||||
use crate::OnceLock;
|
||||
|
||||
static LOAN_PAYMENT_SUBMIT_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
|
||||
fn loan_payment_submit_lock() -> &'static Mutex<()> {
|
||||
LOAN_PAYMENT_SUBMIT_LOCK.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
async fn broadcast_tx(tx_bytes: Vec<u8>, map: Arc<Mutex<Command>>) {
|
||||
// Broadcast newly accepted mempool transactions only to miner peers,
|
||||
|
|
@ -375,7 +382,11 @@ pub async fn save_and_submit(
|
|||
RpcResponse::Binary(msg)
|
||||
}
|
||||
},
|
||||
BORROWER_TYPE => match ContractPaymentTransaction::from_bytes(txtype, &tx).await {
|
||||
BORROWER_TYPE => {
|
||||
// Serialize the pending-total check and insert so concurrent
|
||||
// payments cannot both reserve the same remaining balance.
|
||||
let _loan_payment_guard = loan_payment_submit_lock().lock().await;
|
||||
match ContractPaymentTransaction::from_bytes(txtype, &tx).await {
|
||||
Ok(payment) => match payment.verify(db).await {
|
||||
Ok(existing) => {
|
||||
if !existing.is_empty() {
|
||||
|
|
@ -389,12 +400,15 @@ pub async fn save_and_submit(
|
|||
Ok(payment_bytes) => match payment.add_to_memory().await {
|
||||
Ok(_) => {
|
||||
broadcast_tx(payment_bytes, map.clone()).await;
|
||||
let msg = "successful_broadcast: true"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
RpcResponse::Binary(msg)
|
||||
}
|
||||
Err(err) => {
|
||||
let msg =
|
||||
"successful_broadcast: true".to_string().as_bytes().to_vec();
|
||||
RpcResponse::Binary(msg)
|
||||
}
|
||||
Err(err) => {
|
||||
let msg = format!("error: {err}").to_string().as_bytes().to_vec();
|
||||
format!("error: {err}").to_string().as_bytes().to_vec();
|
||||
RpcResponse::Binary(msg)
|
||||
}
|
||||
},
|
||||
|
|
@ -413,7 +427,8 @@ pub async fn save_and_submit(
|
|||
let msg = format!("error: {err}").to_string().as_bytes().to_vec();
|
||||
RpcResponse::Binary(msg)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
COLLATERAL_TYPE => match CollateralClaimTransaction::from_bytes(txtype, &tx).await {
|
||||
Ok(collateral) => match collateral.verify(db).await {
|
||||
Ok(existing) => {
|
||||
|
|
|
|||
|
|
@ -907,6 +907,55 @@ pub async fn start_loop(
|
|||
.send(&stream_locked, Some(&connections_key), uid)
|
||||
.await;
|
||||
}
|
||||
47 => {
|
||||
// request marketing campaign summary and paged campaign records
|
||||
let (uid, _) = read_bytes_from_stream::read_uid_from_stream(
|
||||
&connections_key,
|
||||
stream_locked.clone(),
|
||||
)
|
||||
.await?;
|
||||
let advertiser = read_bytes_from_stream::read_short_address_from_stream(
|
||||
&connections_key,
|
||||
stream_locked.clone(),
|
||||
)
|
||||
.await?;
|
||||
let campaign = read_bytes_from_stream::read_u64_from_stream(
|
||||
&connections_key,
|
||||
stream_locked.clone(),
|
||||
)
|
||||
.await?;
|
||||
let skip = read_bytes_from_stream::read_u32_from_stream(
|
||||
&connections_key,
|
||||
stream_locked.clone(),
|
||||
)
|
||||
.await?;
|
||||
let limit = read_bytes_from_stream::read_u32_from_stream(
|
||||
&connections_key,
|
||||
stream_locked.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let result = commands::marketing_campaign_history::lookup(
|
||||
advertiser, campaign, skip, limit, &db,
|
||||
)
|
||||
.await;
|
||||
result
|
||||
.send(&stream_locked, Some(&connections_key), uid)
|
||||
.await;
|
||||
}
|
||||
48 => {
|
||||
// request the full token catalog for wallet asset views
|
||||
let (uid, _) = read_bytes_from_stream::read_uid_from_stream(
|
||||
&connections_key,
|
||||
stream_locked.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let result = commands::token_lookup::token_catalog(&db).await;
|
||||
result
|
||||
.send(&stream_locked, Some(&connections_key), uid)
|
||||
.await;
|
||||
}
|
||||
255 => {
|
||||
commands::route_reply::route_reply(
|
||||
&connections_key,
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ use crate::records::memory::response_channels::Byte3;
|
|||
use crate::rpc::command_maps::{
|
||||
MAX_RPC_REPLY_BYTES, RPC_ADDRESS_HISTORY, RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_BY_HASH,
|
||||
RPC_BLOCK_BY_HEIGHT, RPC_BLOCK_HEIGHT, RPC_BLOCK_IP, RPC_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY,
|
||||
RPC_LARGEST_TX_FEE, RPC_LATEST_ADDRESS_TRANSACTIONS,
|
||||
RPC_LOAN_CONTRACT, RPC_MEMPOOL_TX_BY_ADDRESS, RPC_MEMPOOL_TX_BY_SIGNATURE,
|
||||
RPC_LARGEST_TX_FEE, RPC_LATEST_ADDRESS_TRANSACTIONS, RPC_LOAN_CONTRACT,
|
||||
RPC_MARKETING_CAMPAIGN_HISTORY, RPC_MEMPOOL_TX_BY_ADDRESS, RPC_MEMPOOL_TX_BY_SIGNATURE,
|
||||
RPC_MEMPOOL_TX_COUNT, RPC_NETWORK_INFO, RPC_NFT_DETAILS, RPC_NFT_LIST, RPC_REGISTER_WALLET,
|
||||
RPC_TIME, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, RPC_TOTAL_CONFIRMED_TX,
|
||||
RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP, RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP,
|
||||
RPC_WALLET_REGISTRATION_STATUS,
|
||||
RPC_TIME, RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT,
|
||||
RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP, RPC_VALIDATE_MESSAGE,
|
||||
RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP, RPC_WALLET_REGISTRATION_STATUS,
|
||||
};
|
||||
use crate::standalone_tools::transaction_creator::create_transaction_request;
|
||||
use crate::timeout;
|
||||
|
|
@ -251,6 +251,42 @@ async fn build_request_bytes(
|
|||
bin_msg.extend_from_slice(&hashmap_key);
|
||||
bin_msg.extend_from_slice(&address_bytes)
|
||||
}
|
||||
// Validate a signature over a UTF-8 message for a registered wallet.
|
||||
25 => {
|
||||
let command_number: u8 = RPC_VALIDATE_MESSAGE;
|
||||
let mut parts = command_input.splitn(3, '|');
|
||||
let message = parts.next().unwrap_or_default();
|
||||
let address = parts.next().unwrap_or_default();
|
||||
let signature = parts.next().unwrap_or_default();
|
||||
let message_bytes = message.as_bytes();
|
||||
let message_size = u16::try_from(message_bytes.len()).map_err(|_| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "Signed message is too long")
|
||||
})?;
|
||||
let address_bytes = Wallet::normalize_to_short_address(address)
|
||||
.and_then(|short| Wallet::short_address_to_bytes(&short))
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "Invalid signer address")
|
||||
})?;
|
||||
let signature_bytes = decode(signature).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("Invalid signature: {err}"),
|
||||
)
|
||||
})?;
|
||||
if signature_bytes.len() != Wallet::SIGNATURE_LENGTH {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Signature has the wrong byte length",
|
||||
));
|
||||
}
|
||||
|
||||
bin_msg.extend_from_slice(&command_number.to_le_bytes());
|
||||
bin_msg.extend_from_slice(&hashmap_key);
|
||||
bin_msg.extend_from_slice(&message_size.to_le_bytes());
|
||||
bin_msg.extend_from_slice(message_bytes);
|
||||
bin_msg.extend_from_slice(&address_bytes);
|
||||
bin_msg.extend_from_slice(&signature_bytes);
|
||||
}
|
||||
// Server-owner block IP request.
|
||||
26 => {
|
||||
let command_number: u8 = RPC_BLOCK_IP;
|
||||
|
|
@ -541,6 +577,46 @@ async fn build_request_bytes(
|
|||
bin_msg.extend_from_slice(&address_bytes);
|
||||
bin_msg.extend_from_slice(&limit.to_le_bytes());
|
||||
}
|
||||
// Lookup marketing campaign summary and paged campaign records.
|
||||
47 => {
|
||||
let command_number: u8 = RPC_MARKETING_CAMPAIGN_HISTORY;
|
||||
let parts = command_input.split('|').collect::<Vec<_>>();
|
||||
if parts.len() != 4 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Marketing campaign history input must be advertiser|campaign|skip|limit",
|
||||
));
|
||||
}
|
||||
let address_bytes = Wallet::normalize_to_short_address(parts[0])
|
||||
.and_then(|s| Wallet::short_address_to_bytes(&s))
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "Invalid advertiser address")
|
||||
})?;
|
||||
let campaign = parts[1]
|
||||
.parse::<u64>()
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid campaign id"))?;
|
||||
let skip = parts[2].parse::<u32>().map_err(|_| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "Invalid campaign history skip")
|
||||
})?;
|
||||
let limit = parts[3].parse::<u32>().map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Invalid campaign history limit",
|
||||
)
|
||||
})?;
|
||||
|
||||
bin_msg.extend_from_slice(&command_number.to_le_bytes());
|
||||
bin_msg.extend_from_slice(&hashmap_key);
|
||||
bin_msg.extend_from_slice(&address_bytes);
|
||||
bin_msg.extend_from_slice(&campaign.to_le_bytes());
|
||||
bin_msg.extend_from_slice(&skip.to_le_bytes());
|
||||
bin_msg.extend_from_slice(&limit.to_le_bytes());
|
||||
}
|
||||
48 => {
|
||||
let command_number: u8 = RPC_TOKEN_CATALOG;
|
||||
bin_msg.extend_from_slice(&command_number.to_le_bytes());
|
||||
bin_msg.extend_from_slice(&hashmap_key);
|
||||
}
|
||||
_ => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
|
|
@ -551,3 +627,30 @@ async fn build_request_bytes(
|
|||
|
||||
Ok(bin_msg)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_request_bytes;
|
||||
use crate::rpc::command_maps::RPC_VALIDATE_MESSAGE;
|
||||
use crate::wallets::structures::Wallet;
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_message_request_uses_expected_wire_layout() {
|
||||
let address = format!("{}.cltc", "00".repeat(20));
|
||||
let signature = "00".repeat(Wallet::SIGNATURE_LENGTH);
|
||||
let uid = [1, 2, 3];
|
||||
let bytes = build_request_bytes(
|
||||
format!("message|{address}|{signature}"),
|
||||
RPC_VALIDATE_MESSAGE as usize,
|
||||
uid,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(bytes[0], RPC_VALIDATE_MESSAGE);
|
||||
assert_eq!(&bytes[1..4], &uid);
|
||||
assert_eq!(u16::from_le_bytes([bytes[4], bytes[5]]), 7);
|
||||
assert_eq!(&bytes[6..13], b"message");
|
||||
assert_eq!(bytes.len(), 1 + 3 + 2 + 7 + 22 + Wallet::SIGNATURE_LENGTH);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::common::asset_names::loan_collateral_asset_name_or_base;
|
||||
use crate::common::nft_assets::nft_asset_name;
|
||||
use crate::common::types::Transaction;
|
||||
use crate::decode;
|
||||
|
|
@ -9,6 +10,7 @@ use crate::records::memory::mempool::{
|
|||
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::verifications::async_funcs::total_payments::get_total_payments;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -30,12 +32,21 @@ pub(crate) struct TransactionBalanceReservation {
|
|||
signatures: Vec<String>,
|
||||
debits: HashMap<(String, String), u64>,
|
||||
available: HashMap<(String, String), u64>,
|
||||
loan_payment: Option<LoanPaymentReservation>,
|
||||
}
|
||||
|
||||
struct LoanPaymentReservation {
|
||||
contract_hash: String,
|
||||
payment_amount: u64,
|
||||
confirmed_paid: u64,
|
||||
contract_total: u64,
|
||||
}
|
||||
|
||||
pub struct BlockBalanceTracker {
|
||||
transaction_hashes: HashSet<String>,
|
||||
transaction_signatures: HashSet<String>,
|
||||
reserved: HashMap<(String, String), u64>,
|
||||
loan_payments_reserved: HashMap<String, u64>,
|
||||
}
|
||||
|
||||
impl BlockBalanceTracker {
|
||||
|
|
@ -44,6 +55,7 @@ impl BlockBalanceTracker {
|
|||
transaction_hashes: HashSet::new(),
|
||||
transaction_signatures: HashSet::new(),
|
||||
reserved: HashMap::new(),
|
||||
loan_payments_reserved: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +73,30 @@ impl BlockBalanceTracker {
|
|||
}
|
||||
}
|
||||
|
||||
let loan_payment_update = if let Some(payment) = reservation.loan_payment {
|
||||
let already_reserved = self
|
||||
.loan_payments_reserved
|
||||
.get(&payment.contract_hash)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
let block_total = already_reserved
|
||||
.checked_add(payment.payment_amount)
|
||||
.ok_or_else(|| "Block loan payment reservation overflowed.".to_string())?;
|
||||
let total_after_block = payment
|
||||
.confirmed_paid
|
||||
.checked_add(block_total)
|
||||
.ok_or_else(|| "Block loan repayment total overflowed.".to_string())?;
|
||||
if total_after_block > payment.contract_total {
|
||||
return Err(format!(
|
||||
"Loan payments inside block exceed remaining contract balance: contract={}",
|
||||
payment.contract_hash
|
||||
));
|
||||
}
|
||||
Some((payment.contract_hash, block_total))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut updates = Vec::new();
|
||||
for (key, transaction_debit) in reservation.debits {
|
||||
let already_reserved = self.reserved.get(&key).copied().unwrap_or(0);
|
||||
|
|
@ -83,6 +119,10 @@ impl BlockBalanceTracker {
|
|||
for (key, required) in updates {
|
||||
self.reserved.insert(key, required);
|
||||
}
|
||||
if let Some((contract_hash, block_total)) = loan_payment_update {
|
||||
self.loan_payments_reserved
|
||||
.insert(contract_hash, block_total);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -315,6 +355,8 @@ async fn transaction_balance_view(
|
|||
Transaction::Lender(tx) => {
|
||||
let loan = &tx.unsigned_loan_contract;
|
||||
let mut debits = Vec::new();
|
||||
let collateral = loan_collateral_asset_name_or_base(&loan.collateral, &BASECOIN)
|
||||
.ok_or_else(|| "Invalid loan collateral asset.".to_string())?;
|
||||
|
||||
add_debit(
|
||||
&mut debits,
|
||||
|
|
@ -326,7 +368,7 @@ async fn transaction_balance_view(
|
|||
add_debit(
|
||||
&mut debits,
|
||||
&loan.borrower,
|
||||
loan.collateral.clone(),
|
||||
collateral,
|
||||
loan.collateral_amount,
|
||||
);
|
||||
|
||||
|
|
@ -422,6 +464,23 @@ pub(crate) async fn prepare_transaction_balance_reservation(
|
|||
}
|
||||
|
||||
let already_reserved_in_mempool = transaction_is_pending(&view).await?;
|
||||
let loan_payment = if let Transaction::Borrower(tx) = transaction {
|
||||
let payment = &tx.unsigned_contract_payment;
|
||||
let contract = loan_contract_for_hash(db, &payment.contract_hash).await?;
|
||||
let contract_total = contract
|
||||
.unsigned_loan_contract
|
||||
.payment_amount
|
||||
.checked_mul(contract.unsigned_loan_contract.payment_number as u64)
|
||||
.ok_or_else(|| "Loan repayment total overflowed.".to_string())?;
|
||||
Some(LoanPaymentReservation {
|
||||
contract_hash: payment.contract_hash.clone(),
|
||||
payment_amount: payment.payback_amount,
|
||||
confirmed_paid: get_total_payments(db, &payment.contract_hash).await,
|
||||
contract_total,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut debits = HashMap::new();
|
||||
let mut available = HashMap::new();
|
||||
|
||||
|
|
@ -452,5 +511,50 @@ pub(crate) async fn prepare_transaction_balance_reservation(
|
|||
signatures: view.signatures,
|
||||
debits,
|
||||
available,
|
||||
loan_payment,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{BlockBalanceTracker, LoanPaymentReservation, TransactionBalanceReservation};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn payment_reservation(
|
||||
hash: &str,
|
||||
contract_hash: &str,
|
||||
payment_amount: u64,
|
||||
confirmed_paid: u64,
|
||||
contract_total: u64,
|
||||
) -> TransactionBalanceReservation {
|
||||
TransactionBalanceReservation {
|
||||
hash: hash.to_string(),
|
||||
signatures: vec![format!("signature-{hash}")],
|
||||
debits: HashMap::new(),
|
||||
available: HashMap::new(),
|
||||
loan_payment: Some(LoanPaymentReservation {
|
||||
contract_hash: contract_hash.to_string(),
|
||||
payment_amount,
|
||||
confirmed_paid,
|
||||
contract_total,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cumulative_block_payments_cannot_exceed_contract_total() {
|
||||
let mut tracker = BlockBalanceTracker::new();
|
||||
|
||||
tracker
|
||||
.reserve(payment_reservation("one", "contract", 50, 20, 100))
|
||||
.unwrap();
|
||||
tracker
|
||||
.reserve(payment_reservation("two", "contract", 30, 20, 100))
|
||||
.unwrap();
|
||||
|
||||
let error = tracker
|
||||
.reserve(payment_reservation("three", "contract", 1, 20, 100))
|
||||
.unwrap_err();
|
||||
assert!(error.contains("exceed remaining contract balance"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ pub async fn verify_transactions(
|
|||
db: &Db,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
balance_tracker: Arc<Mutex<BlockBalanceTracker>>,
|
||||
block_timestamp: u32,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let mut results: Vec<String> = Vec::new();
|
||||
|
||||
|
|
@ -26,7 +27,15 @@ pub async fn verify_transactions(
|
|||
return Err("Stop signal received.".to_string());
|
||||
}
|
||||
|
||||
match verify_transaction(miner, transaction.clone(), db, balance_tracker.clone()).await {
|
||||
match verify_transaction(
|
||||
miner,
|
||||
transaction.clone(),
|
||||
db,
|
||||
balance_tracker.clone(),
|
||||
block_timestamp,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
results.push(result);
|
||||
}
|
||||
|
|
@ -48,6 +57,7 @@ pub async fn verify_transactions(
|
|||
transaction.clone(),
|
||||
db,
|
||||
balance_tracker.clone(),
|
||||
block_timestamp,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
@ -71,6 +81,7 @@ async fn verify_transaction(
|
|||
transaction: Transaction,
|
||||
db: &Db,
|
||||
balance_tracker: Arc<Mutex<BlockBalanceTracker>>,
|
||||
block_timestamp: u32,
|
||||
) -> Result<String, String> {
|
||||
if let Transaction::Genesis(genesis_tx) = &transaction {
|
||||
match genesis_tx.verify().await {
|
||||
|
|
@ -202,7 +213,7 @@ async fn verify_transaction(
|
|||
}
|
||||
}
|
||||
if let Transaction::Borrower(loan_payment_tx) = &transaction {
|
||||
match loan_payment_tx.verify(db).await {
|
||||
match loan_payment_tx.verify_for_block(db).await {
|
||||
Ok(value) => {
|
||||
reserve_verified_transaction(db, &transaction, balance_tracker).await?;
|
||||
return Ok(value);
|
||||
|
|
@ -215,7 +226,7 @@ async fn verify_transaction(
|
|||
}
|
||||
}
|
||||
if let Transaction::Collateral(collateral_claim_tx) = &transaction {
|
||||
match collateral_claim_tx.verify(db).await {
|
||||
match collateral_claim_tx.verify_at(db, block_timestamp).await {
|
||||
Ok(value) => {
|
||||
reserve_verified_transaction(db, &transaction, balance_tracker).await?;
|
||||
return Ok(value);
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ impl Block {
|
|||
self.transactions.clone(),
|
||||
db.clone(),
|
||||
verification_service,
|
||||
timestamp,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
@ -183,11 +184,12 @@ impl Block {
|
|||
transactions: Vec<Transaction>,
|
||||
db: crate::sled::Db,
|
||||
verification_service: Arc<VerificationService>,
|
||||
block_timestamp: u32,
|
||||
) -> Result<Vec<String>, String> {
|
||||
// transaction verification is centralized in the shared
|
||||
// verification service so block assembly stays lightweight
|
||||
verification_service
|
||||
.verify_block_transactions(miner, transactions, db)
|
||||
.verify_block_transactions(miner, transactions, db, block_timestamp)
|
||||
.await
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::blocks::loan_payment::ContractPaymentTransaction;
|
||||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::common::types::BORROWER_FEE;
|
||||
use crate::records::memory::mempool::get_pending_payments_for_contract;
|
||||
use crate::records::wallet_registry::{
|
||||
require_canonical_registered_short_address, resolve_pubkey_from_short_address,
|
||||
};
|
||||
|
|
@ -16,6 +17,18 @@ use crate::{decode, encode};
|
|||
|
||||
impl ContractPaymentTransaction {
|
||||
pub async fn verify(&self, db: &Db) -> Result<String, String> {
|
||||
self.verify_internal(db, true).await
|
||||
}
|
||||
|
||||
pub async fn verify_for_block(&self, db: &Db) -> Result<String, String> {
|
||||
self.verify_internal(db, false).await
|
||||
}
|
||||
|
||||
async fn verify_internal(
|
||||
&self,
|
||||
db: &Db,
|
||||
check_pending_payments: bool,
|
||||
) -> Result<String, String> {
|
||||
let hash = self.unsigned_contract_payment.hash().await;
|
||||
let signature = &self.signature;
|
||||
|
||||
|
|
@ -103,6 +116,20 @@ impl ContractPaymentTransaction {
|
|||
if payback_amount > remaining_balance {
|
||||
return Err("Payment exceeds the remaining contract balance".to_string());
|
||||
}
|
||||
if check_pending_payments {
|
||||
let pending_payments = get_pending_payments_for_contract(contract_hash)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to check pending loan payments: {err}"))?;
|
||||
let pending_with_new = pending_payments
|
||||
.checked_add(payback_amount)
|
||||
.ok_or_else(|| "Pending loan payment total overflowed.".to_string())?;
|
||||
if pending_with_new > remaining_balance {
|
||||
return Err(
|
||||
"Payment exceeds the remaining contract balance after pending payments."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Borrower payments must cover the fixed base-coin fee and an
|
||||
// asset-denominated tip of at least 1% of the payment amount.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::blocks::collateral::CollateralClaimTransaction;
|
||||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::common::loan_schedule::overdue_payment_count;
|
||||
use crate::common::types::COLLATERAL_FEE;
|
||||
use crate::records::memory::mempool::BASECOIN;
|
||||
use crate::records::wallet_registry::{
|
||||
|
|
@ -13,17 +14,38 @@ use crate::verifications::async_funcs::checks::mempool_check::memcheck;
|
|||
use crate::verifications::async_funcs::checks::verify_db::db_hex_verification;
|
||||
use crate::verifications::async_funcs::total_payments::get_total_payments;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::Utc;
|
||||
use crate::{decode, encode};
|
||||
use crate::{DateTime, Datelike, TimeZone, Utc};
|
||||
|
||||
impl CollateralClaimTransaction {
|
||||
pub async fn verify(&self, db: &Db) -> Result<String, String> {
|
||||
self.verify_internal(db, Utc::now().timestamp() as u32, true)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn verify_at(&self, db: &Db, as_of_timestamp: u32) -> Result<String, String> {
|
||||
self.verify_internal(db, as_of_timestamp, false).await
|
||||
}
|
||||
|
||||
async fn verify_internal(
|
||||
&self,
|
||||
db: &Db,
|
||||
as_of_timestamp: u32,
|
||||
allow_mempool_short_circuit: bool,
|
||||
) -> Result<String, String> {
|
||||
let hash = self.unsigned_collateral_claim.hash().await;
|
||||
let signature = &self.signature;
|
||||
|
||||
// Transactions already present in the mempool can short-circuit
|
||||
// the deeper verification path and reuse their stored signature.
|
||||
if memcheck(signature, &hash).await {
|
||||
if self.unsigned_collateral_claim.time > as_of_timestamp {
|
||||
return Err(
|
||||
"Collateral claim timestamp cannot be later than its containing block.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let already_in_mempool = memcheck(signature, &hash).await;
|
||||
// Submission can reuse an already verified mempool entry. Block
|
||||
// validation still checks the schedule against the block timestamp.
|
||||
if allow_mempool_short_circuit && already_in_mempool {
|
||||
return Ok(signature.to_string());
|
||||
}
|
||||
|
||||
|
|
@ -118,12 +140,14 @@ impl CollateralClaimTransaction {
|
|||
}
|
||||
|
||||
// Both possible claimant roles must be able to cover the base-coin fee.
|
||||
if address == contract.unsigned_loan_contract.lender
|
||||
if !already_in_mempool
|
||||
&& address == contract.unsigned_loan_contract.lender
|
||||
&& !balance_checkup(db, 0, txfee, BASECOIN.clone(), &address).await
|
||||
{
|
||||
return Err("Insuficient funds for this Transfer Transaction!".to_string());
|
||||
}
|
||||
if address == contract.unsigned_loan_contract.borrower
|
||||
if !already_in_mempool
|
||||
&& address == contract.unsigned_loan_contract.borrower
|
||||
&& !balance_checkup(db, 0, txfee, BASECOIN.clone(), &address).await
|
||||
{
|
||||
return Err("Insuficient funds for this Transfer Transaction!".to_string());
|
||||
|
|
@ -148,13 +172,13 @@ impl CollateralClaimTransaction {
|
|||
}
|
||||
}
|
||||
|
||||
// Lender claims depend on how many scheduled periods have elapsed,
|
||||
// excluding the initial funding period.
|
||||
let periods_elapsed = Self::time_passed(
|
||||
// A payment becomes overdue only after its entire UTC due date has passed.
|
||||
let payments_due = overdue_payment_count(
|
||||
contract.unsigned_loan_contract.timestamp,
|
||||
&contract.unsigned_loan_contract.payment_period,
|
||||
);
|
||||
let payments_due = periods_elapsed.saturating_sub(1);
|
||||
contract.unsigned_loan_contract.payment_number,
|
||||
as_of_timestamp,
|
||||
)?;
|
||||
|
||||
// The lender must wait until the grace period has expired.
|
||||
if payments_due <= contract.unsigned_loan_contract.grace_period as usize {
|
||||
|
|
@ -176,39 +200,4 @@ impl CollateralClaimTransaction {
|
|||
let sign = "";
|
||||
Ok(sign.to_string())
|
||||
}
|
||||
|
||||
fn time_passed(start_time: u32, period: &str) -> usize {
|
||||
// Convert the start timestamp into elapsed payment periods using
|
||||
// the contract's declared schedule unit.
|
||||
let start_datetime = Utc
|
||||
.timestamp_opt(start_time as i64, 0)
|
||||
.single()
|
||||
.expect("Invalid start timestamp");
|
||||
let current_timestamp = Utc::now().timestamp() as u32;
|
||||
let current_datetime = Utc
|
||||
.timestamp_opt(current_timestamp as i64, 0)
|
||||
.single()
|
||||
.expect("Invalid current timestamp");
|
||||
|
||||
let duration = current_datetime - start_datetime;
|
||||
|
||||
match period {
|
||||
"d" => duration.num_days() as usize,
|
||||
"w" => (duration.num_days() / 7) as usize,
|
||||
"m" => Self::calculate_months_between(start_datetime, current_datetime),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_months_between(start: DateTime<Utc>, end: DateTime<Utc>) -> usize {
|
||||
// Monthly contracts count whole calendar-month boundaries between
|
||||
// the contract start date and the current date.
|
||||
let start_date = start.date_naive();
|
||||
let end_date = end.date_naive();
|
||||
|
||||
let years_diff = end_date.year() - start_date.year();
|
||||
let months_diff = end_date.month() as i32 - start_date.month() as i32;
|
||||
|
||||
(years_diff * 12 + months_diff) as usize
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::common::asset_names::{
|
||||
is_canonical_padded_asset_identifier, is_canonical_padded_asset_or_base,
|
||||
is_canonical_loan_collateral_or_base, is_canonical_padded_asset_or_base,
|
||||
loan_collateral_asset_name_or_base, LOAN_COLLATERAL_LENGTH,
|
||||
};
|
||||
use crate::common::types::{COIN_LENGTH, LENDER_FEE};
|
||||
use crate::encode;
|
||||
|
|
@ -76,8 +77,12 @@ impl LoanContractTransaction {
|
|||
let loan_amount = self.unsigned_loan_contract.loan_amount;
|
||||
let txfee = self.unsigned_loan_contract.txfee;
|
||||
let loan_coin_length = &loan_coin.to_string().len();
|
||||
let collateral = self.unsigned_loan_contract.collateral.clone();
|
||||
let collateral_length = collateral.to_string().len();
|
||||
let collateral_field = self.unsigned_loan_contract.collateral.clone();
|
||||
let collateral = loan_collateral_asset_name_or_base(&collateral_field, &BASECOIN)
|
||||
.ok_or_else(|| {
|
||||
"Collateral must be the base coin, a token, or an NFT ending in _xxxxx.".to_string()
|
||||
})?;
|
||||
let collateral_length = collateral_field.len();
|
||||
let collateral_amount = self.unsigned_loan_contract.collateral_amount;
|
||||
|
||||
// Creating the loan contract has a fixed minimum lender fee.
|
||||
|
|
@ -88,14 +93,12 @@ impl LoanContractTransaction {
|
|||
}
|
||||
|
||||
// Asset names use the fixed padded coin-length format.
|
||||
if loan_coin_length != &COIN_LENGTH || collateral_length != COIN_LENGTH {
|
||||
return Err(
|
||||
"Coin/Token lengths must be 15 characters each. Consider padding with empty spaces",
|
||||
)
|
||||
if loan_coin_length != &COIN_LENGTH || collateral_length != LOAN_COLLATERAL_LENGTH {
|
||||
return Err("Loan assets must use a 15-byte loan coin and a 21-byte collateral field.")
|
||||
.map_err(|s| s.to_string())?;
|
||||
}
|
||||
if !is_canonical_padded_asset_or_base(&loan_coin, &BASECOIN)
|
||||
|| !is_canonical_padded_asset_identifier(&collateral)
|
||||
|| !is_canonical_loan_collateral_or_base(&collateral_field, &BASECOIN)
|
||||
{
|
||||
return Err(
|
||||
"Loan assets must use the base coin or canonical alphanumeric asset ids."
|
||||
|
|
@ -112,13 +115,14 @@ impl LoanContractTransaction {
|
|||
return Err("Coin/Token does not exist.".to_string());
|
||||
}
|
||||
|
||||
// Collateral can be a token or an NFT asset, but it must already
|
||||
// Collateral can be the base coin, a token, or an NFT asset, but it must already
|
||||
// exist before the contract can be created.
|
||||
let nft_tree = "nfts";
|
||||
if !(db_bytes_verification(db, tree, &collateral).await
|
||||
if !(collateral.trim().eq_ignore_ascii_case(BASECOIN.trim())
|
||||
|| db_bytes_verification(db, tree, &collateral).await
|
||||
|| db_bytes_verification(db, nft_tree, &collateral).await)
|
||||
{
|
||||
return Err("Coin/Token/Nft does not exist.".to_string());
|
||||
return Err("Coin/Token/NFT does not exist.".to_string());
|
||||
}
|
||||
|
||||
// The lender must be able to fund the loan and fee, while the
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ impl MarketingTransaction {
|
|||
|
||||
// Ad payloads are fixed-format records with a constrained ad type
|
||||
// and fixed-length keyword and displayed-link fields.
|
||||
let ad = &self.unsigned_marketing.ad_type;
|
||||
let ad = self.unsigned_marketing.ad_type.trim();
|
||||
if ad != "social" && ad != "banner" && ad != "text" {
|
||||
return Err("Wrong ad type. An ad must be type social, banner or text. If text you must add 2 spaces at the end.").map_err(|s| s.to_string())?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ impl VerificationService {
|
|||
miner: String,
|
||||
transactions: Vec<Transaction>,
|
||||
db: crate::sled::Db,
|
||||
block_timestamp: u32,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let (reply_tx, reply_rx) = oneshot::channel();
|
||||
let request = VerificationRequest {
|
||||
|
|
@ -84,6 +85,7 @@ impl VerificationService {
|
|||
miner,
|
||||
transactions,
|
||||
db,
|
||||
block_timestamp,
|
||||
},
|
||||
reply_tx,
|
||||
};
|
||||
|
|
@ -126,6 +128,7 @@ impl VerificationService {
|
|||
&chunk_job.db,
|
||||
stop_flag,
|
||||
balance_tracker,
|
||||
chunk_job.block_timestamp,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
@ -167,6 +170,7 @@ impl VerificationService {
|
|||
miner,
|
||||
transactions,
|
||||
db,
|
||||
block_timestamp,
|
||||
} = job;
|
||||
|
||||
for chunk in transactions.chunks(chunk_size) {
|
||||
|
|
@ -174,6 +178,7 @@ impl VerificationService {
|
|||
miner: miner.clone(),
|
||||
transactions: chunk.to_vec(),
|
||||
db: db.clone(),
|
||||
block_timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ pub struct VerificationJob {
|
|||
pub miner: String,
|
||||
pub transactions: Vec<Transaction>,
|
||||
pub db: Db,
|
||||
pub block_timestamp: u32,
|
||||
}
|
||||
|
||||
// VerificationChunkJob is the per-worker slice created from a larger verification job.
|
||||
|
|
@ -13,6 +14,7 @@ pub struct VerificationChunkJob {
|
|||
pub miner: String,
|
||||
pub transactions: Vec<Transaction>,
|
||||
pub db: Db,
|
||||
pub block_timestamp: u32,
|
||||
}
|
||||
|
||||
// VerificationChunkResult carries one worker's results plus a flag for chunk-level failure.
|
||||
|
|
|
|||
|
|
@ -46,8 +46,9 @@ impl Wallet {
|
|||
|
||||
let public_key_bytes = keypair.public_key().to_vec();
|
||||
if !Self::has_valid_public_key_bytes(&public_key_bytes) {
|
||||
return Err("FN-DSA public key does not satisfy this network's wallet key rule."
|
||||
.to_string());
|
||||
return Err(
|
||||
"FN-DSA public key does not satisfy this network's wallet key rule.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(public_key_bytes)
|
||||
|
|
|
|||
|
|
@ -39,11 +39,14 @@ impl Wallet {
|
|||
if let Some(encrypted_text) = decode_image_and_extract_text(&wallet.private_key) {
|
||||
// Decrypt the private key with the user-provided wallet key.
|
||||
if let Some(decrypted_private_key) = decrypts(&encrypted_text, Some(&wallet_key)) {
|
||||
let regenerated_public_key = Self::regenerate_public_key(&decrypted_private_key)?;
|
||||
let regenerated_public_key =
|
||||
Self::regenerate_public_key(&decrypted_private_key)?;
|
||||
let regenerated_public_key_hex = crate::encode(regenerated_public_key.clone());
|
||||
if regenerated_public_key_hex != wallet.public_key {
|
||||
return Err("Wallet public key does not match the decrypted private key."
|
||||
.to_string());
|
||||
return Err(
|
||||
"Wallet public key does not match the decrypted private key."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let regenerated_short_address_bytes =
|
||||
|
|
@ -53,10 +56,9 @@ impl Wallet {
|
|||
.to_string()
|
||||
})?;
|
||||
let regenerated_short_address =
|
||||
Self::bytes_to_short_address(®enerated_short_address_bytes)
|
||||
.ok_or_else(|| {
|
||||
"Wallet public key produced an invalid short address.".to_string()
|
||||
})?;
|
||||
Self::bytes_to_short_address(®enerated_short_address_bytes).ok_or_else(
|
||||
|| "Wallet public key produced an invalid short address.".to_string(),
|
||||
)?;
|
||||
if regenerated_short_address != wallet.short_address {
|
||||
return Err(
|
||||
"Wallet short address does not match the decrypted private key."
|
||||
|
|
|
|||
Loading…
Reference in New Issue