404 lines
12 KiB
Rust
404 lines
12 KiB
Rust
use crate::blocks::loans::LoanContractTransaction;
|
|
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};
|
|
use crate::config::SETTINGS;
|
|
use crate::decode;
|
|
use crate::lazy_static;
|
|
use crate::records::memory::structs::BalanceKey;
|
|
use crate::records::wallet_registry::resolve_canonical_registered_short_address;
|
|
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::HashMap;
|
|
use crate::NoTls;
|
|
use crate::{task, AtomicBool};
|
|
use anyhow::{anyhow, Result};
|
|
use once_cell::sync::OnceCell;
|
|
use std::fs::File;
|
|
use std::io::Write;
|
|
use tokio_postgres::Client;
|
|
|
|
lazy_static! {
|
|
pub static ref BASECOIN: String = {
|
|
let (
|
|
_network_name,
|
|
base_coin,
|
|
_suffix,
|
|
_torrentpath,
|
|
_wallet_path,
|
|
_blockpath,
|
|
_db_path,
|
|
_balance_path,
|
|
_log_path,
|
|
) = block_extension_and_paths();
|
|
format!("{base_coin:<15}")
|
|
};
|
|
static ref CLEANUP_RUNNING: AtomicBool = AtomicBool::new(false);
|
|
}
|
|
|
|
pub static DB: OnceCell<Client> = OnceCell::new();
|
|
|
|
pub const EPOCH_ROW_CAP: i64 = 100_000;
|
|
const NFT_UNIT: i64 = 100_000_000;
|
|
const CLEANUP_DEPTH: u32 = 10;
|
|
const CLEANUP_BATCH_LIMIT: i64 = 1000;
|
|
|
|
#[derive(Clone)]
|
|
enum SelectedMempoolTransaction {
|
|
// These variants hold the minimal fields needed to score, mark, and
|
|
// later apply selected mempool transactions into a saved block.
|
|
Transfer {
|
|
id: i64,
|
|
fee: i64,
|
|
sender: String,
|
|
value: i64,
|
|
coin: String,
|
|
nft_series: i32,
|
|
receiver: String,
|
|
hash: String,
|
|
},
|
|
Token {
|
|
id: i64,
|
|
fee: i64,
|
|
creator: String,
|
|
number: i64,
|
|
ticker: String,
|
|
hash: String,
|
|
},
|
|
IssueToken {
|
|
id: i64,
|
|
fee: i64,
|
|
creator: String,
|
|
number: i64,
|
|
ticker: String,
|
|
hash: String,
|
|
},
|
|
Burn {
|
|
id: i64,
|
|
fee: i64,
|
|
address: String,
|
|
coin: String,
|
|
nft_series: i32,
|
|
value: i64,
|
|
hash: String,
|
|
},
|
|
Nft {
|
|
id: i64,
|
|
fee: i64,
|
|
creator: String,
|
|
nft_name: String,
|
|
series: i16,
|
|
count: i64,
|
|
hash: String,
|
|
},
|
|
Marketing {
|
|
id: i64,
|
|
fee: i64,
|
|
advertiser: String,
|
|
hash: String,
|
|
},
|
|
Vanity {
|
|
id: i64,
|
|
fee: i64,
|
|
address: String,
|
|
vanity_address: String,
|
|
hash: String,
|
|
},
|
|
Swap {
|
|
id: i64,
|
|
fee1: i64,
|
|
fee2: i64,
|
|
ticker1: String,
|
|
nft_series1: i32,
|
|
ticker2: String,
|
|
nft_series2: i32,
|
|
value1: i64,
|
|
value2: i64,
|
|
sender1: String,
|
|
tip1: i64,
|
|
tip2: i64,
|
|
sender2: String,
|
|
hash: String,
|
|
},
|
|
Lender {
|
|
id: i64,
|
|
fee: i64,
|
|
loan_coin: String,
|
|
loan_amount: i64,
|
|
lender: String,
|
|
collateral: String,
|
|
collateral_amount: i64,
|
|
borrower: String,
|
|
txid: String,
|
|
hash: String,
|
|
},
|
|
Borrower {
|
|
id: i64,
|
|
fee: i64,
|
|
payback_amount: i64,
|
|
contract_hash: String,
|
|
address: String,
|
|
tip: i64,
|
|
hash: String,
|
|
},
|
|
Collateral {
|
|
id: i64,
|
|
fee: i64,
|
|
address: String,
|
|
contract_hash: String,
|
|
hash: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct SelectedMempoolBatch {
|
|
// The selected transaction view is kept separate from the original
|
|
// serialized bytes so save paths can stream the original payloads.
|
|
transactions: Vec<SelectedMempoolTransaction>,
|
|
originals: Vec<Vec<u8>>,
|
|
}
|
|
|
|
impl SelectedMempoolTransaction {
|
|
fn table_name(&self) -> &'static str {
|
|
match self {
|
|
SelectedMempoolTransaction::Transfer { .. } => "transfer",
|
|
SelectedMempoolTransaction::Token { .. } => "token",
|
|
SelectedMempoolTransaction::IssueToken { .. } => "issue_token",
|
|
SelectedMempoolTransaction::Burn { .. } => "burn",
|
|
SelectedMempoolTransaction::Nft { .. } => "nft",
|
|
SelectedMempoolTransaction::Marketing { .. } => "marketing",
|
|
SelectedMempoolTransaction::Vanity { .. } => "vanity_address",
|
|
SelectedMempoolTransaction::Swap { .. } => "swap",
|
|
SelectedMempoolTransaction::Lender { .. } => "loan_contract",
|
|
SelectedMempoolTransaction::Borrower { .. } => "loan_payment",
|
|
SelectedMempoolTransaction::Collateral { .. } => "collateral_claim",
|
|
}
|
|
}
|
|
|
|
fn id(&self) -> i64 {
|
|
match self {
|
|
SelectedMempoolTransaction::Transfer { id, .. }
|
|
| SelectedMempoolTransaction::Token { id, .. }
|
|
| SelectedMempoolTransaction::IssueToken { id, .. }
|
|
| SelectedMempoolTransaction::Burn { id, .. }
|
|
| SelectedMempoolTransaction::Nft { id, .. }
|
|
| SelectedMempoolTransaction::Marketing { id, .. }
|
|
| SelectedMempoolTransaction::Vanity { id, .. }
|
|
| SelectedMempoolTransaction::Swap { id, .. }
|
|
| SelectedMempoolTransaction::Lender { id, .. }
|
|
| SelectedMempoolTransaction::Borrower { id, .. }
|
|
| SelectedMempoolTransaction::Collateral { id, .. } => *id,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SelectedMempoolBatch {
|
|
pub fn is_empty(&self) -> bool {
|
|
self.transactions.is_empty()
|
|
}
|
|
}
|
|
|
|
mod lookups;
|
|
mod processing;
|
|
mod schema;
|
|
mod selection;
|
|
|
|
pub use lookups::{
|
|
get_basecoin_balance, get_coin_balance, get_pending_payments_for_contract, largest_fee,
|
|
signature_exists, total_transactions, transaction_by_signature, transactions_by_address,
|
|
};
|
|
pub use processing::{
|
|
delete_by_signatures, mark_processed_by_signatures, mark_selected_transactions_processed,
|
|
restore_processed_by_signatures, restore_selected_transactions_processed,
|
|
spawn_processed_cleanup,
|
|
};
|
|
pub use schema::{clear_mempool, init_db, setup_mempool};
|
|
pub use selection::{
|
|
apply_selected_transaction_math, clear_selected_transaction_sql, delete_selected_transactions,
|
|
select_transactions_for_block, stream_selected_transaction_originals,
|
|
};
|
|
|
|
fn required_string(row: &tokio_postgres::Row, column: &str) -> Result<String> {
|
|
row.try_get::<_, Option<String>>(column)?
|
|
.ok_or_else(|| anyhow!("Missing required column {column}"))
|
|
}
|
|
|
|
fn add_balance_change(
|
|
db: &Db,
|
|
balance_changes: &mut HashMap<BalanceKey, i64>,
|
|
address: &str,
|
|
coin: &str,
|
|
delta: i64,
|
|
) {
|
|
add_balance_change_bytes(
|
|
balance_changes,
|
|
address_key_bytes(db, address),
|
|
coin.as_bytes().to_vec(),
|
|
delta,
|
|
);
|
|
}
|
|
|
|
fn add_balance_change_bytes(
|
|
balance_changes: &mut HashMap<BalanceKey, i64>,
|
|
address: Vec<u8>,
|
|
coin: Vec<u8>,
|
|
delta: i64,
|
|
) {
|
|
*balance_changes
|
|
.entry(BalanceKey { address, coin })
|
|
.or_insert(0) += delta;
|
|
}
|
|
|
|
fn address_key_bytes(db: &Db, address: &str) -> Vec<u8> {
|
|
resolve_canonical_registered_short_address(db, address)
|
|
.ok()
|
|
.flatten()
|
|
.or_else(|| Wallet::normalize_to_short_address(address))
|
|
.map(|normalized| normalized.as_bytes().to_vec())
|
|
.unwrap_or_else(|| address.as_bytes().to_vec())
|
|
}
|
|
|
|
fn canonical_mempool_addresses(db: &Db, address: &str) -> Vec<String> {
|
|
let canonical = resolve_canonical_registered_short_address(db, address)
|
|
.ok()
|
|
.flatten()
|
|
.or_else(|| Wallet::normalize_to_short_address(address))
|
|
.unwrap_or_else(|| address.to_string());
|
|
|
|
vec![canonical]
|
|
}
|
|
|
|
async fn resolve_loan_details(db: &Db, contract_hash: &str) -> Result<(Vec<u8>, Vec<u8>)> {
|
|
let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, decode(contract_hash)?).await;
|
|
if bytes.is_empty() || bytes[0] != 7 {
|
|
return Ok((Vec::new(), Vec::new()));
|
|
}
|
|
match LoanContractTransaction::from_bytes(7, &bytes[1..]).await {
|
|
Ok(loan) => Ok((
|
|
loan.unsigned_loan_contract.loan_coin.as_bytes().to_vec(),
|
|
address_key_bytes(db, &loan.unsigned_loan_contract.lender),
|
|
)),
|
|
Err(_) => Ok((Vec::new(), Vec::new())),
|
|
}
|
|
}
|
|
|
|
async fn resolve_collateral_details(db: &Db, contract_hash: &str) -> Result<(Vec<u8>, i64)> {
|
|
let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, decode(contract_hash)?).await;
|
|
if bytes.is_empty() || bytes[0] != 7 {
|
|
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(),
|
|
loan.unsigned_loan_contract.collateral_amount as i64,
|
|
)),
|
|
Err(_) => Ok((Vec::new(), 0)),
|
|
}
|
|
}
|
|
|
|
fn ids_for_table(batch: &SelectedMempoolBatch, table: &str) -> Vec<i64> {
|
|
batch
|
|
.transactions
|
|
.iter()
|
|
.filter(|tx| tx.table_name() == table)
|
|
.map(SelectedMempoolTransaction::id)
|
|
.collect()
|
|
}
|
|
|
|
async fn mark_rows_by_ids(
|
|
client: &Client,
|
|
table: &str,
|
|
ids: &[i64],
|
|
block_number: i32,
|
|
) -> Result<()> {
|
|
if ids.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let statement =
|
|
format!("UPDATE {table} SET processed=true, processed_block_number=$1 WHERE id = ANY($2)");
|
|
client.execute(&statement, &[&block_number, &ids]).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn unmark_rows_by_ids(client: &Client, table: &str, ids: &[i64]) -> Result<()> {
|
|
if ids.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let statement = format!(
|
|
"UPDATE {table} SET processed=false, processed_block_number=NULL WHERE id = ANY($1)"
|
|
);
|
|
client.execute(&statement, &[&ids]).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn delete_rows(client: &Client, table: &str, ids: &[i64]) -> Result<()> {
|
|
if ids.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let statement = format!("DELETE FROM {table} WHERE id = ANY($1)");
|
|
client.execute(&statement, &[&ids]).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn unmark_by_signatures(
|
|
client: &Client,
|
|
table: &str,
|
|
signature_column: &str,
|
|
signatures: &[String],
|
|
) -> Result<u64> {
|
|
let statement = format!(
|
|
"UPDATE {table} SET processed=false, processed_block_number=NULL WHERE {signature_column} = ANY($1) AND processed = true"
|
|
);
|
|
Ok(client.execute(&statement, &[&signatures]).await?)
|
|
}
|
|
|
|
async fn delete_processed_before_or_at(block_number: u32, limit: i64) -> Result<()> {
|
|
// Periodic cleanup deletes processed mempool rows in bounded batches
|
|
// so long-lived nodes do not accumulate infinite processed history.
|
|
let client = DB.get().expect("DB not initialized");
|
|
let bn = block_number as i32;
|
|
|
|
delete_processed_rows_limited(client, "transfer", bn, limit).await?;
|
|
delete_processed_rows_limited(client, "token", bn, limit).await?;
|
|
delete_processed_rows_limited(client, "issue_token", bn, limit).await?;
|
|
delete_processed_rows_limited(client, "burn", bn, limit).await?;
|
|
delete_processed_rows_limited(client, "nft", bn, limit).await?;
|
|
delete_processed_rows_limited(client, "marketing", bn, limit).await?;
|
|
delete_processed_rows_limited(client, "vanity_address", bn, limit).await?;
|
|
delete_processed_rows_limited(client, "swap", bn, limit).await?;
|
|
delete_processed_rows_limited(client, "loan_contract", bn, limit).await?;
|
|
delete_processed_rows_limited(client, "loan_payment", bn, limit).await?;
|
|
delete_processed_rows_limited(client, "collateral_claim", bn, limit).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn delete_processed_rows_limited(
|
|
client: &Client,
|
|
table: &str,
|
|
block_number: i32,
|
|
limit: i64,
|
|
) -> Result<u64> {
|
|
let statement = format!(
|
|
r#"
|
|
DELETE FROM {table}
|
|
WHERE id IN (
|
|
SELECT id
|
|
FROM {table}
|
|
WHERE processed = true
|
|
AND processed_block_number IS NOT NULL
|
|
AND processed_block_number <= $1
|
|
ORDER BY processed_block_number ASC, id ASC
|
|
LIMIT $2
|
|
)
|
|
"#
|
|
);
|
|
|
|
Ok(client.execute(&statement, &[&block_number, &limit]).await?)
|
|
}
|