Fix staged torrent candidate race
This commit is contained in:
parent
f92823ac90
commit
61a64cf538
|
|
@ -11,10 +11,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
|
|||
// Pad the asset name so it matches the 15-byte on-chain asset format.
|
||||
fn pad_to_width(input: &str, width: usize) -> String {
|
||||
let mut result = String::with_capacity(width);
|
||||
let _ = std::fmt::write(
|
||||
&mut result,
|
||||
format_args!("{input:<width$}"),
|
||||
);
|
||||
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
|
||||
result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
|
|||
// Pad the ticker so it matches the fixed-width 15-byte on-chain format.
|
||||
fn pad_to_width(input: &str, width: usize) -> String {
|
||||
let mut result = String::with_capacity(width);
|
||||
let _ = std::fmt::write(
|
||||
&mut result,
|
||||
format_args!("{input:<width$}"),
|
||||
);
|
||||
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
|
||||
result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,7 @@ use blockchain::{Local, LocalResult, NaiveDate, NaiveTime, TimeZone};
|
|||
fn pad_to_width(input: &str, width: usize) -> String {
|
||||
// Asset names are fixed-width fields in the loan transaction bytes.
|
||||
let mut result = String::with_capacity(width);
|
||||
let _ = std::fmt::write(
|
||||
&mut result,
|
||||
format_args!("{input:<width$}"),
|
||||
);
|
||||
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
|
||||
result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
|
|||
// pad the coin to ensure 15 characters
|
||||
fn pad_to_width(input: &str, width: usize) -> String {
|
||||
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
|
||||
let _ = std::fmt::write(
|
||||
&mut result,
|
||||
format_args!("{input:<width$}"),
|
||||
);
|
||||
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
|
||||
result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,9 +40,7 @@ async fn main() {
|
|||
// Refuse to overwrite an existing wallet file.
|
||||
if let Ok(metadata) = metadata(&wallet_path).await {
|
||||
if metadata.is_file() {
|
||||
eprintln!(
|
||||
"Error: Wallet already exists at the specified path: {wallet_path:?}"
|
||||
);
|
||||
eprintln!("Error: Wallet already exists at the specified path: {wallet_path:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
|
|||
// pad the coin to ensure 15 characters
|
||||
fn pad_to_width(input: &str, width: usize) -> String {
|
||||
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
|
||||
let _ = std::fmt::write(
|
||||
&mut result,
|
||||
format_args!("{input:<width$}"),
|
||||
);
|
||||
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
|
||||
result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
|
|||
// pad the coin to ensure 15 characters
|
||||
fn pad_to_width(input: &str, width: usize) -> String {
|
||||
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
|
||||
let _ = std::fmt::write(
|
||||
&mut result,
|
||||
format_args!("{input:<width$}"),
|
||||
);
|
||||
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
|
||||
result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
|
|||
// pad the coin to ensure 15 characters
|
||||
fn pad_to_width(input: &str, width: usize) -> String {
|
||||
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
|
||||
let _ = std::fmt::write(
|
||||
&mut result,
|
||||
format_args!("{input:<width$}"),
|
||||
);
|
||||
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
|
||||
result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
|
|||
// pad the coin to ensure 15 characters
|
||||
fn pad_to_width(input: &str, width: usize) -> String {
|
||||
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
|
||||
let _ = std::fmt::write(
|
||||
&mut result,
|
||||
format_args!("{input:<width$}"),
|
||||
);
|
||||
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
|
||||
result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,17 +26,50 @@ async fn decode_one_transaction(tx_bytes: &[u8]) -> Option<String> {
|
|||
let body = &tx_bytes[1..];
|
||||
|
||||
match txtype {
|
||||
TRANSFER_TYPE => to_string_pretty(&TransferTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
CREATE_TOKEN_TYPE => to_string_pretty(&CreateTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
CREATE_NFT_TYPE => to_string_pretty(&CreateNftTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
MARKETING_TYPE => to_string_pretty(&MarketingTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
TRANSFER_TYPE => {
|
||||
to_string_pretty(&TransferTransaction::from_bytes(txtype, body).await.ok()?).ok()
|
||||
}
|
||||
CREATE_TOKEN_TYPE => to_string_pretty(
|
||||
&CreateTokenTransaction::from_bytes(txtype, body)
|
||||
.await
|
||||
.ok()?,
|
||||
)
|
||||
.ok(),
|
||||
CREATE_NFT_TYPE => {
|
||||
to_string_pretty(&CreateNftTransaction::from_bytes(txtype, body).await.ok()?).ok()
|
||||
}
|
||||
MARKETING_TYPE => {
|
||||
to_string_pretty(&MarketingTransaction::from_bytes(txtype, body).await.ok()?).ok()
|
||||
}
|
||||
SWAP_TYPE => to_string_pretty(&SwapTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
LENDER_TYPE => to_string_pretty(&LoanContractTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
BORROWER_TYPE => to_string_pretty(&ContractPaymentTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
COLLATERAL_TYPE => to_string_pretty(&CollateralClaimTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
LENDER_TYPE => to_string_pretty(
|
||||
&LoanContractTransaction::from_bytes(txtype, body)
|
||||
.await
|
||||
.ok()?,
|
||||
)
|
||||
.ok(),
|
||||
BORROWER_TYPE => to_string_pretty(
|
||||
&ContractPaymentTransaction::from_bytes(txtype, body)
|
||||
.await
|
||||
.ok()?,
|
||||
)
|
||||
.ok(),
|
||||
COLLATERAL_TYPE => to_string_pretty(
|
||||
&CollateralClaimTransaction::from_bytes(txtype, body)
|
||||
.await
|
||||
.ok()?,
|
||||
)
|
||||
.ok(),
|
||||
BURN_TYPE => to_string_pretty(&BurnTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
ISSUE_TOKEN_TYPE => to_string_pretty(&IssueTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
VANITY_ADDRESS_TYPE => to_string_pretty(&VanityAddressTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
ISSUE_TOKEN_TYPE => {
|
||||
to_string_pretty(&IssueTokenTransaction::from_bytes(txtype, body).await.ok()?).ok()
|
||||
}
|
||||
VANITY_ADDRESS_TYPE => to_string_pretty(
|
||||
&VanityAddressTransaction::from_bytes(txtype, body)
|
||||
.await
|
||||
.ok()?,
|
||||
)
|
||||
.ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,17 +27,50 @@ async fn decode_mempool_transaction(response: &[u8]) -> Option<String> {
|
|||
let body = &response[1..];
|
||||
|
||||
match txtype {
|
||||
TRANSFER_TYPE => to_string_pretty(&TransferTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
CREATE_TOKEN_TYPE => to_string_pretty(&CreateTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
CREATE_NFT_TYPE => to_string_pretty(&CreateNftTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
MARKETING_TYPE => to_string_pretty(&MarketingTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
TRANSFER_TYPE => {
|
||||
to_string_pretty(&TransferTransaction::from_bytes(txtype, body).await.ok()?).ok()
|
||||
}
|
||||
CREATE_TOKEN_TYPE => to_string_pretty(
|
||||
&CreateTokenTransaction::from_bytes(txtype, body)
|
||||
.await
|
||||
.ok()?,
|
||||
)
|
||||
.ok(),
|
||||
CREATE_NFT_TYPE => {
|
||||
to_string_pretty(&CreateNftTransaction::from_bytes(txtype, body).await.ok()?).ok()
|
||||
}
|
||||
MARKETING_TYPE => {
|
||||
to_string_pretty(&MarketingTransaction::from_bytes(txtype, body).await.ok()?).ok()
|
||||
}
|
||||
SWAP_TYPE => to_string_pretty(&SwapTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
LENDER_TYPE => to_string_pretty(&LoanContractTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
BORROWER_TYPE => to_string_pretty(&ContractPaymentTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
COLLATERAL_TYPE => to_string_pretty(&CollateralClaimTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
LENDER_TYPE => to_string_pretty(
|
||||
&LoanContractTransaction::from_bytes(txtype, body)
|
||||
.await
|
||||
.ok()?,
|
||||
)
|
||||
.ok(),
|
||||
BORROWER_TYPE => to_string_pretty(
|
||||
&ContractPaymentTransaction::from_bytes(txtype, body)
|
||||
.await
|
||||
.ok()?,
|
||||
)
|
||||
.ok(),
|
||||
COLLATERAL_TYPE => to_string_pretty(
|
||||
&CollateralClaimTransaction::from_bytes(txtype, body)
|
||||
.await
|
||||
.ok()?,
|
||||
)
|
||||
.ok(),
|
||||
BURN_TYPE => to_string_pretty(&BurnTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
ISSUE_TOKEN_TYPE => to_string_pretty(&IssueTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
VANITY_ADDRESS_TYPE => to_string_pretty(&VanityAddressTransaction::from_bytes(txtype, body).await.ok()?).ok(),
|
||||
ISSUE_TOKEN_TYPE => {
|
||||
to_string_pretty(&IssueTokenTransaction::from_bytes(txtype, body).await.ok()?).ok()
|
||||
}
|
||||
VANITY_ADDRESS_TYPE => to_string_pretty(
|
||||
&VanityAddressTransaction::from_bytes(txtype, body)
|
||||
.await
|
||||
.ok()?,
|
||||
)
|
||||
.ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,10 +45,9 @@ fn decode_network_info(response: &[u8]) -> Option<String> {
|
|||
|
||||
// Mainnet uses CLC and testnet uses CLTC, so the prefix length is
|
||||
// inferred from the total payload size instead of hard-coded.
|
||||
let wallet_prefix =
|
||||
String::from_utf8_lossy(response.get(offset..offset + wallet_prefix_len)?)
|
||||
.trim()
|
||||
.to_string();
|
||||
let wallet_prefix = String::from_utf8_lossy(response.get(offset..offset + wallet_prefix_len)?)
|
||||
.trim()
|
||||
.to_string();
|
||||
offset += wallet_prefix_len;
|
||||
|
||||
let height = read_u32(response, &mut offset)?;
|
||||
|
|
|
|||
|
|
@ -89,9 +89,7 @@ async fn main() {
|
|||
let address_file_path = if args.len() == 2 {
|
||||
args[1].clone()
|
||||
} else {
|
||||
match prompt_for_path(
|
||||
"Please enter the path to the file containing the wallet address: ",
|
||||
) {
|
||||
match prompt_for_path("Please enter the path to the file containing the wallet address: ") {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use blockchain::common::skein::{
|
||||
skein_256_hash_data, skein_256_hash_bytes, skein_128_hash_bytes, skein_128_hash_data,
|
||||
skein_128_hash_bytes, skein_128_hash_data, skein_256_hash_bytes, skein_256_hash_data,
|
||||
};
|
||||
use blockchain::env;
|
||||
use blockchain::File;
|
||||
|
|
|
|||
|
|
@ -1,189 +1,189 @@
|
|||
use blockchain::common::binary_conversions::hex_to_u64;
|
||||
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use blockchain::common::skein::{skein_256_hash_data, skein_128_hash_bytes};
|
||||
use blockchain::encode;
|
||||
use blockchain::env;
|
||||
use blockchain::records::unpack_block::unpack_header::load_block_header;
|
||||
use blockchain::records::wallet_registry::resolve_pubkey_from_short_address;
|
||||
use blockchain::torrent::structs::Torrent;
|
||||
use blockchain::wallets::structures::Wallet;
|
||||
use blockchain::{AsyncReadExt, File};
|
||||
use colored::*;
|
||||
use std::process;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Validate that a local block file, block header, and torrent metadata agree.
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() != 2 {
|
||||
eprintln!("Usage: {} <block_number>", args[0]);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let block_number: u32 = match args[1].parse() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
eprintln!("Block number must be an integer.");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
block_ext,
|
||||
torrent_path,
|
||||
_wallet_path,
|
||||
block_path,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let block_filename = format!("{block_path}/{block_number}.{block_ext}");
|
||||
let torrent_filename = format!("{torrent_path}/{block_number}.torrent");
|
||||
|
||||
// Load and decode the torrent metadata first because later checks compare against it.
|
||||
let mut torrent_bytes = Vec::new();
|
||||
let mut torrent_file = File::open(&torrent_filename).await.unwrap_or_else(|_| {
|
||||
eprintln!("Error: cannot open torrent file '{torrent_filename}'");
|
||||
process::exit(1);
|
||||
});
|
||||
torrent_file.read_to_end(&mut torrent_bytes).await.unwrap();
|
||||
let torrent = Torrent::from_bytes(&torrent_bytes).await.unwrap();
|
||||
|
||||
// Load the local header and resolve the miner public key for signature/VRF checks.
|
||||
let header = load_block_header(block_number).await.unwrap();
|
||||
let block_hash = header.hash().await;
|
||||
let block_difficulty = hex_to_u64(&block_hash).await.unwrap();
|
||||
let miner_pubkey = resolve_pubkey_from_short_address(
|
||||
&blockchain::startup::initialize_startup::open_chain_state().await,
|
||||
&header.unmined_block.miner,
|
||||
)
|
||||
.unwrap_or(None)
|
||||
.unwrap_or_default();
|
||||
let miner_pubkey_hex = encode(&miner_pubkey);
|
||||
|
||||
// Load the raw block bytes for file-size, info-hash, and piece-hash checks.
|
||||
let mut block_data = Vec::new();
|
||||
let mut block_file = File::open(&block_filename).await.unwrap_or_else(|_| {
|
||||
eprintln!("Error: cannot open block file '{block_filename}'");
|
||||
process::exit(1);
|
||||
});
|
||||
block_file.read_to_end(&mut block_data).await.unwrap();
|
||||
|
||||
let info_hash_computed = skein_128_hash_bytes(&block_data);
|
||||
|
||||
// Rebuild the unmined-block hash used by the miner proof signature.
|
||||
let unmined_json = serde_json::to_string(&header.unmined_block).unwrap();
|
||||
let unmined_hash = skein_256_hash_data(&unmined_json);
|
||||
let signature_ok =
|
||||
Wallet::verify_transaction_with_public_key(&unmined_hash, &header.proof, &miner_pubkey_hex)
|
||||
.await;
|
||||
|
||||
let passed = "[PASSED]".green();
|
||||
let failed = "[FAILED]".red();
|
||||
|
||||
// Compare every header field that is duplicated inside the torrent metadata.
|
||||
if header.unmined_block.timestamp == torrent.info.timestamp {
|
||||
println!("timestamp match: {:>90}", format!("{passed}"));
|
||||
} else {
|
||||
println!("timestamp match: {:>90}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if header.unmined_block.nonce == torrent.info.nonce {
|
||||
println!("nonce match: {:>94}", format!("{passed}"));
|
||||
} else {
|
||||
println!("nonce match: {:>94}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if header.unmined_block.miner == torrent.mined_by {
|
||||
println!("wallet address match: {:>85}", format!("{passed}"));
|
||||
} else {
|
||||
println!("wallet address match: {:>85}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if block_difficulty < torrent.info.this_block_difficulty {
|
||||
println!("block difficulty check: {:>83}", format!("{passed}"));
|
||||
} else {
|
||||
println!("block difficulty check: {:>83}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if header.vrf == torrent.info.vrf {
|
||||
println!("VRF match: {:>96}", format!("{passed}"));
|
||||
} else {
|
||||
println!("VRF match: {:>96}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if block_data.len() == torrent.info.length as usize {
|
||||
println!("file size check: {:>90}", format!("{passed}"));
|
||||
} else {
|
||||
println!("file size check: {:>90}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if block_hash == torrent.info.block_hash {
|
||||
println!("block header hash check: {:>82}", format!("{passed}"));
|
||||
} else {
|
||||
println!("block header hash check: {:>82}", format!("{failed}"));
|
||||
}
|
||||
|
||||
let vrf_ok = Wallet::vrf_verify_with_public_key(
|
||||
header.vrf,
|
||||
&unmined_hash,
|
||||
&miner_pubkey_hex,
|
||||
&header.proof,
|
||||
)
|
||||
.await;
|
||||
if vrf_ok {
|
||||
println!("VRF validation check: {:>85}", format!("{passed}"));
|
||||
} else {
|
||||
println!("VRF validation check: {:>85}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if signature_ok {
|
||||
println!("VRF Proof check: {:>90}", format!("{passed}"));
|
||||
} else {
|
||||
println!("VRF Proof check: {:>90}", format!("{failed}"));
|
||||
}
|
||||
|
||||
let mut all_pieces_passed = true;
|
||||
// Piece hashes prove the torrent metadata still matches every block-file chunk.
|
||||
for piece in &torrent.info.pieces {
|
||||
for (index, expected_hash) in piece.iter() {
|
||||
let idx = *index as usize;
|
||||
let start = (idx - 1) * torrent.info.piece_length as usize;
|
||||
let end = std::cmp::min(start + torrent.info.piece_length as usize, block_data.len());
|
||||
let slice = &block_data[start..end];
|
||||
let hash = skein_128_hash_bytes(slice);
|
||||
|
||||
if hash == *expected_hash {
|
||||
println!("piece {} hash check: {:>87}", idx, format!("{passed}"));
|
||||
} else {
|
||||
all_pieces_passed = false;
|
||||
println!("piece {} hash check: {:>87}", idx, format!("{failed}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if info_hash_computed == torrent.info.info_hash {
|
||||
println!("block hash check: {:>89}", format!("{passed}"));
|
||||
} else {
|
||||
println!("block hash check: {:>89}", format!("{failed}"));
|
||||
}
|
||||
|
||||
// The final pass/fail summary requires every individual validation to pass.
|
||||
if header.unmined_block.nonce == torrent.info.nonce
|
||||
&& header.unmined_block.miner == torrent.mined_by
|
||||
&& header.unmined_block.timestamp == torrent.info.timestamp
|
||||
&& block_difficulty < torrent.info.this_block_difficulty
|
||||
&& header.vrf == torrent.info.vrf
|
||||
&& block_data.len() == torrent.info.length as usize
|
||||
&& all_pieces_passed
|
||||
&& block_hash == torrent.info.block_hash
|
||||
&& signature_ok
|
||||
&& vrf_ok
|
||||
{
|
||||
println!("\nBlock {block_number} fully validated.");
|
||||
} else {
|
||||
println!("\nBlock {block_number} FAILED validation.");
|
||||
}
|
||||
}
|
||||
use blockchain::common::binary_conversions::hex_to_u64;
|
||||
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use blockchain::common::skein::{skein_128_hash_bytes, skein_256_hash_data};
|
||||
use blockchain::encode;
|
||||
use blockchain::env;
|
||||
use blockchain::records::unpack_block::unpack_header::load_block_header;
|
||||
use blockchain::records::wallet_registry::resolve_pubkey_from_short_address;
|
||||
use blockchain::torrent::structs::Torrent;
|
||||
use blockchain::wallets::structures::Wallet;
|
||||
use blockchain::{AsyncReadExt, File};
|
||||
use colored::*;
|
||||
use std::process;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Validate that a local block file, block header, and torrent metadata agree.
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() != 2 {
|
||||
eprintln!("Usage: {} <block_number>", args[0]);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let block_number: u32 = match args[1].parse() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
eprintln!("Block number must be an integer.");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
block_ext,
|
||||
torrent_path,
|
||||
_wallet_path,
|
||||
block_path,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let block_filename = format!("{block_path}/{block_number}.{block_ext}");
|
||||
let torrent_filename = format!("{torrent_path}/{block_number}.torrent");
|
||||
|
||||
// Load and decode the torrent metadata first because later checks compare against it.
|
||||
let mut torrent_bytes = Vec::new();
|
||||
let mut torrent_file = File::open(&torrent_filename).await.unwrap_or_else(|_| {
|
||||
eprintln!("Error: cannot open torrent file '{torrent_filename}'");
|
||||
process::exit(1);
|
||||
});
|
||||
torrent_file.read_to_end(&mut torrent_bytes).await.unwrap();
|
||||
let torrent = Torrent::from_bytes(&torrent_bytes).await.unwrap();
|
||||
|
||||
// Load the local header and resolve the miner public key for signature/VRF checks.
|
||||
let header = load_block_header(block_number).await.unwrap();
|
||||
let block_hash = header.hash().await;
|
||||
let block_difficulty = hex_to_u64(&block_hash).await.unwrap();
|
||||
let miner_pubkey = resolve_pubkey_from_short_address(
|
||||
&blockchain::startup::initialize_startup::open_chain_state().await,
|
||||
&header.unmined_block.miner,
|
||||
)
|
||||
.unwrap_or(None)
|
||||
.unwrap_or_default();
|
||||
let miner_pubkey_hex = encode(&miner_pubkey);
|
||||
|
||||
// Load the raw block bytes for file-size, info-hash, and piece-hash checks.
|
||||
let mut block_data = Vec::new();
|
||||
let mut block_file = File::open(&block_filename).await.unwrap_or_else(|_| {
|
||||
eprintln!("Error: cannot open block file '{block_filename}'");
|
||||
process::exit(1);
|
||||
});
|
||||
block_file.read_to_end(&mut block_data).await.unwrap();
|
||||
|
||||
let info_hash_computed = skein_128_hash_bytes(&block_data);
|
||||
|
||||
// Rebuild the unmined-block hash used by the miner proof signature.
|
||||
let unmined_json = serde_json::to_string(&header.unmined_block).unwrap();
|
||||
let unmined_hash = skein_256_hash_data(&unmined_json);
|
||||
let signature_ok =
|
||||
Wallet::verify_transaction_with_public_key(&unmined_hash, &header.proof, &miner_pubkey_hex)
|
||||
.await;
|
||||
|
||||
let passed = "[PASSED]".green();
|
||||
let failed = "[FAILED]".red();
|
||||
|
||||
// Compare every header field that is duplicated inside the torrent metadata.
|
||||
if header.unmined_block.timestamp == torrent.info.timestamp {
|
||||
println!("timestamp match: {:>90}", format!("{passed}"));
|
||||
} else {
|
||||
println!("timestamp match: {:>90}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if header.unmined_block.nonce == torrent.info.nonce {
|
||||
println!("nonce match: {:>94}", format!("{passed}"));
|
||||
} else {
|
||||
println!("nonce match: {:>94}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if header.unmined_block.miner == torrent.mined_by {
|
||||
println!("wallet address match: {:>85}", format!("{passed}"));
|
||||
} else {
|
||||
println!("wallet address match: {:>85}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if block_difficulty < torrent.info.this_block_difficulty {
|
||||
println!("block difficulty check: {:>83}", format!("{passed}"));
|
||||
} else {
|
||||
println!("block difficulty check: {:>83}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if header.vrf == torrent.info.vrf {
|
||||
println!("VRF match: {:>96}", format!("{passed}"));
|
||||
} else {
|
||||
println!("VRF match: {:>96}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if block_data.len() == torrent.info.length as usize {
|
||||
println!("file size check: {:>90}", format!("{passed}"));
|
||||
} else {
|
||||
println!("file size check: {:>90}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if block_hash == torrent.info.block_hash {
|
||||
println!("block header hash check: {:>82}", format!("{passed}"));
|
||||
} else {
|
||||
println!("block header hash check: {:>82}", format!("{failed}"));
|
||||
}
|
||||
|
||||
let vrf_ok = Wallet::vrf_verify_with_public_key(
|
||||
header.vrf,
|
||||
&unmined_hash,
|
||||
&miner_pubkey_hex,
|
||||
&header.proof,
|
||||
)
|
||||
.await;
|
||||
if vrf_ok {
|
||||
println!("VRF validation check: {:>85}", format!("{passed}"));
|
||||
} else {
|
||||
println!("VRF validation check: {:>85}", format!("{failed}"));
|
||||
}
|
||||
|
||||
if signature_ok {
|
||||
println!("VRF Proof check: {:>90}", format!("{passed}"));
|
||||
} else {
|
||||
println!("VRF Proof check: {:>90}", format!("{failed}"));
|
||||
}
|
||||
|
||||
let mut all_pieces_passed = true;
|
||||
// Piece hashes prove the torrent metadata still matches every block-file chunk.
|
||||
for piece in &torrent.info.pieces {
|
||||
for (index, expected_hash) in piece.iter() {
|
||||
let idx = *index as usize;
|
||||
let start = (idx - 1) * torrent.info.piece_length as usize;
|
||||
let end = std::cmp::min(start + torrent.info.piece_length as usize, block_data.len());
|
||||
let slice = &block_data[start..end];
|
||||
let hash = skein_128_hash_bytes(slice);
|
||||
|
||||
if hash == *expected_hash {
|
||||
println!("piece {} hash check: {:>87}", idx, format!("{passed}"));
|
||||
} else {
|
||||
all_pieces_passed = false;
|
||||
println!("piece {} hash check: {:>87}", idx, format!("{failed}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if info_hash_computed == torrent.info.info_hash {
|
||||
println!("block hash check: {:>89}", format!("{passed}"));
|
||||
} else {
|
||||
println!("block hash check: {:>89}", format!("{failed}"));
|
||||
}
|
||||
|
||||
// The final pass/fail summary requires every individual validation to pass.
|
||||
if header.unmined_block.nonce == torrent.info.nonce
|
||||
&& header.unmined_block.miner == torrent.mined_by
|
||||
&& header.unmined_block.timestamp == torrent.info.timestamp
|
||||
&& block_difficulty < torrent.info.this_block_difficulty
|
||||
&& header.vrf == torrent.info.vrf
|
||||
&& block_data.len() == torrent.info.length as usize
|
||||
&& all_pieces_passed
|
||||
&& block_hash == torrent.info.block_hash
|
||||
&& signature_ok
|
||||
&& vrf_ok
|
||||
{
|
||||
println!("\nBlock {block_number} fully validated.");
|
||||
} else {
|
||||
println!("\nBlock {block_number} FAILED validation.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,9 +83,7 @@ async fn main() {
|
|||
let address_path = if args.len() == 2 {
|
||||
args[1].clone()
|
||||
} else {
|
||||
match prompt_for_path(
|
||||
"Please enter the path to the file containing the wallet address: ",
|
||||
) {
|
||||
match prompt_for_path("Please enter the path to the file containing the wallet address: ") {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
|
|
|
|||
|
|
@ -118,9 +118,8 @@ async fn main() {
|
|||
"Do you agree that payments should be made {}?",
|
||||
display_payment_period(&payment_period)
|
||||
);
|
||||
let question4 = format!(
|
||||
"Do you agree that this loan requires {payment_number} total payments?"
|
||||
);
|
||||
let question4 =
|
||||
format!("Do you agree that this loan requires {payment_number} total payments?");
|
||||
let question5 = format!(
|
||||
"Do you agree that each payment should be {} {}?",
|
||||
display_amount(payment_amount),
|
||||
|
|
|
|||
|
|
@ -1,249 +1,242 @@
|
|||
use blockchain::blocks::swap::UnsignedSwapTransaction;
|
||||
use blockchain::common::cli_prompts::{ask_yes_no_question, prompt_hidden_nonempty};
|
||||
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use blockchain::env;
|
||||
use blockchain::fs;
|
||||
use blockchain::json;
|
||||
use blockchain::read_to_string;
|
||||
use blockchain::records::wallet_registry::resolve_local_input_short_address;
|
||||
use blockchain::wallets::structures::Wallet;
|
||||
use blockchain::Value;
|
||||
|
||||
// padd the coin to ensure 15 characters
|
||||
fn pad_to_width(input: &str, width: usize) -> String {
|
||||
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
|
||||
let _ = std::fmt::write(
|
||||
&mut result,
|
||||
format_args!("{input:<width$}"),
|
||||
);
|
||||
result
|
||||
}
|
||||
|
||||
fn normalize_short_address_input(address: &str) -> Result<String, String> {
|
||||
resolve_local_input_short_address(address.trim())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Get the filename from the command line arguments
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() != 2 {
|
||||
println!("Usage:./sign_swap <path/to/file.json>");
|
||||
return;
|
||||
}
|
||||
let filename = &args[1];
|
||||
let decryption_key = prompt_hidden_nonempty(
|
||||
"What is your wallet decryption key? ",
|
||||
"Wallet key cannot be empty. Please try again.",
|
||||
)
|
||||
.await;
|
||||
|
||||
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
|
||||
Ok(wallet) => wallet,
|
||||
Err(err) => {
|
||||
eprintln!("Wallet decryption failed: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let private_key = &wallet.saved.private_key;
|
||||
let address = &wallet.saved.short_address;
|
||||
|
||||
// Read the contents of the file
|
||||
let contents = match read_to_string(filename).await {
|
||||
Ok(contents) => contents,
|
||||
Err(_) => {
|
||||
println!("Error reading file: {filename}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse the JSON
|
||||
let json: Result<Value, _> = serde_json::from_str(&contents);
|
||||
let json = match json {
|
||||
Ok(json) => json,
|
||||
Err(_) => {
|
||||
println!("Error parsing JSON in file: {filename}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let txtype = 6;
|
||||
|
||||
let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32;
|
||||
let offer_expiration = json["offer_expiration"].as_u64().unwrap_or_default() as u32;
|
||||
|
||||
// get values from transaction json
|
||||
let token_name1 = json["ticker1"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
let nft_series1: u32 = json["nft_series1"].as_u64().unwrap_or_default() as u32;
|
||||
let value1: u64 = json["value1"].as_u64().unwrap_or_default();
|
||||
let receive_amount = value1 as f64 / 100000000.0;
|
||||
|
||||
let token_name2 = json["ticker2"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
let nft_series2: u32 = json["nft_series2"].as_u64().unwrap_or_default() as u32;
|
||||
let value2: u64 = json["value2"].as_u64().unwrap_or_default();
|
||||
let send_amount = value2 as f64 / 100000000.0;
|
||||
|
||||
let txfee1_value: u64 = json["txfee1"].as_u64().unwrap_or_default();
|
||||
let tip1_value: u64 = json["tip1"].as_u64().unwrap_or_default();
|
||||
|
||||
let sender1 = match normalize_short_address_input(json["sender1"].as_str().unwrap_or_default())
|
||||
{
|
||||
Ok(address) => address,
|
||||
Err(_) => {
|
||||
println!("sender1 wallet invalid");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let txfee2_value: u64 = json["txfee2"].as_u64().unwrap_or_default();
|
||||
let txfee2 = txfee2_value as f64 / 100000000.0;
|
||||
let tip2_value: u64 = json["tip2"].as_u64().unwrap_or_default();
|
||||
let tip2 = tip2_value as f64 / 100000000.0;
|
||||
|
||||
let sender2 = match normalize_short_address_input(json["sender2"].as_str().unwrap_or_default())
|
||||
{
|
||||
Ok(address) => address,
|
||||
Err(_) => {
|
||||
println!("sender2 wallet invalid");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// ensure wallet and sender2 match
|
||||
if sender2 != address.trim() {
|
||||
println!(
|
||||
"Transaction is not valid for your wallet address. Expected {sender2} found {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let (
|
||||
_network_name,
|
||||
network_coin,
|
||||
_suffix,
|
||||
_torrent_path,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
// setup validation questions
|
||||
let question1 = format!(
|
||||
"Are you expecting to receive {receive_amount} {token_name1}?"
|
||||
);
|
||||
let question2 = format!("Are you expecting to send {send_amount} {token_name2}?");
|
||||
let question3 = format!(
|
||||
"Are you willing to spend {txfee2} {network_coin} in fees?"
|
||||
);
|
||||
let question4 = format!("Are you willing to tip {tip2} {token_name2}?");
|
||||
|
||||
// ask validation questions
|
||||
if !ask_yes_no_question(&question1).await {
|
||||
println!("Transaction is not valid");
|
||||
return;
|
||||
}
|
||||
if !ask_yes_no_question(&question2).await {
|
||||
println!("Transaction is not valid");
|
||||
return;
|
||||
}
|
||||
if !ask_yes_no_question(&question3).await {
|
||||
println!("Transaction is not valid");
|
||||
return;
|
||||
}
|
||||
if !ask_yes_no_question(&question4).await {
|
||||
println!("Transaction is not valid");
|
||||
return;
|
||||
}
|
||||
|
||||
let padded_token_name1 = pad_to_width(&token_name1, 15);
|
||||
let padded_token_name2 = pad_to_width(&token_name2, 15);
|
||||
|
||||
let unsigned_swap = UnsignedSwapTransaction::new(
|
||||
txtype,
|
||||
timestamp,
|
||||
offer_expiration,
|
||||
&padded_token_name1,
|
||||
nft_series1,
|
||||
value1,
|
||||
&padded_token_name2,
|
||||
nft_series2,
|
||||
value2,
|
||||
&sender1,
|
||||
address.trim(),
|
||||
tip1_value,
|
||||
tip2_value,
|
||||
txfee1_value,
|
||||
txfee2_value,
|
||||
)
|
||||
.await;
|
||||
|
||||
let hashed_data = unsigned_swap.hash().await;
|
||||
|
||||
let original_hash = &json["hash"].as_str().unwrap_or_default().trim();
|
||||
let signature1 = &json["signature1"].as_str().unwrap_or_default().trim();
|
||||
|
||||
let signature2 = if hashed_data == *original_hash.to_string() {
|
||||
match unsigned_swap.hash_and_sign(&private_key.to_string()).await {
|
||||
Ok(signature) => signature,
|
||||
Err(err) => {
|
||||
println!("Signing transaction failed: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Signing transaction failed. The included hash was incorrect.");
|
||||
return;
|
||||
};
|
||||
|
||||
let output = json!({
|
||||
"txtype": txtype,
|
||||
"timestamp": timestamp,
|
||||
"offer_expiration": offer_expiration,
|
||||
"ticker1": padded_token_name1,
|
||||
"nft_series1": nft_series1,
|
||||
"value1": value1,
|
||||
"ticker2": padded_token_name2,
|
||||
"nft_series2": nft_series2,
|
||||
"value2": value2,
|
||||
"sender1": sender1,
|
||||
"sender2": address.trim(),
|
||||
"tip1": tip1_value,
|
||||
"tip2": tip2_value,
|
||||
"txfee1": txfee1_value,
|
||||
"txfee2": txfee2_value,
|
||||
"hash": hashed_data,
|
||||
"signature1": signature1,
|
||||
"signature2": signature2
|
||||
});
|
||||
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
|
||||
|
||||
// Define the directory path
|
||||
let dir_path = "./transactions";
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
if let Err(e) = fs::create_dir_all(dir_path) {
|
||||
eprintln!("Failed to create directory: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Define the file path
|
||||
let file_path = format!("{dir_path}/{hashed_data}.json");
|
||||
|
||||
// Write the JSON string to the file
|
||||
if let Err(e) = fs::write(&file_path, &output_str) {
|
||||
eprintln!("Failed to write file: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Transaction: {output_str}");
|
||||
}
|
||||
use blockchain::blocks::swap::UnsignedSwapTransaction;
|
||||
use blockchain::common::cli_prompts::{ask_yes_no_question, prompt_hidden_nonempty};
|
||||
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use blockchain::env;
|
||||
use blockchain::fs;
|
||||
use blockchain::json;
|
||||
use blockchain::read_to_string;
|
||||
use blockchain::records::wallet_registry::resolve_local_input_short_address;
|
||||
use blockchain::wallets::structures::Wallet;
|
||||
use blockchain::Value;
|
||||
|
||||
// padd the coin to ensure 15 characters
|
||||
fn pad_to_width(input: &str, width: usize) -> String {
|
||||
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
|
||||
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
|
||||
result
|
||||
}
|
||||
|
||||
fn normalize_short_address_input(address: &str) -> Result<String, String> {
|
||||
resolve_local_input_short_address(address.trim())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Get the filename from the command line arguments
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() != 2 {
|
||||
println!("Usage:./sign_swap <path/to/file.json>");
|
||||
return;
|
||||
}
|
||||
let filename = &args[1];
|
||||
let decryption_key = prompt_hidden_nonempty(
|
||||
"What is your wallet decryption key? ",
|
||||
"Wallet key cannot be empty. Please try again.",
|
||||
)
|
||||
.await;
|
||||
|
||||
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
|
||||
Ok(wallet) => wallet,
|
||||
Err(err) => {
|
||||
eprintln!("Wallet decryption failed: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let private_key = &wallet.saved.private_key;
|
||||
let address = &wallet.saved.short_address;
|
||||
|
||||
// Read the contents of the file
|
||||
let contents = match read_to_string(filename).await {
|
||||
Ok(contents) => contents,
|
||||
Err(_) => {
|
||||
println!("Error reading file: {filename}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse the JSON
|
||||
let json: Result<Value, _> = serde_json::from_str(&contents);
|
||||
let json = match json {
|
||||
Ok(json) => json,
|
||||
Err(_) => {
|
||||
println!("Error parsing JSON in file: {filename}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let txtype = 6;
|
||||
|
||||
let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32;
|
||||
let offer_expiration = json["offer_expiration"].as_u64().unwrap_or_default() as u32;
|
||||
|
||||
// get values from transaction json
|
||||
let token_name1 = json["ticker1"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
let nft_series1: u32 = json["nft_series1"].as_u64().unwrap_or_default() as u32;
|
||||
let value1: u64 = json["value1"].as_u64().unwrap_or_default();
|
||||
let receive_amount = value1 as f64 / 100000000.0;
|
||||
|
||||
let token_name2 = json["ticker2"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
let nft_series2: u32 = json["nft_series2"].as_u64().unwrap_or_default() as u32;
|
||||
let value2: u64 = json["value2"].as_u64().unwrap_or_default();
|
||||
let send_amount = value2 as f64 / 100000000.0;
|
||||
|
||||
let txfee1_value: u64 = json["txfee1"].as_u64().unwrap_or_default();
|
||||
let tip1_value: u64 = json["tip1"].as_u64().unwrap_or_default();
|
||||
|
||||
let sender1 = match normalize_short_address_input(json["sender1"].as_str().unwrap_or_default())
|
||||
{
|
||||
Ok(address) => address,
|
||||
Err(_) => {
|
||||
println!("sender1 wallet invalid");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let txfee2_value: u64 = json["txfee2"].as_u64().unwrap_or_default();
|
||||
let txfee2 = txfee2_value as f64 / 100000000.0;
|
||||
let tip2_value: u64 = json["tip2"].as_u64().unwrap_or_default();
|
||||
let tip2 = tip2_value as f64 / 100000000.0;
|
||||
|
||||
let sender2 = match normalize_short_address_input(json["sender2"].as_str().unwrap_or_default())
|
||||
{
|
||||
Ok(address) => address,
|
||||
Err(_) => {
|
||||
println!("sender2 wallet invalid");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// ensure wallet and sender2 match
|
||||
if sender2 != address.trim() {
|
||||
println!(
|
||||
"Transaction is not valid for your wallet address. Expected {sender2} found {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let (
|
||||
_network_name,
|
||||
network_coin,
|
||||
_suffix,
|
||||
_torrent_path,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
// setup validation questions
|
||||
let question1 = format!("Are you expecting to receive {receive_amount} {token_name1}?");
|
||||
let question2 = format!("Are you expecting to send {send_amount} {token_name2}?");
|
||||
let question3 = format!("Are you willing to spend {txfee2} {network_coin} in fees?");
|
||||
let question4 = format!("Are you willing to tip {tip2} {token_name2}?");
|
||||
|
||||
// ask validation questions
|
||||
if !ask_yes_no_question(&question1).await {
|
||||
println!("Transaction is not valid");
|
||||
return;
|
||||
}
|
||||
if !ask_yes_no_question(&question2).await {
|
||||
println!("Transaction is not valid");
|
||||
return;
|
||||
}
|
||||
if !ask_yes_no_question(&question3).await {
|
||||
println!("Transaction is not valid");
|
||||
return;
|
||||
}
|
||||
if !ask_yes_no_question(&question4).await {
|
||||
println!("Transaction is not valid");
|
||||
return;
|
||||
}
|
||||
|
||||
let padded_token_name1 = pad_to_width(&token_name1, 15);
|
||||
let padded_token_name2 = pad_to_width(&token_name2, 15);
|
||||
|
||||
let unsigned_swap = UnsignedSwapTransaction::new(
|
||||
txtype,
|
||||
timestamp,
|
||||
offer_expiration,
|
||||
&padded_token_name1,
|
||||
nft_series1,
|
||||
value1,
|
||||
&padded_token_name2,
|
||||
nft_series2,
|
||||
value2,
|
||||
&sender1,
|
||||
address.trim(),
|
||||
tip1_value,
|
||||
tip2_value,
|
||||
txfee1_value,
|
||||
txfee2_value,
|
||||
)
|
||||
.await;
|
||||
|
||||
let hashed_data = unsigned_swap.hash().await;
|
||||
|
||||
let original_hash = &json["hash"].as_str().unwrap_or_default().trim();
|
||||
let signature1 = &json["signature1"].as_str().unwrap_or_default().trim();
|
||||
|
||||
let signature2 = if hashed_data == *original_hash.to_string() {
|
||||
match unsigned_swap.hash_and_sign(&private_key.to_string()).await {
|
||||
Ok(signature) => signature,
|
||||
Err(err) => {
|
||||
println!("Signing transaction failed: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Signing transaction failed. The included hash was incorrect.");
|
||||
return;
|
||||
};
|
||||
|
||||
let output = json!({
|
||||
"txtype": txtype,
|
||||
"timestamp": timestamp,
|
||||
"offer_expiration": offer_expiration,
|
||||
"ticker1": padded_token_name1,
|
||||
"nft_series1": nft_series1,
|
||||
"value1": value1,
|
||||
"ticker2": padded_token_name2,
|
||||
"nft_series2": nft_series2,
|
||||
"value2": value2,
|
||||
"sender1": sender1,
|
||||
"sender2": address.trim(),
|
||||
"tip1": tip1_value,
|
||||
"tip2": tip2_value,
|
||||
"txfee1": txfee1_value,
|
||||
"txfee2": txfee2_value,
|
||||
"hash": hashed_data,
|
||||
"signature1": signature1,
|
||||
"signature2": signature2
|
||||
});
|
||||
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
|
||||
|
||||
// Define the directory path
|
||||
let dir_path = "./transactions";
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
if let Err(e) = fs::create_dir_all(dir_path) {
|
||||
eprintln!("Failed to create directory: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Define the file path
|
||||
let file_path = format!("{dir_path}/{hashed_data}.json");
|
||||
|
||||
// Write the JSON string to the file
|
||||
if let Err(e) = fs::write(&file_path, &output_str) {
|
||||
eprintln!("Failed to write file: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Transaction: {output_str}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,10 +185,8 @@ impl VrfBlock {
|
|||
cursor
|
||||
.write_all(&self.unmined_block.timestamp.to_le_bytes())
|
||||
.await?;
|
||||
let miner_bytes =
|
||||
Wallet::short_address_to_bytes(&self.unmined_block.miner).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid short miner address")
|
||||
})?;
|
||||
let miner_bytes = Wallet::short_address_to_bytes(&self.unmined_block.miner)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid short miner address"))?;
|
||||
cursor.write_all(&miner_bytes).await?;
|
||||
cursor
|
||||
.write_all(&decode(&self.unmined_block.previous_hash).unwrap())
|
||||
|
|
@ -207,8 +205,7 @@ impl VrfBlock {
|
|||
pub async fn from_bytes(bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
// A VRF header must be exactly the fixed header byte length.
|
||||
if bytes.len() != VRF_BLOCK_BYTES {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count for Block",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count for Block"));
|
||||
}
|
||||
|
||||
// Read from the fixed-width VRF header bytes.
|
||||
|
|
@ -219,9 +216,8 @@ impl VrfBlock {
|
|||
|
||||
let mut miner_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut miner_bytes).await?;
|
||||
let miner = Wallet::bytes_to_short_address(&miner_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid short miner address")
|
||||
})?;
|
||||
let miner = Wallet::bytes_to_short_address(&miner_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid short miner address"))?;
|
||||
|
||||
// Decode parent hash, difficulty, nonce, VRF number, and proof.
|
||||
let mut prev_hash_bytes = vec![0; 32];
|
||||
|
|
|
|||
|
|
@ -105,9 +105,7 @@ impl BurnTransaction {
|
|||
.write_all(&self.unsigned_burn.time.to_le_bytes())
|
||||
.await?;
|
||||
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_burn.address)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid burn short address")
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid burn short address"))?;
|
||||
cursor.write_all(&address_bytes).await?;
|
||||
cursor.write_all(self.unsigned_burn.coin.as_bytes()).await?;
|
||||
cursor
|
||||
|
|
@ -126,8 +124,7 @@ impl BurnTransaction {
|
|||
|
||||
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
let mut cursor = Cursor::new(bytes);
|
||||
|
|
@ -136,10 +133,8 @@ impl BurnTransaction {
|
|||
|
||||
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut address_bytes).await?;
|
||||
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid burn short address bytes",
|
||||
)
|
||||
})?;
|
||||
let address = Wallet::bytes_to_short_address(&address_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid burn short address bytes"))?;
|
||||
|
||||
let mut coin_bytes = vec![0; 15];
|
||||
cursor.read_exact(&mut coin_bytes).await?;
|
||||
|
|
|
|||
|
|
@ -110,10 +110,7 @@ impl CollateralClaimTransaction {
|
|||
.write_all(&decode(&self.unsigned_collateral_claim.contract_hash).unwrap())
|
||||
.await?;
|
||||
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_collateral_claim.address)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid collateral claimant short address",
|
||||
)
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid collateral claimant short address"))?;
|
||||
cursor.write_all(&address_bytes).await?;
|
||||
cursor
|
||||
.write_all(&self.unsigned_collateral_claim.txfee.to_le_bytes())
|
||||
|
|
@ -126,8 +123,7 @@ impl CollateralClaimTransaction {
|
|||
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
// The block parser already consumed the transaction type byte.
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
// Read the remaining fixed-width collateral-claim bytes.
|
||||
|
|
@ -144,8 +140,7 @@ impl CollateralClaimTransaction {
|
|||
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut address_bytes).await?;
|
||||
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid collateral claimant short address bytes",
|
||||
)
|
||||
tokio::io::Error::other("Invalid collateral claimant short address bytes")
|
||||
})?;
|
||||
|
||||
let txfee = cursor.read_u64_le().await?;
|
||||
|
|
|
|||
|
|
@ -65,8 +65,7 @@ impl GenesisTransaction {
|
|||
// The transaction type is read by the block parser, so this
|
||||
// function receives only the remaining genesis bytes.
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
// Read from the remaining fixed-width transaction bytes.
|
||||
|
|
|
|||
|
|
@ -104,10 +104,7 @@ impl IssueTokenTransaction {
|
|||
.write_all(&self.unsigned_issue_token.time.to_le_bytes())
|
||||
.await?;
|
||||
let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_issue_token.creator)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid issue-token creator short address",
|
||||
)
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid issue-token creator short address"))?;
|
||||
cursor.write_all(&creator_bytes).await?;
|
||||
cursor
|
||||
.write_all(self.unsigned_issue_token.ticker.as_bytes())
|
||||
|
|
@ -126,8 +123,7 @@ impl IssueTokenTransaction {
|
|||
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
// The block parser already consumed the transaction type byte.
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
let mut cursor = Cursor::new(bytes);
|
||||
|
|
@ -138,8 +134,7 @@ impl IssueTokenTransaction {
|
|||
let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut creator_bytes).await?;
|
||||
let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid issue-token creator short address bytes",
|
||||
)
|
||||
tokio::io::Error::other("Invalid issue-token creator short address bytes")
|
||||
})?;
|
||||
|
||||
// Decode token ticker, issued amount, fee, and signature.
|
||||
|
|
|
|||
|
|
@ -123,9 +123,7 @@ impl ContractPaymentTransaction {
|
|||
.write_all(&decode(&self.unsigned_contract_payment.contract_hash).unwrap())
|
||||
.await?;
|
||||
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_contract_payment.address)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid payer short address")
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid payer short address"))?;
|
||||
cursor.write_all(&address_bytes).await?;
|
||||
cursor
|
||||
.write_all(&self.unsigned_contract_payment.tip.to_le_bytes())
|
||||
|
|
@ -142,8 +140,7 @@ impl ContractPaymentTransaction {
|
|||
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
// The block parser already consumed the transaction type byte.
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
// Read the remaining fixed-width loan-payment bytes.
|
||||
|
|
@ -160,10 +157,8 @@ impl ContractPaymentTransaction {
|
|||
// Decode payer short address, miner tip, fee, txid hash, and signature.
|
||||
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut address_bytes).await?;
|
||||
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid payer short address bytes",
|
||||
)
|
||||
})?;
|
||||
let address = Wallet::bytes_to_short_address(&address_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid payer short address bytes"))?;
|
||||
|
||||
let tip = cursor.read_u64_le().await?;
|
||||
let txfee = cursor.read_u64_le().await?;
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ pub struct UnsignedLoanContractTransaction {
|
|||
pub payment_period: String, // 1 byte d, w, or m for days, weeks, or months
|
||||
pub payment_number: u8, // 1 byte total number of payments
|
||||
pub payment_amount: u64, // 8 bytes amount due for each payment
|
||||
pub grace_period: u8, // 1 byte missed payments before collateral claim is allowed
|
||||
pub max_late_value: u64, // 8 bytes max overdue value before collateral claim is allowed
|
||||
pub txfee: u64, // 8 bytes transaction fee paid by the lender
|
||||
pub grace_period: u8, // 1 byte missed payments before collateral claim is allowed
|
||||
pub max_late_value: u64, // 8 bytes max overdue value before collateral claim is allowed
|
||||
pub txfee: u64, // 8 bytes transaction fee paid by the lender
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)] // 1486 bytes
|
||||
|
|
@ -35,9 +35,23 @@ pub struct LoanContractTransaction {
|
|||
}
|
||||
|
||||
impl LoanContractTransaction {
|
||||
pub const BYTE_LENGTH: usize =
|
||||
1 + 4 + 15 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 15 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ 1 + 1 + 8 + 1 + 8 + 8 + 32 + Wallet::SIGNATURE_LENGTH + Wallet::SIGNATURE_LENGTH;
|
||||
pub const BYTE_LENGTH: usize = 1
|
||||
+ 4
|
||||
+ 15
|
||||
+ 8
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ 15
|
||||
+ 8
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ 1
|
||||
+ 1
|
||||
+ 8
|
||||
+ 1
|
||||
+ 8
|
||||
+ 8
|
||||
+ 32
|
||||
+ Wallet::SIGNATURE_LENGTH
|
||||
+ Wallet::SIGNATURE_LENGTH;
|
||||
}
|
||||
|
||||
impl UnsignedLoanContractTransaction {
|
||||
|
|
@ -163,9 +177,7 @@ impl LoanContractTransaction {
|
|||
.write_all(&self.unsigned_loan_contract.loan_amount.to_le_bytes())
|
||||
.await?;
|
||||
let lender_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.lender)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid lender short address")
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid lender short address"))?;
|
||||
cursor.write_all(&lender_bytes).await?;
|
||||
cursor
|
||||
.write_all(self.unsigned_loan_contract.collateral.as_bytes())
|
||||
|
|
@ -174,10 +186,7 @@ impl LoanContractTransaction {
|
|||
.write_all(&self.unsigned_loan_contract.collateral_amount.to_le_bytes())
|
||||
.await?;
|
||||
let borrower_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.borrower)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid borrower short address",
|
||||
)
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid borrower short address"))?;
|
||||
cursor.write_all(&borrower_bytes).await?;
|
||||
cursor
|
||||
.write_all(self.unsigned_loan_contract.payment_period.as_bytes())
|
||||
|
|
@ -207,8 +216,7 @@ impl LoanContractTransaction {
|
|||
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
// The block parser already consumed the transaction type byte.
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
// Read the remaining fixed-width loan-contract bytes.
|
||||
|
|
@ -226,10 +234,8 @@ impl LoanContractTransaction {
|
|||
// Decode lender short address.
|
||||
let mut lender_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut lender_bytes).await?;
|
||||
let lender = Wallet::bytes_to_short_address(&lender_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid lender short address bytes",
|
||||
)
|
||||
})?;
|
||||
let lender = Wallet::bytes_to_short_address(&lender_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid lender short address bytes"))?;
|
||||
|
||||
// Decode collateral asset and amount.
|
||||
let mut collateral_bytes = vec![0; 15];
|
||||
|
|
@ -241,10 +247,8 @@ impl LoanContractTransaction {
|
|||
// Decode borrower short address.
|
||||
let mut borrower_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut borrower_bytes).await?;
|
||||
let borrower = Wallet::bytes_to_short_address(&borrower_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid borrower short address bytes",
|
||||
)
|
||||
})?;
|
||||
let borrower = Wallet::bytes_to_short_address(&borrower_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid borrower short address bytes"))?;
|
||||
|
||||
// Decode payment schedule and late-payment rules.
|
||||
let mut payment_period_bytes = vec![0; 1];
|
||||
|
|
|
|||
|
|
@ -31,8 +31,19 @@ pub struct MarketingTransaction {
|
|||
}
|
||||
|
||||
impl MarketingTransaction {
|
||||
pub const BYTE_LENGTH: usize =
|
||||
1 + 4 + 8 + 6 + 40 + 100 + 1 + 1 + 2 + 2 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH;
|
||||
pub const BYTE_LENGTH: usize = 1
|
||||
+ 4
|
||||
+ 8
|
||||
+ 6
|
||||
+ 40
|
||||
+ 100
|
||||
+ 1
|
||||
+ 1
|
||||
+ 2
|
||||
+ 2
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ 8
|
||||
+ Wallet::SIGNATURE_LENGTH;
|
||||
}
|
||||
|
||||
impl UnsignedMarketingTransaction {
|
||||
|
|
@ -152,10 +163,7 @@ impl MarketingTransaction {
|
|||
.write_all(&self.unsigned_marketing.click_value.to_le_bytes())
|
||||
.await?;
|
||||
let advertiser_bytes = Wallet::short_address_to_bytes(&self.unsigned_marketing.advertiser)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid advertiser short address",
|
||||
)
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid advertiser short address"))?;
|
||||
cursor.write_all(&advertiser_bytes).await?;
|
||||
cursor
|
||||
.write_all(&self.unsigned_marketing.txfee.to_le_bytes())
|
||||
|
|
@ -168,8 +176,7 @@ impl MarketingTransaction {
|
|||
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
// The block parser already consumed the transaction type byte.
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
// Read the remaining fixed-width marketing bytes.
|
||||
|
|
@ -201,10 +208,8 @@ impl MarketingTransaction {
|
|||
// Decode advertiser short address, fee, and signature.
|
||||
let mut advertiser_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut advertiser_bytes).await?;
|
||||
let advertiser = Wallet::bytes_to_short_address(&advertiser_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid advertiser short address bytes",
|
||||
)
|
||||
})?;
|
||||
let advertiser = Wallet::bytes_to_short_address(&advertiser_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid advertiser short address bytes"))?;
|
||||
|
||||
let txfee = cursor.read_u64_le().await?;
|
||||
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
|
||||
|
|
|
|||
|
|
@ -10,15 +10,15 @@ use crate::{AsyncReadExt, AsyncWriteExt};
|
|||
|
||||
#[derive(Debug, Serialize, Clone)] // 255 bytes
|
||||
pub struct UnsignedCreateNftTransaction {
|
||||
pub txtype: u8, // 1 byte transaction type, should be 4
|
||||
pub time: u32, // 4 bytes transaction timestamp
|
||||
pub creator: String, // 22 bytes creator short address
|
||||
pub series: u8, // 1 byte 0 for single NFT, 1 for series
|
||||
pub nft_name: String, // 15 bytes NFT or collection name padded with spaces
|
||||
pub txtype: u8, // 1 byte transaction type, should be 4
|
||||
pub time: u32, // 4 bytes transaction timestamp
|
||||
pub creator: String, // 22 bytes creator short address
|
||||
pub series: u8, // 1 byte 0 for single NFT, 1 for series
|
||||
pub nft_name: String, // 15 bytes NFT or collection name padded with spaces
|
||||
pub item_ipfs: String, // 100 bytes padded CID string
|
||||
pub count: u32, // 4 bytes 1 for single NFT, otherwise series item count
|
||||
pub desc: String, // 100 bytes description padded with spaces
|
||||
pub txfee: u64, // 8 bytes transaction fee
|
||||
pub count: u32, // 4 bytes 1 for single NFT, otherwise series item count
|
||||
pub desc: String, // 100 bytes description padded with spaces
|
||||
pub txfee: u64, // 8 bytes transaction fee
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)] // 921 bytes
|
||||
|
|
@ -28,8 +28,16 @@ pub struct CreateNftTransaction {
|
|||
}
|
||||
|
||||
impl CreateNftTransaction {
|
||||
pub const BYTE_LENGTH: usize =
|
||||
1 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 1 + 15 + 100 + 4 + 100 + 8 + Wallet::SIGNATURE_LENGTH;
|
||||
pub const BYTE_LENGTH: usize = 1
|
||||
+ 4
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ 1
|
||||
+ 15
|
||||
+ 100
|
||||
+ 4
|
||||
+ 100
|
||||
+ 8
|
||||
+ Wallet::SIGNATURE_LENGTH;
|
||||
}
|
||||
|
||||
impl UnsignedCreateNftTransaction {
|
||||
|
|
@ -119,9 +127,7 @@ impl CreateNftTransaction {
|
|||
.write_all(&self.unsigned_create_nft.time.to_le_bytes())
|
||||
.await?;
|
||||
let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_create_nft.creator)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid creator short address")
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid creator short address"))?;
|
||||
cursor.write_all(&creator_bytes).await?;
|
||||
cursor
|
||||
.write_all(&self.unsigned_create_nft.series.to_le_bytes())
|
||||
|
|
@ -149,8 +155,7 @@ impl CreateNftTransaction {
|
|||
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
// The block parser already consumed the transaction type byte.
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
// Read the remaining fixed-width NFT-creation bytes.
|
||||
|
|
@ -161,10 +166,8 @@ impl CreateNftTransaction {
|
|||
|
||||
let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut creator_bytes).await?;
|
||||
let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid creator short address bytes",
|
||||
)
|
||||
})?;
|
||||
let creator = Wallet::bytes_to_short_address(&creator_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid creator short address bytes"))?;
|
||||
|
||||
// Decode series flag, name, CID, count, description, fee, and signature.
|
||||
let series = cursor.read_u8().await?;
|
||||
|
|
|
|||
|
|
@ -65,8 +65,7 @@ impl RewardsTransaction {
|
|||
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
// The block parser already consumed the transaction type byte.
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
// Read the remaining fixed reward bytes.
|
||||
|
|
|
|||
|
|
@ -14,17 +14,17 @@ pub struct UnsignedSwapTransaction {
|
|||
pub timestamp: u32, // 4 bytes offer creation timestamp
|
||||
pub offer_expiration: u32, // 4 bytes offer expiration timestamp
|
||||
pub ticker1: String, // 15 bytes asset offered by sender1
|
||||
pub nft_series1: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item
|
||||
pub value1: u64, // 8 bytes amount offered by sender1
|
||||
pub ticker2: String, // 15 bytes asset offered by sender2
|
||||
pub nft_series2: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item
|
||||
pub value2: u64, // 8 bytes amount offered by sender2
|
||||
pub sender1: String, // 22 bytes sender1 short address
|
||||
pub sender2: String, // 22 bytes sender2 short address
|
||||
pub tip1: u64, // 8 bytes miner tip paid in ticker1
|
||||
pub tip2: u64, // 8 bytes miner tip paid in ticker2
|
||||
pub txfee1: u64, // 8 bytes sender1 fee
|
||||
pub txfee2: u64, // 8 bytes sender2 fee
|
||||
pub nft_series1: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item
|
||||
pub value1: u64, // 8 bytes amount offered by sender1
|
||||
pub ticker2: String, // 15 bytes asset offered by sender2
|
||||
pub nft_series2: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item
|
||||
pub value2: u64, // 8 bytes amount offered by sender2
|
||||
pub sender1: String, // 22 bytes sender1 short address
|
||||
pub sender2: String, // 22 bytes sender2 short address
|
||||
pub tip1: u64, // 8 bytes miner tip paid in ticker1
|
||||
pub tip2: u64, // 8 bytes miner tip paid in ticker2
|
||||
pub txfee1: u64, // 8 bytes sender1 fee
|
||||
pub txfee2: u64, // 8 bytes sender2 fee
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)] // 1471 bytes
|
||||
|
|
@ -35,9 +35,23 @@ pub struct SwapTransaction {
|
|||
}
|
||||
|
||||
impl SwapTransaction {
|
||||
pub const BYTE_LENGTH: usize =
|
||||
1 + 4 + 4 + 15 + 4 + 8 + 15 + 4 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ 8 + 8 + 8 + 8 + Wallet::SIGNATURE_LENGTH + Wallet::SIGNATURE_LENGTH;
|
||||
pub const BYTE_LENGTH: usize = 1
|
||||
+ 4
|
||||
+ 4
|
||||
+ 15
|
||||
+ 4
|
||||
+ 8
|
||||
+ 15
|
||||
+ 4
|
||||
+ 8
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ 8
|
||||
+ 8
|
||||
+ 8
|
||||
+ 8
|
||||
+ Wallet::SIGNATURE_LENGTH
|
||||
+ Wallet::SIGNATURE_LENGTH;
|
||||
}
|
||||
|
||||
impl UnsignedSwapTransaction {
|
||||
|
|
@ -178,13 +192,9 @@ impl SwapTransaction {
|
|||
.write_all(&self.unsigned_swap.value2.to_le_bytes())
|
||||
.await?;
|
||||
let sender1_bytes = Wallet::short_address_to_bytes(&self.unsigned_swap.sender1)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid sender1 short address")
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid sender1 short address"))?;
|
||||
let sender2_bytes = Wallet::short_address_to_bytes(&self.unsigned_swap.sender2)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid sender2 short address")
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid sender2 short address"))?;
|
||||
cursor.write_all(&sender1_bytes).await?;
|
||||
cursor.write_all(&sender2_bytes).await?;
|
||||
cursor
|
||||
|
|
@ -208,8 +218,7 @@ impl SwapTransaction {
|
|||
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
// The block parser already consumed the transaction type byte.
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
// Read the remaining fixed-width swap bytes.
|
||||
|
|
@ -238,17 +247,13 @@ impl SwapTransaction {
|
|||
// Decode both sender short addresses.
|
||||
let mut sender1_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut sender1_bytes).await?;
|
||||
let sender1 = Wallet::bytes_to_short_address(&sender1_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid sender1 short address bytes",
|
||||
)
|
||||
})?;
|
||||
let sender1 = Wallet::bytes_to_short_address(&sender1_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid sender1 short address bytes"))?;
|
||||
|
||||
let mut sender2_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut sender2_bytes).await?;
|
||||
let sender2 = Wallet::bytes_to_short_address(&sender2_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid sender2 short address bytes",
|
||||
)
|
||||
})?;
|
||||
let sender2 = Wallet::bytes_to_short_address(&sender2_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid sender2 short address bytes"))?;
|
||||
|
||||
// Decode miner tips, fees, and both signatures.
|
||||
let tip1 = cursor.read_u64_le().await?;
|
||||
|
|
|
|||
|
|
@ -115,9 +115,7 @@ impl CreateTokenTransaction {
|
|||
.write_all(&self.unsigned_create_token.time.to_le_bytes())
|
||||
.await?;
|
||||
let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_create_token.creator)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid creator short address")
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid creator short address"))?;
|
||||
cursor.write_all(&creator_bytes).await?;
|
||||
cursor
|
||||
.write_all(self.unsigned_create_token.ticker.as_bytes())
|
||||
|
|
@ -139,8 +137,7 @@ impl CreateTokenTransaction {
|
|||
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
// The block parser already consumed the transaction type byte.
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
// Read the remaining fixed-width token-creation bytes.
|
||||
|
|
@ -152,10 +149,8 @@ impl CreateTokenTransaction {
|
|||
// Decode the creator short address.
|
||||
let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut creator_bytes).await?;
|
||||
let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid creator short address bytes",
|
||||
)
|
||||
})?;
|
||||
let creator = Wallet::bytes_to_short_address(&creator_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid creator short address bytes"))?;
|
||||
|
||||
// Decode the fixed 15-byte token ticker.
|
||||
let mut ticker_bytes = vec![0; 15];
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ pub struct UnsignedTransferTransaction {
|
|||
pub txtype: u8, // 1 byte transaction type, should be 2
|
||||
pub time: u32, // 4 bytes transaction timestamp
|
||||
pub value: u64, // 8 bytes number of coins or tokens to send
|
||||
pub coin: String, // 15 bytes base coin, token ticker, or NFT name, padded with spaces
|
||||
pub nft_series: u32, // 4 bytes 0 for coins/tokens/1-of-1 NFTs, otherwise NFT series item
|
||||
pub sender: String, // 22 bytes sender short address
|
||||
pub coin: String, // 15 bytes base coin, token ticker, or NFT name, padded with spaces
|
||||
pub nft_series: u32, // 4 bytes 0 for coins/tokens/1-of-1 NFTs, otherwise NFT series item
|
||||
pub sender: String, // 22 bytes sender short address
|
||||
pub receiver: String, // 22 bytes receiver short address
|
||||
pub txfee: u64, // 8 bytes transaction fee
|
||||
pub txfee: u64, // 8 bytes transaction fee
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)] // 750 bytes
|
||||
|
|
@ -28,8 +28,15 @@ pub struct TransferTransaction {
|
|||
}
|
||||
|
||||
impl TransferTransaction {
|
||||
pub const BYTE_LENGTH: usize =
|
||||
1 + 4 + 8 + 15 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH;
|
||||
pub const BYTE_LENGTH: usize = 1
|
||||
+ 4
|
||||
+ 8
|
||||
+ 15
|
||||
+ 4
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ 8
|
||||
+ Wallet::SIGNATURE_LENGTH;
|
||||
}
|
||||
|
||||
impl UnsignedTransferTransaction {
|
||||
|
|
@ -126,14 +133,9 @@ impl TransferTransaction {
|
|||
.write_all(&self.unsigned_transfer.nft_series.to_le_bytes())
|
||||
.await?;
|
||||
let sender_bytes = Wallet::short_address_to_bytes(&self.unsigned_transfer.sender)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid sender short address")
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid sender short address"))?;
|
||||
let receiver_bytes = Wallet::short_address_to_bytes(&self.unsigned_transfer.receiver)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid receiver short address",
|
||||
)
|
||||
})?;
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid receiver short address"))?;
|
||||
cursor.write_all(&sender_bytes).await?;
|
||||
cursor.write_all(&receiver_bytes).await?;
|
||||
cursor
|
||||
|
|
@ -147,8 +149,7 @@ impl TransferTransaction {
|
|||
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
|
||||
// The block parser already consumed the transaction type byte.
|
||||
if bytes.len() != Self::BYTE_LENGTH - 1 {
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count",
|
||||
));
|
||||
return Err(tokio::io::Error::other("Invalid Byte Count"));
|
||||
}
|
||||
|
||||
// Read the remaining fixed-width transfer bytes.
|
||||
|
|
@ -168,18 +169,14 @@ impl TransferTransaction {
|
|||
// Decode the sender short address.
|
||||
let mut sender_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut sender_bytes).await?;
|
||||
let sender = Wallet::bytes_to_short_address(&sender_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid sender short address bytes",
|
||||
)
|
||||
})?;
|
||||
let sender = Wallet::bytes_to_short_address(&sender_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid sender short address bytes"))?;
|
||||
|
||||
// Decode the receiver short address.
|
||||
let mut receiver_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut receiver_bytes).await?;
|
||||
let receiver = Wallet::bytes_to_short_address(&receiver_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid receiver short address bytes",
|
||||
)
|
||||
})?;
|
||||
let receiver = Wallet::bytes_to_short_address(&receiver_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid receiver short address bytes"))?;
|
||||
|
||||
// Decode fee and signature.
|
||||
let txfee = cursor.read_u64_le().await?;
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ use crate::{AsyncReadExt, AsyncWriteExt};
|
|||
|
||||
#[derive(Debug, Serialize, Clone)] // 57 bytes
|
||||
pub struct UnsignedVanityAddressTransaction {
|
||||
pub txtype: u8, // 1 byte transaction type, should be 12
|
||||
pub timestamp: u32, // 4 bytes transaction timestamp
|
||||
pub address: String, // 22 bytes real short address receiving the vanity mapping
|
||||
pub vanity_address: String, // 22 bytes vanity short address being registered
|
||||
pub txfee: u64, // 8 bytes fee paid for vanity registration
|
||||
pub txtype: u8, // 1 byte transaction type, should be 12
|
||||
pub timestamp: u32, // 4 bytes transaction timestamp
|
||||
pub address: String, // 22 bytes real short address receiving the vanity mapping
|
||||
pub vanity_address: String, // 22 bytes vanity short address being registered
|
||||
pub txfee: u64, // 8 bytes fee paid for vanity registration
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)] // 723 bytes
|
||||
|
|
@ -23,8 +23,12 @@ pub struct VanityAddressTransaction {
|
|||
}
|
||||
|
||||
impl VanityAddressTransaction {
|
||||
pub const BYTE_LENGTH: usize =
|
||||
1 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH;
|
||||
pub const BYTE_LENGTH: usize = 1
|
||||
+ 4
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||
+ 8
|
||||
+ Wallet::SIGNATURE_LENGTH;
|
||||
}
|
||||
|
||||
impl UnsignedVanityAddressTransaction {
|
||||
|
|
@ -99,10 +103,9 @@ impl VanityAddressTransaction {
|
|||
.write_all(&self.unsigned_vanity_address.timestamp.to_le_bytes())
|
||||
.await?;
|
||||
|
||||
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_vanity_address.address)
|
||||
.ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid sender short address")
|
||||
})?;
|
||||
let address_bytes =
|
||||
Wallet::short_address_to_bytes(&self.unsigned_vanity_address.address)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid sender short address"))?;
|
||||
// Vanity addresses use the same 22-byte width as short addresses
|
||||
// but are encoded through the vanity-specific byte conversion.
|
||||
let vanity_bytes =
|
||||
|
|
@ -135,15 +138,13 @@ impl VanityAddressTransaction {
|
|||
|
||||
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut address_bytes).await?;
|
||||
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid sender short address bytes")
|
||||
})?;
|
||||
let address = Wallet::bytes_to_short_address(&address_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid sender short address bytes"))?;
|
||||
|
||||
let mut vanity_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
|
||||
cursor.read_exact(&mut vanity_bytes).await?;
|
||||
let vanity_address = Wallet::bytes_to_vanity_address(&vanity_bytes).ok_or_else(|| {
|
||||
tokio::io::Error::other("Invalid vanity short address bytes")
|
||||
})?;
|
||||
let vanity_address = Wallet::bytes_to_vanity_address(&vanity_bytes)
|
||||
.ok_or_else(|| tokio::io::Error::other("Invalid vanity short address bytes"))?;
|
||||
|
||||
let txfee = cursor.read_u64_le().await?;
|
||||
|
||||
|
|
|
|||
|
|
@ -28,9 +28,7 @@ pub async fn hex_to_u64(hex_string: &str) -> Result<u64, String> {
|
|||
pub fn binary_to_string(binary_data: Vec<u8>) -> String {
|
||||
match String::from_utf8(binary_data.clone()) {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
"Invalid UTF-8 Data".to_string()
|
||||
}
|
||||
Err(_) => "Invalid UTF-8 Data".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use crate::encode;
|
||||
use crate::ripemd::Digest as RipemdDigest;
|
||||
use crate::Digest;
|
||||
use crate::Output;
|
||||
use crate::Ripemd160;
|
||||
use crate::Skein256;
|
||||
use crate::Skein512;
|
||||
use crate::ripemd::Digest as RipemdDigest;
|
||||
|
||||
pub fn skein_128_hash_bytes(data: &[u8]) -> String {
|
||||
// Contractless 128-bit hashes are Skein256 hashes reduced to 16 bytes.
|
||||
|
|
|
|||
|
|
@ -159,8 +159,7 @@ impl Settings {
|
|||
config_dir,
|
||||
),
|
||||
log_path: Self::expand(
|
||||
conf.get_from(Some("Paths"), "LOG_PATH")
|
||||
.unwrap_or("./logs"),
|
||||
conf.get_from(Some("Paths"), "LOG_PATH").unwrap_or("./logs"),
|
||||
config_dir,
|
||||
),
|
||||
log_level: conf
|
||||
|
|
|
|||
|
|
@ -2,20 +2,22 @@ use crate::blocks::block::{Block, UnminedBlock};
|
|||
use crate::blocks::genesis::{GenesisTransaction, UnsignedGenesisTransaction};
|
||||
use crate::common::check_genesis::genesis_checkup;
|
||||
use crate::common::types::{Transaction, GENESIS_BLOCK_HASH};
|
||||
use crate::miner::flag::{is_mining_running, is_mining_stop_requested, is_normal_mode, set_mining_state, MiningState};
|
||||
use crate::log::{error, info};
|
||||
use crate::miner::flag::{
|
||||
is_mining_running, is_mining_stop_requested, is_normal_mode, set_mining_state, MiningState,
|
||||
};
|
||||
use crate::records::memory::connections::outgoing_connection_count;
|
||||
use crate::records::memory::response_channels::Command;
|
||||
use crate::records::record_chain::save::save_block;
|
||||
use crate::records::record_chain::structs::{SaveBlockParams, SaveType};
|
||||
use crate::sled::Db;
|
||||
use crate::sleep;
|
||||
use crate::verifications::verification_service::VerificationService;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::log::{error, info};
|
||||
use crate::sled::Db;
|
||||
use crate::Arc;
|
||||
use crate::Duration;
|
||||
use crate::Error;
|
||||
use crate::Mutex;
|
||||
use crate::sleep;
|
||||
use crate::Utc;
|
||||
|
||||
pub async fn create_genesis_transaction(
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
use crate::common::check_genesis::genesis_checkup;
|
||||
use crate::records::memory::response_channels::{reserve_entry, Command};
|
||||
use crate::sled::Db;
|
||||
use crate::timeout;
|
||||
use crate::torrent::structs::Torrent;
|
||||
use crate::torrent::torrenting_system::torrent_requests::{
|
||||
handle_response_and_save_torrent, send_request_torrent_message,
|
||||
};
|
||||
use crate::sled::Db;
|
||||
use crate::torrent::structs::Torrent;
|
||||
use crate::Arc;
|
||||
use crate::Duration;
|
||||
use crate::Mutex;
|
||||
use crate::TcpStream;
|
||||
use crate::timeout;
|
||||
|
||||
pub async fn create_genesis_block(
|
||||
local_height: u32,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
use crate::log::info;
|
||||
use crate::common::skein::skein_128_hash_bytes;
|
||||
use crate::log::{info, warn};
|
||||
use crate::miner::flag::begin_reorg_lock;
|
||||
use crate::orphans::replay_errors::should_retry_staged_candidate;
|
||||
use crate::orphans::structs::{OrphanCheckup, UndoTransactions};
|
||||
use crate::orphans::undo_block_transactions::undo_transactions;
|
||||
use crate::records::memory::torrent_status::{
|
||||
get_torrent_status, set_torrent_status, TorrentStatus,
|
||||
};
|
||||
use crate::torrent::structs::Torrent;
|
||||
use crate::records::unpack_block::load_by_binary_data::load_block_from_binary;
|
||||
use crate::records::unpack_block::unpack_header::load_block_header;
|
||||
use crate::torrent::structs::{DownloadSave, Torrent};
|
||||
use crate::torrent::torrenting_system::create_file::combine_pieces;
|
||||
use crate::torrent::torrenting_system::download_pieces::download_block_pieces;
|
||||
use crate::torrent::torrenting_system::save_torrent::{
|
||||
list_staged_torrents_for_height, read_staged_torrent,
|
||||
};
|
||||
use crate::torrent::torrenting_system::temp_database_storage::remove_block_pieces_from_db;
|
||||
use crate::torrent::torrenting_system::torrent_map::create_torrent_map;
|
||||
use crate::torrent::unpack_local_torrent::load_torrent;
|
||||
use crate::verifications::verification_service::global_verification_service;
|
||||
|
||||
async fn staged_candidates_for_height(height: u32) -> Vec<Torrent> {
|
||||
let mut candidates = Vec::new();
|
||||
|
|
@ -42,12 +51,12 @@ fn torrent_beats(left: &Torrent, right: &Torrent) -> bool {
|
|||
.is_lt()
|
||||
}
|
||||
|
||||
async fn best_competing_candidate(
|
||||
async fn ordered_competing_candidates(
|
||||
height: u32,
|
||||
local_torrent: &Torrent,
|
||||
candidates: &[Torrent],
|
||||
) -> Option<Torrent> {
|
||||
let mut preferred: Option<Torrent> = None;
|
||||
) -> Vec<Torrent> {
|
||||
let mut ordered = Vec::new();
|
||||
|
||||
for torrent in candidates {
|
||||
// Identical info hashes are the same block candidate, not a competing fork.
|
||||
|
|
@ -64,19 +73,83 @@ async fn best_competing_candidate(
|
|||
continue;
|
||||
}
|
||||
|
||||
match &preferred {
|
||||
Some(current_torrent) => {
|
||||
// Among candidates that have not been ruled out, choose the
|
||||
// same deterministic winner the block fight uses.
|
||||
if torrent_beats(torrent, current_torrent) {
|
||||
preferred = Some(torrent.clone());
|
||||
}
|
||||
}
|
||||
None => preferred = Some(torrent.clone()),
|
||||
ordered.push(torrent.clone());
|
||||
}
|
||||
|
||||
// Among candidates that have not been ruled out, choose the same
|
||||
// deterministic order the block fight uses.
|
||||
ordered.sort_by(|a, b| {
|
||||
a.info
|
||||
.timestamp
|
||||
.cmp(&b.info.timestamp)
|
||||
.then(a.info.nonce.cmp(&b.info.nonce))
|
||||
.then(a.info.vrf.cmp(&b.info.vrf))
|
||||
});
|
||||
ordered
|
||||
}
|
||||
|
||||
async fn cleanup_candidate_pieces(db: &crate::sled::Db, height: u32, torrent: &Torrent) {
|
||||
let _ = remove_block_pieces_from_db(db, height, &torrent.info.info_hash).await;
|
||||
}
|
||||
|
||||
async fn candidate_attaches_before_rollback(
|
||||
params: &OrphanCheckup,
|
||||
height: u32,
|
||||
torrent: &Torrent,
|
||||
wallet_key: &str,
|
||||
) -> Result<(), String> {
|
||||
// Metadata may choose a candidate, but only downloaded block bytes can
|
||||
// prove the rollback is safe.
|
||||
torrent.verify(height, ¶ms.db, wallet_key).await?;
|
||||
|
||||
let verification_service = global_verification_service()
|
||||
.ok_or_else(|| "Verification service not initialized".to_string())?;
|
||||
let torrent_map = create_torrent_map(torrent).await?;
|
||||
let download_save_params = DownloadSave {
|
||||
torrent_map,
|
||||
torrent: torrent.clone(),
|
||||
staged_path: String::new(),
|
||||
block_number: height,
|
||||
allow_during_reorg: true,
|
||||
allow_historical: true,
|
||||
db: params.db.clone(),
|
||||
verification_service: std::sync::Arc::new(verification_service),
|
||||
map: params.map.clone(),
|
||||
};
|
||||
|
||||
download_block_pieces(download_save_params).await?;
|
||||
let result = combine_pieces(¶ms.db, height, &torrent.info.info_hash).await?;
|
||||
|
||||
if result.len() != torrent.info.length as usize {
|
||||
cleanup_candidate_pieces(¶ms.db, height, torrent).await;
|
||||
return Err("Downloaded candidate length does not match torrent metadata.".to_string());
|
||||
}
|
||||
|
||||
if skein_128_hash_bytes(&result) != torrent.info.info_hash {
|
||||
cleanup_candidate_pieces(¶ms.db, height, torrent).await;
|
||||
return Err("Hash validation failed for complete block".to_string());
|
||||
}
|
||||
|
||||
let loaded_block = load_block_from_binary(&result)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to load block from binary: {err}"))?;
|
||||
let header_hash = loaded_block.vrf_block.hash().await;
|
||||
if header_hash != torrent.info.block_hash {
|
||||
cleanup_candidate_pieces(¶ms.db, height, torrent).await;
|
||||
return Err("Candidate header hash does not match torrent metadata.".to_string());
|
||||
}
|
||||
|
||||
if height > 0 {
|
||||
let parent_height = height - 1;
|
||||
let parent_header = load_block_header(parent_height).await?;
|
||||
let parent_hash = parent_header.hash().await;
|
||||
if loaded_block.vrf_block.unmined_block.previous_hash != parent_hash {
|
||||
cleanup_candidate_pieces(¶ms.db, height, torrent).await;
|
||||
return Err("Incorrect previous_block_hash.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
preferred
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), String> {
|
||||
|
|
@ -89,41 +162,69 @@ pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), Stri
|
|||
let local_torrent = load_torrent(¶ms.db, height).await?;
|
||||
let staged_candidates = staged_candidates_for_height(height).await;
|
||||
|
||||
if let Some(competing_torrent) =
|
||||
best_competing_candidate(height, &local_torrent, &staged_candidates).await
|
||||
{
|
||||
let competing_info_hash = competing_torrent.info.info_hash.clone();
|
||||
// If the best staged torrent wins this height, rollback starts
|
||||
// here and replay rebuilds forward from staged candidates.
|
||||
let undo_transactions_params = UndoTransactions {
|
||||
start_height: height,
|
||||
db: params.db.clone(),
|
||||
stream: params.stream.clone(),
|
||||
map: params.map.clone(),
|
||||
node_syncing: params.node_syncing,
|
||||
connections_key: params.connections_key.clone(),
|
||||
};
|
||||
let ordered_candidates =
|
||||
ordered_competing_candidates(height, &local_torrent, &staged_candidates).await;
|
||||
|
||||
if torrent_beats(&competing_torrent, &local_torrent) {
|
||||
set_torrent_status(height, &competing_info_hash, TorrentStatus::Valid).await;
|
||||
if !params.node_syncing {
|
||||
begin_reorg_lock().await;
|
||||
for competing_torrent in ordered_candidates {
|
||||
let competing_info_hash = competing_torrent.info.info_hash.clone();
|
||||
|
||||
if !torrent_beats(&competing_torrent, &local_torrent) {
|
||||
// The local block remains the winner at this height. Since
|
||||
// candidates are sorted best-first, every remaining staged
|
||||
// competitor has also lost to the local block.
|
||||
for staged_torrent in &staged_candidates {
|
||||
if staged_torrent.info.info_hash != local_torrent.info.info_hash {
|
||||
set_torrent_status(
|
||||
height,
|
||||
&staged_torrent.info.info_hash,
|
||||
TorrentStatus::Invalid,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
info!("[orphan] adopting competing staged chain from height {height}");
|
||||
undo_transactions(undo_transactions_params, wallet_key).await?;
|
||||
return Ok(());
|
||||
break;
|
||||
}
|
||||
|
||||
// The local block remains the winner at this height, so every
|
||||
// staged competitor for the same height has now been checked.
|
||||
for staged_torrent in staged_candidates {
|
||||
if staged_torrent.info.info_hash != local_torrent.info.info_hash {
|
||||
set_torrent_status(
|
||||
height,
|
||||
&staged_torrent.info.info_hash,
|
||||
TorrentStatus::Invalid,
|
||||
)
|
||||
.await;
|
||||
match candidate_attaches_before_rollback(
|
||||
¶ms,
|
||||
height,
|
||||
&competing_torrent,
|
||||
wallet_key,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
let undo_transactions_params = UndoTransactions {
|
||||
start_height: height,
|
||||
db: params.db.clone(),
|
||||
stream: params.stream.clone(),
|
||||
map: params.map.clone(),
|
||||
node_syncing: params.node_syncing,
|
||||
connections_key: params.connections_key.clone(),
|
||||
};
|
||||
|
||||
set_torrent_status(height, &competing_info_hash, TorrentStatus::Valid).await;
|
||||
if !params.node_syncing {
|
||||
begin_reorg_lock().await;
|
||||
}
|
||||
info!("[orphan] adopting proven staged chain from height {height}");
|
||||
undo_transactions(undo_transactions_params, wallet_key).await?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
let status = if should_retry_staged_candidate(&err) {
|
||||
TorrentStatus::Pending
|
||||
} else {
|
||||
TorrentStatus::Invalid
|
||||
};
|
||||
set_torrent_status(height, &competing_info_hash, status).await;
|
||||
warn!(
|
||||
"[orphan] staged candidate failed pre-rollback proof: height={height} err={err}"
|
||||
);
|
||||
|
||||
if status == TorrentStatus::Pending {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ pub fn should_retry_staged_candidate(error: &str) -> bool {
|
|||
|| error.contains("piece not found")
|
||||
|| error.contains("Requested candidate not found")
|
||||
|| error.contains("Block not found")
|
||||
|| (error.contains("Block ") && error.contains(" not found"))
|
||||
|| error.contains("Timed out waiting for piece")
|
||||
|| error.contains("Timed out waiting for replacement torrent")
|
||||
|| error.contains("No replacement torrent received")
|
||||
|| error.contains("Piece reply channel closed")
|
||||
|| error.contains("Replay waiting for block pieces")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::orphans::structs::UndoTransactions;
|
||||
use crate::orphans::replay_errors::should_retry_staged_candidate;
|
||||
use crate::orphans::structs::UndoTransactions;
|
||||
use crate::orphans::torrent_candidates::hydrate_torrent_candidates;
|
||||
use crate::records::block_height::get_block_height::get_height;
|
||||
use crate::records::memory::response_channels::reserve_entry;
|
||||
use crate::records::memory::torrent_status::{
|
||||
|
|
@ -13,6 +14,7 @@ use crate::torrent::torrenting_system::torrent_requests::{
|
|||
handle_response_and_save_torrent, send_request_torrent_message,
|
||||
};
|
||||
use crate::{timeout, Duration};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub async fn save_new_blocks(
|
||||
params: &UndoTransactions,
|
||||
|
|
@ -22,6 +24,7 @@ pub async fn save_new_blocks(
|
|||
) -> Result<(), String> {
|
||||
// after rollback, request and save each remote block from the
|
||||
// divergence point up to the height we need to restore
|
||||
let mut hydrated_heights = HashSet::new();
|
||||
loop {
|
||||
let mut resolved_from_staging = false;
|
||||
let staged_candidates = list_staged_torrents_for_height(true_start_height).await?;
|
||||
|
|
@ -101,12 +104,7 @@ pub async fn save_new_blocks(
|
|||
} else {
|
||||
TorrentStatus::Invalid
|
||||
};
|
||||
set_torrent_status(
|
||||
true_start_height,
|
||||
&torrent_info_hash,
|
||||
status,
|
||||
)
|
||||
.await;
|
||||
set_torrent_status(true_start_height, &torrent_info_hash, status).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -124,6 +122,22 @@ pub async fn save_new_blocks(
|
|||
}
|
||||
}
|
||||
|
||||
if hydrated_heights.insert(true_start_height) {
|
||||
let imported = hydrate_torrent_candidates(
|
||||
params.stream.clone(),
|
||||
params.map.clone(),
|
||||
params.connections_key.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if imported > 0 {
|
||||
// Peer candidate hydration can add staged torrents that
|
||||
// are not canonical on the peer yet. Restart this height
|
||||
// so those candidates are tried before canonical fallback.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// No staged candidate worked, so request the replacement torrent
|
||||
// directly from the connected peer.
|
||||
let (hashmap_key, _save_tx, save_rx) = reserve_entry(params.map.clone()).await;
|
||||
|
|
|
|||
|
|
@ -47,11 +47,8 @@ pub async fn update_snapshot(db: &Db, current_height: u32) -> Result<(), String>
|
|||
let hash = header.hash().await;
|
||||
let value = format!("{snapshot_height}:{hash}");
|
||||
let key = b"snapshot";
|
||||
db.insert(key, value.as_bytes()).map_err(|e| {
|
||||
format!(
|
||||
"Failed to store snapshot at height {snapshot_height}: {e}"
|
||||
)
|
||||
})?;
|
||||
db.insert(key, value.as_bytes())
|
||||
.map_err(|e| format!("Failed to store snapshot at height {snapshot_height}: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +94,9 @@ pub async fn snapshot_verified(params: UndoTransactions, wallet_key: &str) -> bo
|
|||
}
|
||||
}
|
||||
_ => {
|
||||
error!("Unable to verify remote snapshot torrent at height {snap_height}");
|
||||
error!(
|
||||
"Unable to verify remote snapshot torrent at height {snap_height}"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ async fn replay_staged_torrents(params: &OrphanCheckup2, wallet_key: &str) -> Re
|
|||
}
|
||||
|
||||
let mut advanced_height = false;
|
||||
let mut retryable_pending = false;
|
||||
for torrent in ordered_candidates {
|
||||
let torrent_info_hash = torrent.info.info_hash.clone();
|
||||
|
||||
|
|
@ -137,6 +138,7 @@ async fn replay_staged_torrents(params: &OrphanCheckup2, wallet_key: &str) -> Re
|
|||
}
|
||||
Err(err) => {
|
||||
if should_retry_staged_candidate(&err) {
|
||||
retryable_pending = true;
|
||||
// Piece availability is not proof that the candidate
|
||||
// lost the block fight; leave it pending so a later
|
||||
// orphan pass can retry after more peers stage it.
|
||||
|
|
@ -163,6 +165,11 @@ async fn replay_staged_torrents(params: &OrphanCheckup2, wallet_key: &str) -> Re
|
|||
if !advanced_height {
|
||||
// Every staged candidate for the current expected height was
|
||||
// exhausted without extending the chain, so stop replay here.
|
||||
if retryable_pending {
|
||||
return Err(format!(
|
||||
"Replay waiting for block pieces at height {expected_height}"
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
|
@ -196,9 +203,16 @@ pub async fn sync_checkup(params: OrphanCheckup2, wallet_key: &str) -> Result<()
|
|||
if !snapshot_verified(undo_transactions_params, wallet_key).await {
|
||||
// A snapshot rollback already happened, so replay staged torrents and
|
||||
// exit instead of running the near-tip rules against stale heights.
|
||||
let mut replay_waiting = false;
|
||||
match replay_staged_torrents(¶ms, wallet_key).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => error!("[orphan] staged torrent replay error: {err}"),
|
||||
Err(err) => {
|
||||
replay_waiting = should_retry_staged_candidate(&err);
|
||||
error!("[orphan] staged torrent replay error: {err}");
|
||||
}
|
||||
}
|
||||
if replay_waiting && !params.node_syncing {
|
||||
return Err("orphan replay is waiting for block data".to_string());
|
||||
}
|
||||
if !params.node_syncing {
|
||||
end_reorg_lock();
|
||||
|
|
@ -218,14 +232,33 @@ pub async fn sync_checkup(params: OrphanCheckup2, wallet_key: &str) -> Result<()
|
|||
};
|
||||
deep_sync_rollback(checkup_params.clone(), wallet_key).await;
|
||||
|
||||
let mut replay_waiting = false;
|
||||
let height_before_window_check = get_height(¶ms.db);
|
||||
match orphan_window_check(checkup_params, wallet_key).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => error!("[orphan] orphan window check error: {err}"),
|
||||
Err(err) => {
|
||||
if should_retry_staged_candidate(&err)
|
||||
&& get_height(¶ms.db) < height_before_window_check
|
||||
{
|
||||
replay_waiting = true;
|
||||
}
|
||||
error!("[orphan] orphan window check error: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
let height_before_replay = get_height(¶ms.db);
|
||||
match replay_staged_torrents(¶ms, wallet_key).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => error!("[orphan] staged torrent replay error: {err}"),
|
||||
Err(err) => {
|
||||
replay_waiting |= should_retry_staged_candidate(&err);
|
||||
error!("[orphan] staged torrent replay error: {err}");
|
||||
}
|
||||
}
|
||||
if get_height(¶ms.db) > height_before_replay {
|
||||
replay_waiting = false;
|
||||
}
|
||||
if replay_waiting && !params.node_syncing {
|
||||
return Err("orphan replay is waiting for block data".to_string());
|
||||
}
|
||||
if !params.node_syncing {
|
||||
end_reorg_lock();
|
||||
|
|
|
|||
|
|
@ -39,10 +39,17 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re
|
|||
for transaction in transactions.into_iter().rev() {
|
||||
match transaction {
|
||||
Transaction::Rewards(rewards_tx) => {
|
||||
undo_rewards_transaction(rewards_tx, &mining_receiver, ¶ms.db, current_height).await
|
||||
undo_rewards_transaction(
|
||||
rewards_tx,
|
||||
&mining_receiver,
|
||||
¶ms.db,
|
||||
current_height,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Transaction::Transfer(transfer_tx) => {
|
||||
undo_transfer_transaction(transfer_tx.clone(), &mining_receiver, ¶ms.db).await;
|
||||
undo_transfer_transaction(transfer_tx.clone(), &mining_receiver, ¶ms.db)
|
||||
.await;
|
||||
rolled_back_transactions.push(Transaction::Transfer(transfer_tx));
|
||||
}
|
||||
Transaction::Burn(burn_tx) => {
|
||||
|
|
@ -50,20 +57,35 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re
|
|||
rolled_back_transactions.push(Transaction::Burn(burn_tx));
|
||||
}
|
||||
Transaction::Token(create_token_tx) => {
|
||||
undo_create_token_transaction(create_token_tx.clone(), &mining_receiver, ¶ms.db)
|
||||
.await;
|
||||
undo_create_token_transaction(
|
||||
create_token_tx.clone(),
|
||||
&mining_receiver,
|
||||
¶ms.db,
|
||||
)
|
||||
.await;
|
||||
rolled_back_transactions.push(Transaction::Token(create_token_tx));
|
||||
}
|
||||
Transaction::IssueToken(issue_token_tx) => {
|
||||
undo_issue_token_transaction(issue_token_tx.clone(), &mining_receiver, ¶ms.db).await;
|
||||
undo_issue_token_transaction(
|
||||
issue_token_tx.clone(),
|
||||
&mining_receiver,
|
||||
¶ms.db,
|
||||
)
|
||||
.await;
|
||||
rolled_back_transactions.push(Transaction::IssueToken(issue_token_tx));
|
||||
}
|
||||
Transaction::Nft(create_nft_tx) => {
|
||||
undo_create_nft_transaction(create_nft_tx.clone(), &mining_receiver, ¶ms.db).await;
|
||||
undo_create_nft_transaction(
|
||||
create_nft_tx.clone(),
|
||||
&mining_receiver,
|
||||
¶ms.db,
|
||||
)
|
||||
.await;
|
||||
rolled_back_transactions.push(Transaction::Nft(create_nft_tx));
|
||||
}
|
||||
Transaction::Marketing(marketing_tx) => {
|
||||
undo_marketing_transaction(marketing_tx.clone(), &mining_receiver, ¶ms.db).await;
|
||||
undo_marketing_transaction(marketing_tx.clone(), &mining_receiver, ¶ms.db)
|
||||
.await;
|
||||
rolled_back_transactions.push(Transaction::Marketing(marketing_tx));
|
||||
}
|
||||
Transaction::Swap(swap_tx) => {
|
||||
|
|
@ -71,15 +93,22 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re
|
|||
rolled_back_transactions.push(Transaction::Swap(swap_tx));
|
||||
}
|
||||
Transaction::Lender(loan_tx) => {
|
||||
undo_loan_creation_transaction(loan_tx.clone(), &mining_receiver, ¶ms.db).await;
|
||||
undo_loan_creation_transaction(loan_tx.clone(), &mining_receiver, ¶ms.db)
|
||||
.await;
|
||||
rolled_back_transactions.push(Transaction::Lender(loan_tx));
|
||||
}
|
||||
Transaction::Borrower(borrower_tx) => {
|
||||
undo_borrower_transaction(borrower_tx.clone(), &mining_receiver, ¶ms.db).await?;
|
||||
undo_borrower_transaction(borrower_tx.clone(), &mining_receiver, ¶ms.db)
|
||||
.await?;
|
||||
rolled_back_transactions.push(Transaction::Borrower(borrower_tx));
|
||||
}
|
||||
Transaction::Collateral(collateral_tx) => {
|
||||
undo_collateral_transaction(collateral_tx.clone(), &mining_receiver, ¶ms.db).await?;
|
||||
undo_collateral_transaction(
|
||||
collateral_tx.clone(),
|
||||
&mining_receiver,
|
||||
¶ms.db,
|
||||
)
|
||||
.await?;
|
||||
rolled_back_transactions.push(Transaction::Collateral(collateral_tx));
|
||||
}
|
||||
Transaction::Genesis(_) => {
|
||||
|
|
@ -87,10 +116,11 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re
|
|||
// the requested rollback boundary is invalid.
|
||||
return Err(
|
||||
"Genesis transaction cannot be undone by orphan rollback".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
Transaction::Vanity(vanity_tx) => {
|
||||
undo_vanity_transaction(vanity_tx.clone(), &mining_receiver, ¶ms.db).await?;
|
||||
undo_vanity_transaction(vanity_tx.clone(), &mining_receiver, ¶ms.db)
|
||||
.await?;
|
||||
rolled_back_transactions.push(Transaction::Vanity(vanity_tx));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ use crate::blocks::transfer::TransferTransaction;
|
|||
use crate::blocks::vanity::VanityAddressTransaction;
|
||||
use crate::common::nft_assets::nft_asset_name;
|
||||
use crate::common::types::Transaction;
|
||||
use crate::decode;
|
||||
use crate::records::memory::mempool::{restore_processed_by_signatures, BASECOIN};
|
||||
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::checks::balance_check::balance_checkup;
|
||||
use crate::decode;
|
||||
|
||||
async fn restore_if_spendable<F, Fut>(signatures: &[String], spendable: bool, insert: F)
|
||||
where
|
||||
|
|
@ -107,8 +107,14 @@ pub async fn restore_create_nft(transaction: &CreateNftTransaction, db: &Db) {
|
|||
|
||||
pub async fn restore_marketing(transaction: &MarketingTransaction, db: &Db) {
|
||||
let marketing = &transaction.unsigned_marketing;
|
||||
let spendable =
|
||||
balance_checkup(db, 0, marketing.txfee, BASECOIN.clone(), &marketing.advertiser).await;
|
||||
let spendable = balance_checkup(
|
||||
db,
|
||||
0,
|
||||
marketing.txfee,
|
||||
BASECOIN.clone(),
|
||||
&marketing.advertiser,
|
||||
)
|
||||
.await;
|
||||
let signature = transaction.signature.clone();
|
||||
|
||||
restore_if_spendable(&[signature], spendable, || async {
|
||||
|
|
@ -123,31 +129,53 @@ pub async fn restore_swap(transaction: &SwapTransaction, db: &Db) {
|
|||
let asset2 = nft_asset_name(&swap.ticker2, swap.nft_series2);
|
||||
let value1 = swap.value1.saturating_add(swap.tip1);
|
||||
let value2 = swap.value2.saturating_add(swap.tip2);
|
||||
let sender1_spendable =
|
||||
balance_checkup(db, value1, swap.txfee1, asset1, &swap.sender1).await;
|
||||
let sender2_spendable =
|
||||
balance_checkup(db, value2, swap.txfee2, asset2, &swap.sender2).await;
|
||||
let signatures = vec![transaction.signature1.clone(), transaction.signature2.clone()];
|
||||
let sender1_spendable = balance_checkup(db, value1, swap.txfee1, asset1, &swap.sender1).await;
|
||||
let sender2_spendable = balance_checkup(db, value2, swap.txfee2, asset2, &swap.sender2).await;
|
||||
let signatures = vec![
|
||||
transaction.signature1.clone(),
|
||||
transaction.signature2.clone(),
|
||||
];
|
||||
|
||||
restore_if_spendable(&signatures, sender1_spendable && sender2_spendable, || async {
|
||||
let _ = transaction.add_to_memory().await;
|
||||
})
|
||||
restore_if_spendable(
|
||||
&signatures,
|
||||
sender1_spendable && sender2_spendable,
|
||||
|| async {
|
||||
let _ = transaction.add_to_memory().await;
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn restore_loan_creation(transaction: &LoanContractTransaction, db: &Db) {
|
||||
let loan = &transaction.unsigned_loan_contract;
|
||||
let lender_spendable =
|
||||
balance_checkup(db, loan.loan_amount, loan.txfee, loan.loan_coin.clone(), &loan.lender)
|
||||
.await;
|
||||
let borrower_spendable =
|
||||
balance_checkup(db, loan.collateral_amount, 0, loan.collateral.clone(), &loan.borrower)
|
||||
.await;
|
||||
let signatures = vec![transaction.signature1.clone(), transaction.signature2.clone()];
|
||||
let lender_spendable = balance_checkup(
|
||||
db,
|
||||
loan.loan_amount,
|
||||
loan.txfee,
|
||||
loan.loan_coin.clone(),
|
||||
&loan.lender,
|
||||
)
|
||||
.await;
|
||||
let borrower_spendable = balance_checkup(
|
||||
db,
|
||||
loan.collateral_amount,
|
||||
0,
|
||||
loan.collateral.clone(),
|
||||
&loan.borrower,
|
||||
)
|
||||
.await;
|
||||
let signatures = vec![
|
||||
transaction.signature1.clone(),
|
||||
transaction.signature2.clone(),
|
||||
];
|
||||
|
||||
restore_if_spendable(&signatures, lender_spendable && borrower_spendable, || async {
|
||||
let _ = transaction.add_to_memory().await;
|
||||
})
|
||||
restore_if_spendable(
|
||||
&signatures,
|
||||
lender_spendable && borrower_spendable,
|
||||
|| async {
|
||||
let _ = transaction.add_to_memory().await;
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
|
|
@ -168,8 +196,14 @@ pub async fn restore_borrower(transaction: &ContractPaymentTransaction, db: &Db)
|
|||
|
||||
pub async fn restore_collateral(transaction: &CollateralClaimTransaction, db: &Db) {
|
||||
let collateral = &transaction.unsigned_collateral_claim;
|
||||
let spendable =
|
||||
balance_checkup(db, 0, collateral.txfee, BASECOIN.clone(), &collateral.address).await;
|
||||
let spendable = balance_checkup(
|
||||
db,
|
||||
0,
|
||||
collateral.txfee,
|
||||
BASECOIN.clone(),
|
||||
&collateral.address,
|
||||
)
|
||||
.await;
|
||||
let signature = transaction.signature.clone();
|
||||
|
||||
restore_if_spendable(&[signature], spendable, || async {
|
||||
|
|
|
|||
|
|
@ -1,93 +1,92 @@
|
|||
use crate::blocks::burn::BurnTransaction;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::blocks::burn::BurnTransaction;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::common::nft_assets::nft_asset_name;
|
||||
use crate::decode;
|
||||
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
|
||||
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
|
||||
use crate::records::record_chain::token_provenance::remove_token_history_entry;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_burn_transaction(transaction: BurnTransaction, mining_receiver: &str, db: &Db) {
|
||||
// Reverse the burn fee and burned-asset balance movement before
|
||||
// restoring the live token or NFT state back into the active chain.
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let burned_asset = nft_asset_name(
|
||||
&transaction.unsigned_burn.coin,
|
||||
transaction.unsigned_burn.nft_series,
|
||||
);
|
||||
|
||||
// Remove the miner fee, refund the burner fee, and return the burned asset
|
||||
// to the burner balance.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
transaction.unsigned_burn.txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
&transaction.unsigned_burn.address,
|
||||
transaction.unsigned_burn.txfee,
|
||||
&type_str,
|
||||
operand_addition,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
&transaction.unsigned_burn.address,
|
||||
transaction.unsigned_burn.value,
|
||||
&burned_asset,
|
||||
operand_addition,
|
||||
);
|
||||
|
||||
let hash_binary = decode(&transaction.unsigned_burn.hash().await).unwrap();
|
||||
|
||||
// Delete the txid lookup inserted when the burn was saved.
|
||||
let txid_tree = db.open_tree("txid").unwrap();
|
||||
txid_tree.remove(hash_binary.clone()).unwrap();
|
||||
|
||||
// Restore NFT rows directly, or add the burned amount back into the
|
||||
// fungible token supply if this burn targeted a token asset.
|
||||
let nft_tree = db.open_tree("nfts").unwrap();
|
||||
let nft_origin_tree = db.open_tree("nft_origins").unwrap();
|
||||
if nft_origin_tree
|
||||
.contains_key(burned_asset.as_bytes())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// NFT burns remove the live NFT row; rollback restores the row and
|
||||
// removes the burn from NFT history.
|
||||
let _ = remove_nft_history_entry(db, &burned_asset, &hash_binary);
|
||||
let _ = nft_tree.insert(burned_asset.as_bytes(), b"1");
|
||||
} else {
|
||||
// Token burns reduce supply; rollback adds the burned amount back.
|
||||
let _ = remove_token_history_entry(db, &transaction.unsigned_burn.coin, &hash_binary);
|
||||
let token_tree = db.open_tree("tokens").unwrap();
|
||||
let current_supply = token_tree
|
||||
.get(transaction.unsigned_burn.coin.as_bytes())
|
||||
.unwrap()
|
||||
.map(|bytes| {
|
||||
let mut supply_bytes = [0u8; 8];
|
||||
supply_bytes.copy_from_slice(bytes.as_ref());
|
||||
u64::from_le_bytes(supply_bytes)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let restored_supply = current_supply.saturating_add(transaction.unsigned_burn.value);
|
||||
let _ = token_tree.insert(
|
||||
transaction.unsigned_burn.coin.as_bytes(),
|
||||
&restored_supply.to_le_bytes(),
|
||||
);
|
||||
}
|
||||
|
||||
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
|
||||
use crate::records::record_chain::token_provenance::remove_token_history_entry;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_burn_transaction(transaction: BurnTransaction, mining_receiver: &str, db: &Db) {
|
||||
// Reverse the burn fee and burned-asset balance movement before
|
||||
// restoring the live token or NFT state back into the active chain.
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let burned_asset = nft_asset_name(
|
||||
&transaction.unsigned_burn.coin,
|
||||
transaction.unsigned_burn.nft_series,
|
||||
);
|
||||
|
||||
// Remove the miner fee, refund the burner fee, and return the burned asset
|
||||
// to the burner balance.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
transaction.unsigned_burn.txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
&transaction.unsigned_burn.address,
|
||||
transaction.unsigned_burn.txfee,
|
||||
&type_str,
|
||||
operand_addition,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
&transaction.unsigned_burn.address,
|
||||
transaction.unsigned_burn.value,
|
||||
&burned_asset,
|
||||
operand_addition,
|
||||
);
|
||||
|
||||
let hash_binary = decode(&transaction.unsigned_burn.hash().await).unwrap();
|
||||
|
||||
// Delete the txid lookup inserted when the burn was saved.
|
||||
let txid_tree = db.open_tree("txid").unwrap();
|
||||
txid_tree.remove(hash_binary.clone()).unwrap();
|
||||
|
||||
// Restore NFT rows directly, or add the burned amount back into the
|
||||
// fungible token supply if this burn targeted a token asset.
|
||||
let nft_tree = db.open_tree("nfts").unwrap();
|
||||
let nft_origin_tree = db.open_tree("nft_origins").unwrap();
|
||||
if nft_origin_tree
|
||||
.contains_key(burned_asset.as_bytes())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// NFT burns remove the live NFT row; rollback restores the row and
|
||||
// removes the burn from NFT history.
|
||||
let _ = remove_nft_history_entry(db, &burned_asset, &hash_binary);
|
||||
let _ = nft_tree.insert(burned_asset.as_bytes(), b"1");
|
||||
} else {
|
||||
// Token burns reduce supply; rollback adds the burned amount back.
|
||||
let _ = remove_token_history_entry(db, &transaction.unsigned_burn.coin, &hash_binary);
|
||||
let token_tree = db.open_tree("tokens").unwrap();
|
||||
let current_supply = token_tree
|
||||
.get(transaction.unsigned_burn.coin.as_bytes())
|
||||
.unwrap()
|
||||
.map(|bytes| {
|
||||
let mut supply_bytes = [0u8; 8];
|
||||
supply_bytes.copy_from_slice(bytes.as_ref());
|
||||
u64::from_le_bytes(supply_bytes)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let restored_supply = current_supply.saturating_add(transaction.unsigned_burn.value);
|
||||
let _ = token_tree.insert(
|
||||
transaction.unsigned_burn.coin.as_bytes(),
|
||||
&restored_supply.to_le_bytes(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,81 +1,80 @@
|
|||
use crate::blocks::nft::CreateNftTransaction;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::common::nft_assets::nft_asset_name;
|
||||
use crate::blocks::nft::CreateNftTransaction;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::common::nft_assets::nft_asset_name;
|
||||
use crate::decode;
|
||||
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
|
||||
use crate::records::record_chain::nft_provenance::{remove_nft_history_entry, remove_nft_origin};
|
||||
use crate::sled::Db;
|
||||
|
||||
const NFT_UNIT: u64 = 100_000_000;
|
||||
|
||||
pub async fn undo_create_nft_transaction(
|
||||
transaction: CreateNftTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// remove the created nft state and restore the creator balances
|
||||
// when a create-nft transaction is rolled back
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (txfee, creator) = (
|
||||
&transaction.unsigned_create_nft.txfee,
|
||||
&transaction.unsigned_create_nft.creator,
|
||||
);
|
||||
|
||||
// Remove the miner fee and refund the creator's base-coin fee.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition);
|
||||
let hash_binary = decode(&transaction.unsigned_create_nft.hash().await).unwrap();
|
||||
|
||||
// Remove the create-NFT transaction lookup from the txid tree.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash_binary.clone();
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
// remove each created nft item and clear the
|
||||
// associated provenance entries
|
||||
let tree = db.open_tree("nfts").unwrap();
|
||||
if transaction.unsigned_create_nft.series == 1 {
|
||||
// Series creation mints numbered items, so each item is removed
|
||||
// individually from balances, provenance, origins, and the NFT tree.
|
||||
for item_number in 1..=transaction.unsigned_create_nft.count {
|
||||
let nft_name = nft_asset_name(&transaction.unsigned_create_nft.nft_name, item_number);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
creator,
|
||||
NFT_UNIT,
|
||||
&nft_name,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = remove_nft_history_entry(db, &nft_name, &hash_binary);
|
||||
let _ = remove_nft_origin(db, &nft_name);
|
||||
tree.remove(nft_name.as_bytes()).unwrap();
|
||||
}
|
||||
} else {
|
||||
// Single NFT creation only writes the base NFT name.
|
||||
let nft_name = &transaction.unsigned_create_nft.nft_name;
|
||||
let _ =
|
||||
balance_sheet_operation_with_db(db, creator, NFT_UNIT, nft_name, operand_subtraction);
|
||||
let _ = remove_nft_history_entry(db, nft_name, &hash_binary);
|
||||
let _ = remove_nft_origin(db, nft_name);
|
||||
tree.remove(nft_name.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
use crate::records::record_chain::nft_provenance::{remove_nft_history_entry, remove_nft_origin};
|
||||
use crate::sled::Db;
|
||||
|
||||
const NFT_UNIT: u64 = 100_000_000;
|
||||
|
||||
pub async fn undo_create_nft_transaction(
|
||||
transaction: CreateNftTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// remove the created nft state and restore the creator balances
|
||||
// when a create-nft transaction is rolled back
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (txfee, creator) = (
|
||||
&transaction.unsigned_create_nft.txfee,
|
||||
&transaction.unsigned_create_nft.creator,
|
||||
);
|
||||
|
||||
// Remove the miner fee and refund the creator's base-coin fee.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition);
|
||||
let hash_binary = decode(&transaction.unsigned_create_nft.hash().await).unwrap();
|
||||
|
||||
// Remove the create-NFT transaction lookup from the txid tree.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash_binary.clone();
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
// remove each created nft item and clear the
|
||||
// associated provenance entries
|
||||
let tree = db.open_tree("nfts").unwrap();
|
||||
if transaction.unsigned_create_nft.series == 1 {
|
||||
// Series creation mints numbered items, so each item is removed
|
||||
// individually from balances, provenance, origins, and the NFT tree.
|
||||
for item_number in 1..=transaction.unsigned_create_nft.count {
|
||||
let nft_name = nft_asset_name(&transaction.unsigned_create_nft.nft_name, item_number);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
creator,
|
||||
NFT_UNIT,
|
||||
&nft_name,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = remove_nft_history_entry(db, &nft_name, &hash_binary);
|
||||
let _ = remove_nft_origin(db, &nft_name);
|
||||
tree.remove(nft_name.as_bytes()).unwrap();
|
||||
}
|
||||
} else {
|
||||
// Single NFT creation only writes the base NFT name.
|
||||
let nft_name = &transaction.unsigned_create_nft.nft_name;
|
||||
let _ =
|
||||
balance_sheet_operation_with_db(db, creator, NFT_UNIT, nft_name, operand_subtraction);
|
||||
let _ = remove_nft_history_entry(db, nft_name, &hash_binary);
|
||||
let _ = remove_nft_origin(db, nft_name);
|
||||
tree.remove(nft_name.as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +1,68 @@
|
|||
use crate::blocks::token::CreateTokenTransaction;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::blocks::token::CreateTokenTransaction;
|
||||
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::token_provenance::clear_token_history;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_create_token_transaction(
|
||||
transaction: CreateTokenTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// remove the created token state and restore the creator balances
|
||||
// when a create-token transaction is rolled back
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (txfee, creator, ticker, number) = (
|
||||
&transaction.unsigned_create_token.txfee,
|
||||
&transaction.unsigned_create_token.creator,
|
||||
&transaction.unsigned_create_token.ticker,
|
||||
&transaction.unsigned_create_token.number,
|
||||
);
|
||||
|
||||
// Remove the miner fee, refund the creator fee, and remove the created
|
||||
// supply from the creator balance.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(db, creator, *number, ticker, operand_subtraction);
|
||||
|
||||
let ticker_binary = &transaction.unsigned_create_token.ticker.as_bytes();
|
||||
let hash_binary = decode(&transaction.unsigned_create_token.hash().await).unwrap();
|
||||
|
||||
// Remove the create-token transaction lookup from the txid tree.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash_binary.clone();
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
// remove the token definition and origin entry
|
||||
// created when the token was first saved
|
||||
let tree = db.open_tree("tokens").unwrap();
|
||||
let key = ticker_binary;
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
let origin_tree = db.open_tree("token_origins").unwrap();
|
||||
origin_tree.remove(key).unwrap();
|
||||
|
||||
let limit_tree = db.open_tree("token_limits").unwrap();
|
||||
limit_tree.remove(key).unwrap();
|
||||
// Token history is cleared because the token itself no longer exists.
|
||||
let _ = clear_token_history(db, ticker);
|
||||
|
||||
use crate::records::record_chain::token_provenance::clear_token_history;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_create_token_transaction(
|
||||
transaction: CreateTokenTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// remove the created token state and restore the creator balances
|
||||
// when a create-token transaction is rolled back
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (txfee, creator, ticker, number) = (
|
||||
&transaction.unsigned_create_token.txfee,
|
||||
&transaction.unsigned_create_token.creator,
|
||||
&transaction.unsigned_create_token.ticker,
|
||||
&transaction.unsigned_create_token.number,
|
||||
);
|
||||
|
||||
// Remove the miner fee, refund the creator fee, and remove the created
|
||||
// supply from the creator balance.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(db, creator, *number, ticker, operand_subtraction);
|
||||
|
||||
let ticker_binary = &transaction.unsigned_create_token.ticker.as_bytes();
|
||||
let hash_binary = decode(&transaction.unsigned_create_token.hash().await).unwrap();
|
||||
|
||||
// Remove the create-token transaction lookup from the txid tree.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash_binary.clone();
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
// remove the token definition and origin entry
|
||||
// created when the token was first saved
|
||||
let tree = db.open_tree("tokens").unwrap();
|
||||
let key = ticker_binary;
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
let origin_tree = db.open_tree("token_origins").unwrap();
|
||||
origin_tree.remove(key).unwrap();
|
||||
|
||||
let limit_tree = db.open_tree("token_limits").unwrap();
|
||||
limit_tree.remove(key).unwrap();
|
||||
// Token history is cleared because the token itself no longer exists.
|
||||
let _ = clear_token_history(db, ticker);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +1,61 @@
|
|||
use crate::blocks::issue_token::IssueTokenTransaction;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::blocks::issue_token::IssueTokenTransaction;
|
||||
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::token_provenance::remove_token_history_entry;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_issue_token_transaction(
|
||||
transaction: IssueTokenTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// Reverse the issued supply and fee movements so rollback restores
|
||||
// both the creator balance and the stored token supply.
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let txfee = &transaction.unsigned_issue_token.txfee;
|
||||
let creator = &transaction.unsigned_issue_token.creator;
|
||||
let ticker = &transaction.unsigned_issue_token.ticker;
|
||||
let number = &transaction.unsigned_issue_token.number;
|
||||
|
||||
// Remove the miner fee, refund the issuer fee, and take the issued amount
|
||||
// back out of the issuer balance.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(db, creator, *number, ticker, operand_subtraction);
|
||||
|
||||
let hash_binary = decode(&transaction.unsigned_issue_token.hash().await).unwrap();
|
||||
|
||||
// Delete the issued-token transaction lookup and provenance record.
|
||||
let txid_tree = db.open_tree("txid").unwrap();
|
||||
txid_tree.remove(hash_binary.clone()).unwrap();
|
||||
let _ = remove_token_history_entry(db, ticker, &hash_binary);
|
||||
|
||||
// Restore the previous live token supply by subtracting the issued amount.
|
||||
let token_tree = db.open_tree("tokens").unwrap();
|
||||
if let Ok(Some(existing_supply)) = token_tree.get(ticker.as_bytes()) {
|
||||
let mut supply_bytes = [0u8; 8];
|
||||
supply_bytes.copy_from_slice(existing_supply.as_ref());
|
||||
let current_supply = u64::from_le_bytes(supply_bytes);
|
||||
let restored_supply = current_supply.saturating_sub(*number);
|
||||
let _ = token_tree.insert(ticker.as_bytes(), &restored_supply.to_le_bytes());
|
||||
}
|
||||
|
||||
use crate::records::record_chain::token_provenance::remove_token_history_entry;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_issue_token_transaction(
|
||||
transaction: IssueTokenTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// Reverse the issued supply and fee movements so rollback restores
|
||||
// both the creator balance and the stored token supply.
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let txfee = &transaction.unsigned_issue_token.txfee;
|
||||
let creator = &transaction.unsigned_issue_token.creator;
|
||||
let ticker = &transaction.unsigned_issue_token.ticker;
|
||||
let number = &transaction.unsigned_issue_token.number;
|
||||
|
||||
// Remove the miner fee, refund the issuer fee, and take the issued amount
|
||||
// back out of the issuer balance.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(db, creator, *number, ticker, operand_subtraction);
|
||||
|
||||
let hash_binary = decode(&transaction.unsigned_issue_token.hash().await).unwrap();
|
||||
|
||||
// Delete the issued-token transaction lookup and provenance record.
|
||||
let txid_tree = db.open_tree("txid").unwrap();
|
||||
txid_tree.remove(hash_binary.clone()).unwrap();
|
||||
let _ = remove_token_history_entry(db, ticker, &hash_binary);
|
||||
|
||||
// Restore the previous live token supply by subtracting the issued amount.
|
||||
let token_tree = db.open_tree("tokens").unwrap();
|
||||
if let Ok(Some(existing_supply)) = token_tree.get(ticker.as_bytes()) {
|
||||
let mut supply_bytes = [0u8; 8];
|
||||
supply_bytes.copy_from_slice(existing_supply.as_ref());
|
||||
let current_supply = u64::from_le_bytes(supply_bytes);
|
||||
let restored_supply = current_supply.saturating_sub(*number);
|
||||
let _ = token_tree.insert(ticker.as_bytes(), &restored_supply.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,93 +1,92 @@
|
|||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::blocks::loans::LoanContractTransaction;
|
||||
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::nft_provenance::remove_nft_history_entry;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_loan_creation_transaction(
|
||||
transaction: LoanContractTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// remove a loan contract and restore both sides of the
|
||||
// contract balances when the block is rolled back
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (txfee, loan_coin, loan_amount, lender, collateral, 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(),
|
||||
);
|
||||
let collateral_wallet = format!("collateral_{hash}");
|
||||
|
||||
// undo the fee, loan distribution, and collateral
|
||||
// movement that were applied when the contract was saved
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, lender, *txfee, &type_str, operand_addition);
|
||||
let _ =
|
||||
balance_sheet_operation_with_db(db, borrower, *loan_amount, loan_coin, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, lender, *loan_amount, loan_coin, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
&collateral_wallet,
|
||||
*collateral_amount,
|
||||
collateral,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
borrower,
|
||||
*collateral_amount,
|
||||
collateral,
|
||||
operand_addition,
|
||||
);
|
||||
|
||||
let hash_binary = decode(hash).unwrap();
|
||||
|
||||
// delete the txid and remove the active loan record
|
||||
// so the contract no longer exists on-chain
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash_binary.clone();
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
let tree = db.open_tree("loan").unwrap();
|
||||
let loan_key = decode(&transaction.unsigned_loan_contract.hash().await).unwrap();
|
||||
tree.remove(loan_key).unwrap();
|
||||
|
||||
let nft_tree = db.open_tree("nfts").unwrap();
|
||||
// Loan creation can move NFT collateral or NFT loan assets, so clear
|
||||
// provenance entries for whichever side is NFT-backed.
|
||||
if nft_tree
|
||||
.contains_key(collateral.as_bytes())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_loan_creation_transaction(
|
||||
transaction: LoanContractTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// remove a loan contract and restore both sides of the
|
||||
// contract balances when the block is rolled back
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (txfee, loan_coin, loan_amount, lender, collateral, 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(),
|
||||
);
|
||||
let collateral_wallet = format!("collateral_{hash}");
|
||||
|
||||
// undo the fee, loan distribution, and collateral
|
||||
// movement that were applied when the contract was saved
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, lender, *txfee, &type_str, operand_addition);
|
||||
let _ =
|
||||
balance_sheet_operation_with_db(db, borrower, *loan_amount, loan_coin, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, lender, *loan_amount, loan_coin, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
&collateral_wallet,
|
||||
*collateral_amount,
|
||||
collateral,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
borrower,
|
||||
*collateral_amount,
|
||||
collateral,
|
||||
operand_addition,
|
||||
);
|
||||
|
||||
let hash_binary = decode(hash).unwrap();
|
||||
|
||||
// delete the txid and remove the active loan record
|
||||
// so the contract no longer exists on-chain
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash_binary.clone();
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
let tree = db.open_tree("loan").unwrap();
|
||||
let loan_key = decode(&transaction.unsigned_loan_contract.hash().await).unwrap();
|
||||
tree.remove(loan_key).unwrap();
|
||||
|
||||
let nft_tree = db.open_tree("nfts").unwrap();
|
||||
// Loan creation can move NFT collateral or NFT loan assets, so clear
|
||||
// provenance entries for whichever side is NFT-backed.
|
||||
if nft_tree
|
||||
.contains_key(collateral.as_bytes())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +1,48 @@
|
|||
use crate::blocks::marketing::MarketingTransaction;
|
||||
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::sled::Db;
|
||||
|
||||
pub async fn undo_marketing_transaction(
|
||||
transaction: MarketingTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// reverse the fee payment and remove the marketing txid
|
||||
// when a marketing transaction is rolled back
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (txfee, advertiser) = (
|
||||
&transaction.unsigned_marketing.txfee,
|
||||
&transaction.unsigned_marketing.advertiser,
|
||||
);
|
||||
|
||||
// Remove the miner fee and refund the advertiser fee.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, advertiser, *txfee, &type_str, operand_addition);
|
||||
|
||||
let hash = decode(&transaction.unsigned_marketing.hash().await).unwrap();
|
||||
|
||||
// Remove the marketing transaction lookup from the txid tree.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash;
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_marketing_transaction(
|
||||
transaction: MarketingTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// reverse the fee payment and remove the marketing txid
|
||||
// when a marketing transaction is rolled back
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (txfee, advertiser) = (
|
||||
&transaction.unsigned_marketing.txfee,
|
||||
&transaction.unsigned_marketing.advertiser,
|
||||
);
|
||||
|
||||
// Remove the miner fee and refund the advertiser fee.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, advertiser, *txfee, &type_str, operand_addition);
|
||||
|
||||
let hash = decode(&transaction.unsigned_marketing.hash().await).unwrap();
|
||||
|
||||
// Remove the marketing transaction lookup from the txid tree.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash;
|
||||
tree.remove(key).unwrap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
use crate::blocks::rewards::RewardsTransaction;
|
||||
use crate::blocks::rewards::RewardsTransaction;
|
||||
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::rewards_tx::{remove_reward_credit_marker, reward_credit_applied};
|
||||
use crate::records::record_chain::rewards_tx::{
|
||||
remove_reward_credit_marker, reward_credit_applied,
|
||||
};
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_rewards_transaction(
|
||||
|
|
@ -11,34 +13,34 @@ pub async fn undo_rewards_transaction(
|
|||
db: &Db,
|
||||
block_height: u32,
|
||||
) {
|
||||
// remove the miner reward and delete the reward txid
|
||||
// when the block that minted it is rolled back
|
||||
let operand = "subtraction";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
|
||||
let value = transaction.unsigned.value;
|
||||
|
||||
// remove the miner reward and delete the reward txid
|
||||
// when the block that minted it is rolled back
|
||||
let operand = "subtraction";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
|
||||
let value = transaction.unsigned.value;
|
||||
|
||||
// Rewards are only spendable after finalization, so rollback subtracts
|
||||
// them only when that delayed credit has actually been applied.
|
||||
if reward_credit_applied(db, block_height) {
|
||||
let _ = balance_sheet_operation_with_db(db, mining_receiver, value, &type_str, operand);
|
||||
remove_reward_credit_marker(db, block_height);
|
||||
}
|
||||
|
||||
let hash = decode(transaction.unsigned.hash().await).unwrap();
|
||||
|
||||
// Remove the reward transaction lookup from the txid tree.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash;
|
||||
tree.remove(key).unwrap();
|
||||
}
|
||||
|
||||
let hash = decode(transaction.unsigned.hash().await).unwrap();
|
||||
|
||||
// Remove the reward transaction lookup from the txid tree.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash;
|
||||
tree.remove(key).unwrap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,103 +1,102 @@
|
|||
use crate::blocks::swap::SwapTransaction;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::blocks::swap::SwapTransaction;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::common::nft_assets::nft_asset_name;
|
||||
use crate::decode;
|
||||
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
|
||||
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_swap_transaction(transaction: SwapTransaction, mining_receiver: &str, db: &Db) {
|
||||
// reverse both sides of the asset exchange and remove the
|
||||
// swap transaction from chain state during rollback
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (
|
||||
txfee1,
|
||||
txfee2,
|
||||
value1,
|
||||
value2,
|
||||
ticker1,
|
||||
ticker2,
|
||||
nft_series1,
|
||||
nft_series2,
|
||||
sender1,
|
||||
sender2,
|
||||
tip1,
|
||||
tip2,
|
||||
) = (
|
||||
&transaction.unsigned_swap.txfee1,
|
||||
&transaction.unsigned_swap.txfee2,
|
||||
&transaction.unsigned_swap.value1,
|
||||
&transaction.unsigned_swap.value2,
|
||||
&transaction.unsigned_swap.ticker1,
|
||||
&transaction.unsigned_swap.ticker2,
|
||||
&transaction.unsigned_swap.nft_series1,
|
||||
&transaction.unsigned_swap.nft_series2,
|
||||
&transaction.unsigned_swap.sender1,
|
||||
&transaction.unsigned_swap.sender2,
|
||||
&transaction.unsigned_swap.tip1,
|
||||
&transaction.unsigned_swap.tip2,
|
||||
);
|
||||
let asset1 = nft_asset_name(ticker1, *nft_series1);
|
||||
let asset2 = nft_asset_name(ticker2, *nft_series2);
|
||||
|
||||
// Refund both base-coin fees and remove those fees from the miner balance.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee1,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, sender1, *txfee1, &type_str, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee2,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, sender2, *txfee2, &type_str, operand_addition);
|
||||
// Tips are paid in the swapped assets, so they must be reversed per asset.
|
||||
let _ =
|
||||
balance_sheet_operation_with_db(db, mining_receiver, *tip1, &asset1, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, sender1, *tip1, &asset1, operand_addition);
|
||||
let _ =
|
||||
balance_sheet_operation_with_db(db, mining_receiver, *tip2, &asset2, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, sender2, *tip2, &asset2, operand_addition);
|
||||
// Reverse the actual two-sided asset exchange.
|
||||
let _ = balance_sheet_operation_with_db(db, sender1, *value2, &asset2, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, sender2, *value2, &asset2, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(db, sender2, *value1, &asset1, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, sender1, *value1, &asset1, operand_addition);
|
||||
|
||||
// Convert the txid hash back to bytes for tree lookup/removal.
|
||||
let hash = decode(&transaction.unsigned_swap.hash().await).unwrap();
|
||||
|
||||
// Remove the txid lookup for the rolled-back swap.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash.clone();
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
let nft_tree = db.open_tree("nfts").unwrap();
|
||||
// If either side of the swap was an NFT, remove this swap from that asset's
|
||||
// provenance history as well.
|
||||
if nft_tree.contains_key(asset1.as_bytes()).unwrap_or(false) {
|
||||
let _ = remove_nft_history_entry(db, &asset1, &hash);
|
||||
}
|
||||
if nft_tree.contains_key(asset2.as_bytes()).unwrap_or(false) {
|
||||
let _ = remove_nft_history_entry(db, &asset2, &hash);
|
||||
}
|
||||
|
||||
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_swap_transaction(transaction: SwapTransaction, mining_receiver: &str, db: &Db) {
|
||||
// reverse both sides of the asset exchange and remove the
|
||||
// swap transaction from chain state during rollback
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (
|
||||
txfee1,
|
||||
txfee2,
|
||||
value1,
|
||||
value2,
|
||||
ticker1,
|
||||
ticker2,
|
||||
nft_series1,
|
||||
nft_series2,
|
||||
sender1,
|
||||
sender2,
|
||||
tip1,
|
||||
tip2,
|
||||
) = (
|
||||
&transaction.unsigned_swap.txfee1,
|
||||
&transaction.unsigned_swap.txfee2,
|
||||
&transaction.unsigned_swap.value1,
|
||||
&transaction.unsigned_swap.value2,
|
||||
&transaction.unsigned_swap.ticker1,
|
||||
&transaction.unsigned_swap.ticker2,
|
||||
&transaction.unsigned_swap.nft_series1,
|
||||
&transaction.unsigned_swap.nft_series2,
|
||||
&transaction.unsigned_swap.sender1,
|
||||
&transaction.unsigned_swap.sender2,
|
||||
&transaction.unsigned_swap.tip1,
|
||||
&transaction.unsigned_swap.tip2,
|
||||
);
|
||||
let asset1 = nft_asset_name(ticker1, *nft_series1);
|
||||
let asset2 = nft_asset_name(ticker2, *nft_series2);
|
||||
|
||||
// Refund both base-coin fees and remove those fees from the miner balance.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee1,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, sender1, *txfee1, &type_str, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee2,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ = balance_sheet_operation_with_db(db, sender2, *txfee2, &type_str, operand_addition);
|
||||
// Tips are paid in the swapped assets, so they must be reversed per asset.
|
||||
let _ =
|
||||
balance_sheet_operation_with_db(db, mining_receiver, *tip1, &asset1, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, sender1, *tip1, &asset1, operand_addition);
|
||||
let _ =
|
||||
balance_sheet_operation_with_db(db, mining_receiver, *tip2, &asset2, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, sender2, *tip2, &asset2, operand_addition);
|
||||
// Reverse the actual two-sided asset exchange.
|
||||
let _ = balance_sheet_operation_with_db(db, sender1, *value2, &asset2, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, sender2, *value2, &asset2, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(db, sender2, *value1, &asset1, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, sender1, *value1, &asset1, operand_addition);
|
||||
|
||||
// Convert the txid hash back to bytes for tree lookup/removal.
|
||||
let hash = decode(&transaction.unsigned_swap.hash().await).unwrap();
|
||||
|
||||
// Remove the txid lookup for the rolled-back swap.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash.clone();
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
let nft_tree = db.open_tree("nfts").unwrap();
|
||||
// If either side of the swap was an NFT, remove this swap from that asset's
|
||||
// provenance history as well.
|
||||
if nft_tree.contains_key(asset1.as_bytes()).unwrap_or(false) {
|
||||
let _ = remove_nft_history_entry(db, &asset1, &hash);
|
||||
}
|
||||
if nft_tree.contains_key(asset2.as_bytes()).unwrap_or(false) {
|
||||
let _ = remove_nft_history_entry(db, &asset2, &hash);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +1,70 @@
|
|||
use crate::blocks::transfer::TransferTransaction;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::blocks::transfer::TransferTransaction;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::common::nft_assets::nft_asset_name;
|
||||
use crate::decode;
|
||||
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
|
||||
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_transfer_transaction(
|
||||
transaction: TransferTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// reverse the transfer and fee movements, then remove the
|
||||
// transfer transaction from chain state during rollback
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (coin, nft_series, receiver, sender, value, txfee) = (
|
||||
&transaction.unsigned_transfer.coin,
|
||||
&transaction.unsigned_transfer.nft_series,
|
||||
&transaction.unsigned_transfer.receiver,
|
||||
&transaction.unsigned_transfer.sender,
|
||||
&transaction.unsigned_transfer.value,
|
||||
&transaction.unsigned_transfer.txfee,
|
||||
);
|
||||
let transfer_asset = nft_asset_name(coin, *nft_series);
|
||||
|
||||
// Remove the miner fee, return the transferred asset to the sender, and
|
||||
// refund the sender's base-coin fee.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ =
|
||||
balance_sheet_operation_with_db(db, receiver, *value, &transfer_asset, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, sender, *value, &transfer_asset, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(db, sender, *txfee, &type_str, operand_addition);
|
||||
|
||||
let hash = decode(&transaction.unsigned_transfer.hash().await).unwrap();
|
||||
|
||||
// Remove the txid lookup so the rolled-back transfer no longer resolves as
|
||||
// an on-chain transaction.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash.clone();
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
// NFT transfers also write provenance, so remove this transfer from the
|
||||
// asset history if the transferred asset is an NFT.
|
||||
let nft_tree = db.open_tree("nfts").unwrap();
|
||||
if nft_tree
|
||||
.contains_key(transfer_asset.as_bytes())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let _ = remove_nft_history_entry(db, &transfer_asset, &hash);
|
||||
}
|
||||
|
||||
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
|
||||
use crate::sled::Db;
|
||||
|
||||
pub async fn undo_transfer_transaction(
|
||||
transaction: TransferTransaction,
|
||||
mining_receiver: &str,
|
||||
db: &Db,
|
||||
) {
|
||||
// reverse the transfer and fee movements, then remove the
|
||||
// transfer transaction from chain state during rollback
|
||||
let operand_subtraction = "subtraction";
|
||||
let operand_addition = "addition";
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
type_str,
|
||||
_torrentpath,
|
||||
_wallet_path,
|
||||
_blockpath,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let (coin, nft_series, receiver, sender, value, txfee) = (
|
||||
&transaction.unsigned_transfer.coin,
|
||||
&transaction.unsigned_transfer.nft_series,
|
||||
&transaction.unsigned_transfer.receiver,
|
||||
&transaction.unsigned_transfer.sender,
|
||||
&transaction.unsigned_transfer.value,
|
||||
&transaction.unsigned_transfer.txfee,
|
||||
);
|
||||
let transfer_asset = nft_asset_name(coin, *nft_series);
|
||||
|
||||
// Remove the miner fee, return the transferred asset to the sender, and
|
||||
// refund the sender's base-coin fee.
|
||||
let _ = balance_sheet_operation_with_db(
|
||||
db,
|
||||
mining_receiver,
|
||||
*txfee,
|
||||
&type_str,
|
||||
operand_subtraction,
|
||||
);
|
||||
let _ =
|
||||
balance_sheet_operation_with_db(db, receiver, *value, &transfer_asset, operand_subtraction);
|
||||
let _ = balance_sheet_operation_with_db(db, sender, *value, &transfer_asset, operand_addition);
|
||||
let _ = balance_sheet_operation_with_db(db, sender, *txfee, &type_str, operand_addition);
|
||||
|
||||
let hash = decode(&transaction.unsigned_transfer.hash().await).unwrap();
|
||||
|
||||
// Remove the txid lookup so the rolled-back transfer no longer resolves as
|
||||
// an on-chain transaction.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let key = hash.clone();
|
||||
tree.remove(key).unwrap();
|
||||
|
||||
// NFT transfers also write provenance, so remove this transfer from the
|
||||
// asset history if the transferred asset is an NFT.
|
||||
let nft_tree = db.open_tree("nfts").unwrap();
|
||||
if nft_tree
|
||||
.contains_key(transfer_asset.as_bytes())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let _ = remove_nft_history_entry(db, &transfer_asset, &hash);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,11 +46,7 @@ pub async fn get_balance_with_db(
|
|||
// Balance queries accept vanity/registered aliases, but storage always
|
||||
// resolves back to the canonical short address.
|
||||
let canonical_address = resolve_canonical_registered_short_address(db, address)
|
||||
.map_err(|err| {
|
||||
io::Error::other(
|
||||
format!("Wallet registry lookup failed: {err}"),
|
||||
)
|
||||
})?
|
||||
.map_err(|err| io::Error::other(format!("Wallet registry lookup failed: {err}")))?
|
||||
.unwrap_or_else(|| address.to_string());
|
||||
|
||||
get_balance(&canonical_address, coin_type).await
|
||||
|
|
|
|||
|
|
@ -69,9 +69,7 @@ pub fn balance_sheet_operation(
|
|||
let mut file_balance = if file_exists {
|
||||
// Existing balances are stored as a single little-endian u64.
|
||||
file.read_exact(&mut buffer).map_err(|e| {
|
||||
eprintln!(
|
||||
"Error reading file balance_sheet address {address}: {e}"
|
||||
);
|
||||
eprintln!("Error reading file balance_sheet address {address}: {e}");
|
||||
e
|
||||
})?;
|
||||
u64::from_le_bytes(buffer)
|
||||
|
|
@ -142,11 +140,7 @@ pub fn balance_sheet_operation_with_db(
|
|||
// Vanity or alternate registered addresses resolve to the canonical short
|
||||
// address before the filesystem balance is updated.
|
||||
let canonical_address = resolve_canonical_registered_short_address(db, address)
|
||||
.map_err(|err| {
|
||||
io::Error::other(
|
||||
format!("Wallet registry lookup failed: {err}"),
|
||||
)
|
||||
})?
|
||||
.map_err(|err| io::Error::other(format!("Wallet registry lookup failed: {err}")))?
|
||||
.unwrap_or_else(|| address.to_string());
|
||||
|
||||
balance_sheet_operation(&canonical_address, balance, coin_type, operand)
|
||||
|
|
|
|||
|
|
@ -1,101 +1,97 @@
|
|||
use crate::common::binary_conversions::{binary_to_ip, ip_to_binary};
|
||||
use crate::lazy_static;
|
||||
use crate::log::{info, warn};
|
||||
use crate::records::memory::enums::{ClientType, ConnectionType};
|
||||
use crate::records::memory::response_channels::{delete_entry, reserve_entry, Command};
|
||||
use crate::lazy_static;
|
||||
use crate::log::{info, warn};
|
||||
use crate::records::memory::enums::{ClientType, ConnectionType};
|
||||
use crate::records::memory::response_channels::{delete_entry, reserve_entry, Command};
|
||||
use crate::records::memory::structs::{Connection, StoreConnectionParams};
|
||||
use crate::rpc::client::handshake_processing::{bootstrap_peer_discovery, BootstrapParams};
|
||||
use crate::rpc::command_maps::RPC_BLOCK_HEIGHT;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::sleep;
|
||||
use crate::thread_rng;
|
||||
use crate::timeout;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::Arc;
|
||||
use crate::AsyncWriteExt;
|
||||
use crate::AtomicBool;
|
||||
use crate::AtomicOrdering;
|
||||
use crate::Duration;
|
||||
use crate::IteratorRandom;
|
||||
use crate::Mutex;
|
||||
use crate::RwLock;
|
||||
use crate::rpc::command_maps::RPC_BLOCK_HEIGHT;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::sleep;
|
||||
use crate::thread_rng;
|
||||
use crate::timeout;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::Arc;
|
||||
use crate::AsyncWriteExt;
|
||||
use crate::AtomicBool;
|
||||
use crate::AtomicOrdering;
|
||||
use crate::Duration;
|
||||
use crate::IteratorRandom;
|
||||
use crate::Mutex;
|
||||
use crate::RwLock;
|
||||
use crate::TcpStream;
|
||||
|
||||
fn split_ip_port_key(value: &str) -> Option<(String, u16)> {
|
||||
// Connection keys are stored as ip:port strings; IPv6 addresses may arrive
|
||||
// bracketed, so strip brackets before parsing the port.
|
||||
let (ip_part, port_part) = value.rsplit_once(':')?;
|
||||
let ip = ip_part
|
||||
.strip_prefix('[')
|
||||
.and_then(|inner| inner.strip_suffix(']'))
|
||||
.unwrap_or(ip_part)
|
||||
.to_string();
|
||||
let port = port_part.parse::<u16>().ok()?;
|
||||
Some((ip, port))
|
||||
}
|
||||
|
||||
use crate::records::memory::structs::{ConnectionInfo, ConnectionKey};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ReconnectContext {
|
||||
db: Db,
|
||||
wallet_key: String,
|
||||
map: Arc<Mutex<Command>>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref RECONNECT_CONTEXT: Mutex<Option<ReconnectContext>> = Mutex::new(None);
|
||||
static ref RECONNECT_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
fn try_start_reconnect() -> bool {
|
||||
// Only one reconnect path should run at a time, whether it came from
|
||||
// liveness failure or bootstrap recovery.
|
||||
RECONNECT_IN_PROGRESS
|
||||
.compare_exchange(false, true, AtomicOrdering::SeqCst, AtomicOrdering::SeqCst)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn finish_reconnect() {
|
||||
// Release the reconnect gate after the async reconnect attempt finishes.
|
||||
RECONNECT_IN_PROGRESS.store(false, AtomicOrdering::SeqCst);
|
||||
}
|
||||
|
||||
pub async fn set_reconnect_context(
|
||||
db: Db,
|
||||
wallet_key: String,
|
||||
map: Arc<Mutex<Command>>,
|
||||
) {
|
||||
let mut context = RECONNECT_CONTEXT.lock().await;
|
||||
// Store enough state for later liveness checks to reconnect without
|
||||
// needing the original startup stack.
|
||||
*context = Some(ReconnectContext {
|
||||
db,
|
||||
wallet_key,
|
||||
map,
|
||||
});
|
||||
}
|
||||
|
||||
async fn reconnect_dropped_outgoing(excluded_ip: &str) {
|
||||
if !try_start_reconnect() {
|
||||
warn!("[reconnect] replacement attempt already in progress, skipping duplicate request");
|
||||
return;
|
||||
}
|
||||
|
||||
async {
|
||||
// When an outgoing peer disappears, try to replace it with another
|
||||
// active node that is not already connected and is not the failed IP.
|
||||
let context = {
|
||||
let guard = RECONNECT_CONTEXT.lock().await;
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
let Some(context) = context else {
|
||||
warn!("[reconnect] no reconnect context configured");
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
fn split_ip_port_key(value: &str) -> Option<(String, u16)> {
|
||||
// Connection keys are stored as ip:port strings; IPv6 addresses may arrive
|
||||
// bracketed, so strip brackets before parsing the port.
|
||||
let (ip_part, port_part) = value.rsplit_once(':')?;
|
||||
let ip = ip_part
|
||||
.strip_prefix('[')
|
||||
.and_then(|inner| inner.strip_suffix(']'))
|
||||
.unwrap_or(ip_part)
|
||||
.to_string();
|
||||
let port = port_part.parse::<u16>().ok()?;
|
||||
Some((ip, port))
|
||||
}
|
||||
|
||||
use crate::records::memory::structs::{ConnectionInfo, ConnectionKey};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ReconnectContext {
|
||||
db: Db,
|
||||
wallet_key: String,
|
||||
map: Arc<Mutex<Command>>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref RECONNECT_CONTEXT: Mutex<Option<ReconnectContext>> = Mutex::new(None);
|
||||
static ref RECONNECT_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
fn try_start_reconnect() -> bool {
|
||||
// Only one reconnect path should run at a time, whether it came from
|
||||
// liveness failure or bootstrap recovery.
|
||||
RECONNECT_IN_PROGRESS
|
||||
.compare_exchange(false, true, AtomicOrdering::SeqCst, AtomicOrdering::SeqCst)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn finish_reconnect() {
|
||||
// Release the reconnect gate after the async reconnect attempt finishes.
|
||||
RECONNECT_IN_PROGRESS.store(false, AtomicOrdering::SeqCst);
|
||||
}
|
||||
|
||||
pub async fn set_reconnect_context(db: Db, wallet_key: String, map: Arc<Mutex<Command>>) {
|
||||
let mut context = RECONNECT_CONTEXT.lock().await;
|
||||
// Store enough state for later liveness checks to reconnect without
|
||||
// needing the original startup stack.
|
||||
*context = Some(ReconnectContext {
|
||||
db,
|
||||
wallet_key,
|
||||
map,
|
||||
});
|
||||
}
|
||||
|
||||
async fn reconnect_dropped_outgoing(excluded_ip: &str) {
|
||||
if !try_start_reconnect() {
|
||||
warn!("[reconnect] replacement attempt already in progress, skipping duplicate request");
|
||||
return;
|
||||
}
|
||||
|
||||
async {
|
||||
// When an outgoing peer disappears, try to replace it with another
|
||||
// active node that is not already connected and is not the failed IP.
|
||||
let context = {
|
||||
let guard = RECONNECT_CONTEXT.lock().await;
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
let Some(context) = context else {
|
||||
warn!("[reconnect] no reconnect context configured");
|
||||
return;
|
||||
};
|
||||
|
||||
let excluded_ip_bytes = ip_to_binary(excluded_ip);
|
||||
let live_connection = {
|
||||
let guard = CONNECTIONS.read().await;
|
||||
|
|
@ -131,36 +127,36 @@ async fn reconnect_dropped_outgoing(excluded_ip: &str) {
|
|||
if let Err(err) = bootstrap_peer_discovery(bootstrap_params).await {
|
||||
warn!("[reconnect] bootstrap recovery failed: {err}");
|
||||
}
|
||||
}
|
||||
.await;
|
||||
|
||||
finish_reconnect();
|
||||
}
|
||||
|
||||
pub fn spawn_reconnect_bootstrap(params: BootstrapParams) {
|
||||
if !try_start_reconnect() {
|
||||
warn!("[reconnect] bootstrap recovery already in progress, skipping duplicate request");
|
||||
return;
|
||||
}
|
||||
|
||||
// Bootstrap discovery can perform network requests, so it runs detached
|
||||
// from the caller that noticed the connection problem.
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = bootstrap_peer_discovery(params).await {
|
||||
warn!("[reconnect] bootstrap recovery failed: {err}");
|
||||
}
|
||||
finish_reconnect();
|
||||
});
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
// Initialize the in-memory connection manager state.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// Store a live socket in memory along with its role, peer identity,
|
||||
// and session metadata used by the RPC and peer-management paths.
|
||||
}
|
||||
.await;
|
||||
|
||||
finish_reconnect();
|
||||
}
|
||||
|
||||
pub fn spawn_reconnect_bootstrap(params: BootstrapParams) {
|
||||
if !try_start_reconnect() {
|
||||
warn!("[reconnect] bootstrap recovery already in progress, skipping duplicate request");
|
||||
return;
|
||||
}
|
||||
|
||||
// Bootstrap discovery can perform network requests, so it runs detached
|
||||
// from the caller that noticed the connection problem.
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = bootstrap_peer_discovery(params).await {
|
||||
warn!("[reconnect] bootstrap recovery failed: {err}");
|
||||
}
|
||||
finish_reconnect();
|
||||
});
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
// Initialize the in-memory connection manager state.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// Store a live socket in memory along with its role, peer identity,
|
||||
// and session metadata used by the RPC and peer-management paths.
|
||||
pub fn store_connection(&mut self, params: StoreConnectionParams) -> bool {
|
||||
let StoreConnectionParams {
|
||||
connection_type,
|
||||
|
|
@ -172,101 +168,101 @@ impl Connection {
|
|||
command_map,
|
||||
} = params;
|
||||
|
||||
let ip_bytes = ip_to_binary(&ip);
|
||||
let connection_key = ConnectionKey {
|
||||
connection_type: connection_type.as_bytes(),
|
||||
ip: ip_bytes.clone(),
|
||||
port,
|
||||
};
|
||||
|
||||
let connection_key2 = ConnectionKey {
|
||||
connection_type: connection_type.opposite().as_bytes(),
|
||||
ip: ip_bytes.clone(),
|
||||
port,
|
||||
};
|
||||
|
||||
// Miner nodes are identified by IP, not by port. A second node
|
||||
// announcing the same IP is rejected even if it uses another
|
||||
// socket port.
|
||||
if client_type == ClientType::Miner
|
||||
&& self.connection_map.iter().any(|(key, info)| {
|
||||
key.ip == ip_bytes
|
||||
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
|
||||
})
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Non-miner RPC clients still use the full socket key so short
|
||||
// request/response connections do not collide unnecessarily.
|
||||
if self.connection_map.contains_key(&connection_key)
|
||||
|| self.connection_map.contains_key(&connection_key2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let address = Wallet::long_address_to_bytes(wallet_address);
|
||||
if address.len() != Wallet::ADDRESS_BYTES_LENGTH {
|
||||
return false;
|
||||
}
|
||||
let connection_info = ConnectionInfo::new(
|
||||
connection_type.as_bytes(),
|
||||
ip_bytes,
|
||||
port,
|
||||
stream.clone(),
|
||||
client_type.as_bytes(),
|
||||
address,
|
||||
);
|
||||
self.connection_map.insert(connection_key, connection_info);
|
||||
|
||||
if client_type == ClientType::Miner {
|
||||
Connection::client_checkup(stream, connection_type, ip, port, command_map);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// Remove a specific connection entry by direction, IP, and port.
|
||||
pub fn drop_connection(
|
||||
&mut self,
|
||||
connection_type: ConnectionType,
|
||||
ip: String,
|
||||
port: u16,
|
||||
) -> Option<ConnectionInfo> {
|
||||
let ip_bytes = ip_to_binary(&ip);
|
||||
let connection_key = ConnectionKey {
|
||||
connection_type: connection_type.as_bytes(),
|
||||
ip: ip_bytes,
|
||||
port,
|
||||
};
|
||||
let removed = self.connection_map.remove(&connection_key);
|
||||
if let Some(connection_info) = removed.as_ref() {
|
||||
let stream = Arc::clone(&connection_info.stream);
|
||||
tokio::spawn(async move {
|
||||
let mut stream_guard = stream.lock().await;
|
||||
let _ = stream_guard.shutdown().await;
|
||||
});
|
||||
}
|
||||
if let Some(connection_info) = removed.as_ref() {
|
||||
let client_role = ClientType::from_bytes(&connection_info.client_type)
|
||||
.map(|client_type| client_type.as_str())
|
||||
.unwrap_or("unknown");
|
||||
info!(
|
||||
"[connection_manager] connection dropped: role={} direction={} peer={}:{}",
|
||||
client_role,
|
||||
connection_type.as_str(),
|
||||
ip,
|
||||
port
|
||||
);
|
||||
}
|
||||
removed
|
||||
}
|
||||
|
||||
pub fn client_checkup(
|
||||
stream: Arc<Mutex<TcpStream>>,
|
||||
connection_type: ConnectionType,
|
||||
ip: String,
|
||||
port: u16,
|
||||
command_map: Arc<Mutex<Command>>,
|
||||
let ip_bytes = ip_to_binary(&ip);
|
||||
let connection_key = ConnectionKey {
|
||||
connection_type: connection_type.as_bytes(),
|
||||
ip: ip_bytes.clone(),
|
||||
port,
|
||||
};
|
||||
|
||||
let connection_key2 = ConnectionKey {
|
||||
connection_type: connection_type.opposite().as_bytes(),
|
||||
ip: ip_bytes.clone(),
|
||||
port,
|
||||
};
|
||||
|
||||
// Miner nodes are identified by IP, not by port. A second node
|
||||
// announcing the same IP is rejected even if it uses another
|
||||
// socket port.
|
||||
if client_type == ClientType::Miner
|
||||
&& self.connection_map.iter().any(|(key, info)| {
|
||||
key.ip == ip_bytes
|
||||
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
|
||||
})
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Non-miner RPC clients still use the full socket key so short
|
||||
// request/response connections do not collide unnecessarily.
|
||||
if self.connection_map.contains_key(&connection_key)
|
||||
|| self.connection_map.contains_key(&connection_key2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let address = Wallet::long_address_to_bytes(wallet_address);
|
||||
if address.len() != Wallet::ADDRESS_BYTES_LENGTH {
|
||||
return false;
|
||||
}
|
||||
let connection_info = ConnectionInfo::new(
|
||||
connection_type.as_bytes(),
|
||||
ip_bytes,
|
||||
port,
|
||||
stream.clone(),
|
||||
client_type.as_bytes(),
|
||||
address,
|
||||
);
|
||||
self.connection_map.insert(connection_key, connection_info);
|
||||
|
||||
if client_type == ClientType::Miner {
|
||||
Connection::client_checkup(stream, connection_type, ip, port, command_map);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// Remove a specific connection entry by direction, IP, and port.
|
||||
pub fn drop_connection(
|
||||
&mut self,
|
||||
connection_type: ConnectionType,
|
||||
ip: String,
|
||||
port: u16,
|
||||
) -> Option<ConnectionInfo> {
|
||||
let ip_bytes = ip_to_binary(&ip);
|
||||
let connection_key = ConnectionKey {
|
||||
connection_type: connection_type.as_bytes(),
|
||||
ip: ip_bytes,
|
||||
port,
|
||||
};
|
||||
let removed = self.connection_map.remove(&connection_key);
|
||||
if let Some(connection_info) = removed.as_ref() {
|
||||
let stream = Arc::clone(&connection_info.stream);
|
||||
tokio::spawn(async move {
|
||||
let mut stream_guard = stream.lock().await;
|
||||
let _ = stream_guard.shutdown().await;
|
||||
});
|
||||
}
|
||||
if let Some(connection_info) = removed.as_ref() {
|
||||
let client_role = ClientType::from_bytes(&connection_info.client_type)
|
||||
.map(|client_type| client_type.as_str())
|
||||
.unwrap_or("unknown");
|
||||
info!(
|
||||
"[connection_manager] connection dropped: role={} direction={} peer={}:{}",
|
||||
client_role,
|
||||
connection_type.as_str(),
|
||||
ip,
|
||||
port
|
||||
);
|
||||
}
|
||||
removed
|
||||
}
|
||||
|
||||
pub fn client_checkup(
|
||||
stream: Arc<Mutex<TcpStream>>,
|
||||
connection_type: ConnectionType,
|
||||
ip: String,
|
||||
port: u16,
|
||||
command_map: Arc<Mutex<Command>>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
|
|
@ -274,39 +270,39 @@ impl Connection {
|
|||
let still_registered = {
|
||||
let guard = CONNECTIONS.read().await;
|
||||
guard
|
||||
.as_ref()
|
||||
.map(|conn| {
|
||||
let connection_key = ConnectionKey {
|
||||
connection_type: connection_type.as_bytes(),
|
||||
ip: ip_to_binary(&ip),
|
||||
port,
|
||||
};
|
||||
conn.connection_map.contains_key(&connection_key)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
if !still_registered {
|
||||
break;
|
||||
}
|
||||
|
||||
let message_type = RPC_BLOCK_HEIGHT; // Block-height request used as a lightweight checkup ping.
|
||||
let (checkup_key, _checkup_tx, checkup_rx_mutex) =
|
||||
reserve_entry(command_map.clone()).await;
|
||||
|
||||
// Send a lightweight ping message and wait for the reply
|
||||
// routed back through the shared response hashmap.
|
||||
let mut message: Vec<u8> = Vec::with_capacity(4);
|
||||
message.push(message_type);
|
||||
message.extend_from_slice(&checkup_key);
|
||||
|
||||
RpcResponse::send_raw(&stream, None, &message).await;
|
||||
|
||||
let response_result = {
|
||||
let mut checkup_rx = checkup_rx_mutex.lock().await;
|
||||
timeout(Duration::from_secs(30), checkup_rx.recv()).await
|
||||
};
|
||||
|
||||
.as_ref()
|
||||
.map(|conn| {
|
||||
let connection_key = ConnectionKey {
|
||||
connection_type: connection_type.as_bytes(),
|
||||
ip: ip_to_binary(&ip),
|
||||
port,
|
||||
};
|
||||
conn.connection_map.contains_key(&connection_key)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
if !still_registered {
|
||||
break;
|
||||
}
|
||||
|
||||
let message_type = RPC_BLOCK_HEIGHT; // Block-height request used as a lightweight checkup ping.
|
||||
let (checkup_key, _checkup_tx, checkup_rx_mutex) =
|
||||
reserve_entry(command_map.clone()).await;
|
||||
|
||||
// Send a lightweight ping message and wait for the reply
|
||||
// routed back through the shared response hashmap.
|
||||
let mut message: Vec<u8> = Vec::with_capacity(4);
|
||||
message.push(message_type);
|
||||
message.extend_from_slice(&checkup_key);
|
||||
|
||||
RpcResponse::send_raw(&stream, None, &message).await;
|
||||
|
||||
let response_result = {
|
||||
let mut checkup_rx = checkup_rx_mutex.lock().await;
|
||||
timeout(Duration::from_secs(30), checkup_rx.recv()).await
|
||||
};
|
||||
|
||||
match response_result {
|
||||
Ok(Some(_reply)) => {
|
||||
info!(
|
||||
|
|
@ -314,26 +310,26 @@ impl Connection {
|
|||
connection_type.as_str(),
|
||||
ip,
|
||||
port
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
let still_registered = {
|
||||
let guard = CONNECTIONS.read().await;
|
||||
guard
|
||||
.as_ref()
|
||||
.map(|conn| {
|
||||
let connection_key = ConnectionKey {
|
||||
connection_type: connection_type.as_bytes(),
|
||||
ip: ip_to_binary(&ip),
|
||||
port,
|
||||
};
|
||||
conn.connection_map.contains_key(&connection_key)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
if !still_registered {
|
||||
delete_entry(command_map.clone(), checkup_key).await;
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
let still_registered = {
|
||||
let guard = CONNECTIONS.read().await;
|
||||
guard
|
||||
.as_ref()
|
||||
.map(|conn| {
|
||||
let connection_key = ConnectionKey {
|
||||
connection_type: connection_type.as_bytes(),
|
||||
ip: ip_to_binary(&ip),
|
||||
port,
|
||||
};
|
||||
conn.connection_map.contains_key(&connection_key)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
if !still_registered {
|
||||
delete_entry(command_map.clone(), checkup_key).await;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -350,77 +346,75 @@ impl Connection {
|
|||
if let Some(conn) = guard.as_mut() {
|
||||
conn.drop_connection(connection_type, ip.clone(), port);
|
||||
}
|
||||
drop(guard);
|
||||
if connection_type == ConnectionType::Outgoing {
|
||||
reconnect_dropped_outgoing(&ip).await;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Count active incoming peer connections.
|
||||
pub fn count_incoming_connections(&self) -> usize {
|
||||
self.connection_map
|
||||
.values()
|
||||
.filter(|info| {
|
||||
ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Incoming)
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
// Count active outgoing peer connections.
|
||||
pub fn count_outgoing_connections(&self) -> usize {
|
||||
self.connection_map
|
||||
.values()
|
||||
.filter(|info| {
|
||||
ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Outgoing)
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
// Return all live peer streams so broadcast-style paths can fan out
|
||||
// messages without caring whether a peer was incoming or outgoing.
|
||||
pub fn get_all_streams(&self) -> Vec<Arc<Mutex<TcpStream>>> {
|
||||
self.connection_map
|
||||
.values()
|
||||
.filter(|connection_info| {
|
||||
ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner)
|
||||
})
|
||||
.map(|connection_info| Arc::clone(&connection_info.stream))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Return all non-client peer streams so network-wide broadcasts can
|
||||
// reach every reachable chain peer.
|
||||
pub fn get_all_peer_streams(&self) -> Vec<Arc<Mutex<TcpStream>>> {
|
||||
self.connection_map
|
||||
.values()
|
||||
.filter(|connection_info| {
|
||||
ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner)
|
||||
})
|
||||
.map(|connection_info| Arc::clone(&connection_info.stream))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Resolve a stored outgoing node connection back to its live stream.
|
||||
pub fn get_stream_for_outgoing(&self, ip: &str, port: u16) -> Option<Arc<Mutex<TcpStream>>> {
|
||||
let ip_bytes = ip_to_binary(ip);
|
||||
let connection_key = ConnectionKey {
|
||||
connection_type: ConnectionType::Outgoing.as_bytes(),
|
||||
ip: ip_bytes,
|
||||
port,
|
||||
};
|
||||
self.connection_map
|
||||
.get(&connection_key)
|
||||
.filter(|info| {
|
||||
ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
|
||||
})
|
||||
.map(|info| Arc::clone(&info.stream))
|
||||
}
|
||||
|
||||
drop(guard);
|
||||
if connection_type == ConnectionType::Outgoing {
|
||||
reconnect_dropped_outgoing(&ip).await;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Count active incoming peer connections.
|
||||
pub fn count_incoming_connections(&self) -> usize {
|
||||
self.connection_map
|
||||
.values()
|
||||
.filter(|info| {
|
||||
ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Incoming)
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
// Count active outgoing peer connections.
|
||||
pub fn count_outgoing_connections(&self) -> usize {
|
||||
self.connection_map
|
||||
.values()
|
||||
.filter(|info| {
|
||||
ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Outgoing)
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
// Return all live peer streams so broadcast-style paths can fan out
|
||||
// messages without caring whether a peer was incoming or outgoing.
|
||||
pub fn get_all_streams(&self) -> Vec<Arc<Mutex<TcpStream>>> {
|
||||
self.connection_map
|
||||
.values()
|
||||
.filter(|connection_info| {
|
||||
ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner)
|
||||
})
|
||||
.map(|connection_info| Arc::clone(&connection_info.stream))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Return all non-client peer streams so network-wide broadcasts can
|
||||
// reach every reachable chain peer.
|
||||
pub fn get_all_peer_streams(&self) -> Vec<Arc<Mutex<TcpStream>>> {
|
||||
self.connection_map
|
||||
.values()
|
||||
.filter(|connection_info| {
|
||||
ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner)
|
||||
})
|
||||
.map(|connection_info| Arc::clone(&connection_info.stream))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Resolve a stored outgoing node connection back to its live stream.
|
||||
pub fn get_stream_for_outgoing(&self, ip: &str, port: u16) -> Option<Arc<Mutex<TcpStream>>> {
|
||||
let ip_bytes = ip_to_binary(ip);
|
||||
let connection_key = ConnectionKey {
|
||||
connection_type: ConnectionType::Outgoing.as_bytes(),
|
||||
ip: ip_bytes,
|
||||
port,
|
||||
};
|
||||
self.connection_map
|
||||
.get(&connection_key)
|
||||
.filter(|info| ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner))
|
||||
.map(|info| Arc::clone(&info.stream))
|
||||
}
|
||||
|
||||
// Look up a live miner stream by the exact ip:port connection key.
|
||||
// Network-map records only store bare IPs, so they must not be used
|
||||
// to select an arbitrary live socket.
|
||||
|
|
@ -430,16 +424,18 @@ impl Connection {
|
|||
let conn = lock.as_ref()?;
|
||||
|
||||
let ip_bytes = ip_to_binary(&ip);
|
||||
conn.connection_map.iter().find_map(|(connection_key, info)| {
|
||||
if connection_key.ip == ip_bytes
|
||||
&& connection_key.port == port
|
||||
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
|
||||
{
|
||||
Some(Arc::clone(&info.stream))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
conn.connection_map
|
||||
.iter()
|
||||
.find_map(|(connection_key, info)| {
|
||||
if connection_key.ip == ip_bytes
|
||||
&& connection_key.port == port
|
||||
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
|
||||
{
|
||||
Some(Arc::clone(&info.stream))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Build the serialized connection key for a live stream when only
|
||||
|
|
@ -456,101 +452,101 @@ impl Connection {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Find the first stored connection record for the requested IP.
|
||||
pub fn find_connection_info(&self, ip: &str) -> Option<(ConnectionType, u16)> {
|
||||
let ip_bytes = ip_to_binary(ip);
|
||||
|
||||
for (key, _info) in self.connection_map.iter() {
|
||||
if key.ip == ip_bytes {
|
||||
let connection_type = ConnectionType::from_bytes(&key.connection_type)?;
|
||||
return Some((connection_type, key.port));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Find a stored connection by IP, constrained to a specific client role.
|
||||
pub fn find_connection_info_by_client_type(
|
||||
&self,
|
||||
ip: &str,
|
||||
client_type: ClientType,
|
||||
) -> Option<(ConnectionType, u16)> {
|
||||
let ip_bytes = ip_to_binary(ip);
|
||||
let client_type_bytes = client_type.as_bytes();
|
||||
|
||||
for (key, info) in self.connection_map.iter() {
|
||||
if key.ip == ip_bytes && info.client_type == client_type_bytes {
|
||||
let connection_type = ConnectionType::from_bytes(&key.connection_type)?;
|
||||
return Some((connection_type, key.port));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Find the stored outgoing port for a peer IP so reconnect and
|
||||
// cleanup logic can target the correct connection entry.
|
||||
pub fn find_outgoing_port(&self, ip: &str) -> Option<u16> {
|
||||
let ip_bytes = ip_to_binary(ip);
|
||||
self.connection_map
|
||||
.iter()
|
||||
.find(|(key, _)| {
|
||||
key.connection_type == ConnectionType::Outgoing.as_bytes() && key.ip == ip_bytes
|
||||
})
|
||||
.map(|(key, _)| key.port)
|
||||
}
|
||||
|
||||
// Prefer a random incoming node connection, falling back to an
|
||||
// outgoing node connection when no incoming peer is available.
|
||||
pub fn get_random_connection(&self, excluded_key: Option<&str>) -> Option<(Vec<u8>, u16)> {
|
||||
let mut rng = thread_rng();
|
||||
let excluded = excluded_key.and_then(split_ip_port_key);
|
||||
|
||||
if let Some((key, _info)) = self
|
||||
.connection_map
|
||||
.iter()
|
||||
.filter(|(key, info)| {
|
||||
ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Incoming)
|
||||
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
|
||||
&& excluded
|
||||
.as_ref()
|
||||
.map(|(ip, _)| key.ip != ip_to_binary(ip))
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.choose(&mut rng)
|
||||
{
|
||||
return Some((key.ip.clone(), key.port));
|
||||
}
|
||||
|
||||
if let Some((key, _info)) = self
|
||||
.connection_map
|
||||
.iter()
|
||||
.filter(|(key, info)| {
|
||||
ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Outgoing)
|
||||
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
|
||||
&& excluded
|
||||
.as_ref()
|
||||
.map(|(ip, _)| key.ip != ip_to_binary(ip))
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.choose(&mut rng)
|
||||
{
|
||||
return Some((key.ip.clone(), key.port));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CONNECTIONS: Arc<RwLock<Option<Connection>>> = Arc::new(RwLock::new(None));
|
||||
}
|
||||
|
||||
|
||||
// Find the first stored connection record for the requested IP.
|
||||
pub fn find_connection_info(&self, ip: &str) -> Option<(ConnectionType, u16)> {
|
||||
let ip_bytes = ip_to_binary(ip);
|
||||
|
||||
for (key, _info) in self.connection_map.iter() {
|
||||
if key.ip == ip_bytes {
|
||||
let connection_type = ConnectionType::from_bytes(&key.connection_type)?;
|
||||
return Some((connection_type, key.port));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Find a stored connection by IP, constrained to a specific client role.
|
||||
pub fn find_connection_info_by_client_type(
|
||||
&self,
|
||||
ip: &str,
|
||||
client_type: ClientType,
|
||||
) -> Option<(ConnectionType, u16)> {
|
||||
let ip_bytes = ip_to_binary(ip);
|
||||
let client_type_bytes = client_type.as_bytes();
|
||||
|
||||
for (key, info) in self.connection_map.iter() {
|
||||
if key.ip == ip_bytes && info.client_type == client_type_bytes {
|
||||
let connection_type = ConnectionType::from_bytes(&key.connection_type)?;
|
||||
return Some((connection_type, key.port));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Find the stored outgoing port for a peer IP so reconnect and
|
||||
// cleanup logic can target the correct connection entry.
|
||||
pub fn find_outgoing_port(&self, ip: &str) -> Option<u16> {
|
||||
let ip_bytes = ip_to_binary(ip);
|
||||
self.connection_map
|
||||
.iter()
|
||||
.find(|(key, _)| {
|
||||
key.connection_type == ConnectionType::Outgoing.as_bytes() && key.ip == ip_bytes
|
||||
})
|
||||
.map(|(key, _)| key.port)
|
||||
}
|
||||
|
||||
// Prefer a random incoming node connection, falling back to an
|
||||
// outgoing node connection when no incoming peer is available.
|
||||
pub fn get_random_connection(&self, excluded_key: Option<&str>) -> Option<(Vec<u8>, u16)> {
|
||||
let mut rng = thread_rng();
|
||||
let excluded = excluded_key.and_then(split_ip_port_key);
|
||||
|
||||
if let Some((key, _info)) = self
|
||||
.connection_map
|
||||
.iter()
|
||||
.filter(|(key, info)| {
|
||||
ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Incoming)
|
||||
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
|
||||
&& excluded
|
||||
.as_ref()
|
||||
.map(|(ip, _)| key.ip != ip_to_binary(ip))
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.choose(&mut rng)
|
||||
{
|
||||
return Some((key.ip.clone(), key.port));
|
||||
}
|
||||
|
||||
if let Some((key, _info)) = self
|
||||
.connection_map
|
||||
.iter()
|
||||
.filter(|(key, info)| {
|
||||
ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Outgoing)
|
||||
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
|
||||
&& excluded
|
||||
.as_ref()
|
||||
.map(|(ip, _)| key.ip != ip_to_binary(ip))
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.choose(&mut rng)
|
||||
{
|
||||
return Some((key.ip.clone(), key.port));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CONNECTIONS: Arc<RwLock<Option<Connection>>> = Arc::new(RwLock::new(None));
|
||||
}
|
||||
|
||||
pub async fn initialize_connection() {
|
||||
// Lazily create the singleton connection manager the first time the
|
||||
// node starts accepting or opening peer connections.
|
||||
let mut connection_instance = CONNECTIONS.write().await;
|
||||
if connection_instance.is_none() {
|
||||
*connection_instance = Some(Connection::new());
|
||||
// Lazily create the singleton connection manager the first time the
|
||||
// node starts accepting or opening peer connections.
|
||||
let mut connection_instance = CONNECTIONS.write().await;
|
||||
if connection_instance.is_none() {
|
||||
*connection_instance = Some(Connection::new());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -565,19 +561,19 @@ pub async fn outgoing_connection_count() -> usize {
|
|||
}
|
||||
|
||||
pub async fn get_client_type_from_memory(key: &str) -> Option<ClientType> {
|
||||
// Recover the stored client role from the serialized connection key
|
||||
// used throughout the RPC layer.
|
||||
let (ip, port) = split_ip_port_key(key)?;
|
||||
let ip_bytes = ip_to_binary(&ip);
|
||||
|
||||
let guard = CONNECTIONS.read().await;
|
||||
let conn = guard.as_ref()?;
|
||||
|
||||
for (connection_key, info) in conn.connection_map.iter() {
|
||||
if connection_key.ip == ip_bytes && connection_key.port == port {
|
||||
return ClientType::from_bytes(&info.client_type);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
// Recover the stored client role from the serialized connection key
|
||||
// used throughout the RPC layer.
|
||||
let (ip, port) = split_ip_port_key(key)?;
|
||||
let ip_bytes = ip_to_binary(&ip);
|
||||
|
||||
let guard = CONNECTIONS.read().await;
|
||||
let conn = guard.as_ref()?;
|
||||
|
||||
for (connection_key, info) in conn.connection_map.iter() {
|
||||
if connection_key.ip == ip_bytes && connection_key.port == port {
|
||||
return ClientType::from_bytes(&info.client_type);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,89 +1,89 @@
|
|||
use super::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub async fn signature_exists(signature: &str, hash: &str) -> Result<bool> {
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
|
||||
// Check every mempool table because the signature column names differ by
|
||||
// transaction type, especially for two-party swaps and loans.
|
||||
let row = client
|
||||
.query_one(
|
||||
r#"
|
||||
SELECT
|
||||
CASE
|
||||
WHEN EXISTS (SELECT 1 FROM transfer a WHERE a.signature = $1 AND a.hash = $2 AND a.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM token b WHERE b.signature = $1 AND b.hash = $2 AND b.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM issue_token c WHERE c.signature = $1 AND c.hash = $2 AND c.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM burn d WHERE d.signature = $1 AND d.hash = $2 AND d.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM nft e WHERE e.signature = $1 AND e.hash = $2 AND e.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM marketing f WHERE f.signature = $1 AND f.hash = $2 AND f.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM vanity_address va WHERE va.signature = $1 AND va.hash = $2 AND va.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM swap g WHERE g.signature1 = $1 AND g.hash = $2 AND g.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM swap h WHERE h.signature2 = $1 AND h.hash = $2 AND h.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM loan_contract i WHERE i.signature1 = $1 AND i.hash = $2 AND i.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM loan_contract j WHERE j.signature2 = $1 AND j.hash = $2 AND j.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM loan_payment k WHERE k.signature = $1 AND k.hash = $2 AND k.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM collateral_claim l WHERE l.signature = $1 AND l.hash = $2 AND l.processed = false)
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END AS signature_found;
|
||||
"#,
|
||||
&[&signature, &hash],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let found: i32 = row.get(0);
|
||||
Ok(found == 1)
|
||||
}
|
||||
|
||||
.query_one(
|
||||
r#"
|
||||
SELECT
|
||||
CASE
|
||||
WHEN EXISTS (SELECT 1 FROM transfer a WHERE a.signature = $1 AND a.hash = $2 AND a.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM token b WHERE b.signature = $1 AND b.hash = $2 AND b.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM issue_token c WHERE c.signature = $1 AND c.hash = $2 AND c.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM burn d WHERE d.signature = $1 AND d.hash = $2 AND d.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM nft e WHERE e.signature = $1 AND e.hash = $2 AND e.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM marketing f WHERE f.signature = $1 AND f.hash = $2 AND f.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM vanity_address va WHERE va.signature = $1 AND va.hash = $2 AND va.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM swap g WHERE g.signature1 = $1 AND g.hash = $2 AND g.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM swap h WHERE h.signature2 = $1 AND h.hash = $2 AND h.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM loan_contract i WHERE i.signature1 = $1 AND i.hash = $2 AND i.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM loan_contract j WHERE j.signature2 = $1 AND j.hash = $2 AND j.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM loan_payment k WHERE k.signature = $1 AND k.hash = $2 AND k.processed = false)
|
||||
OR EXISTS (SELECT 1 FROM collateral_claim l WHERE l.signature = $1 AND l.hash = $2 AND l.processed = false)
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END AS signature_found;
|
||||
"#,
|
||||
&[&signature, &hash],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let found: i32 = row.get(0);
|
||||
Ok(found == 1)
|
||||
}
|
||||
|
||||
pub async fn transaction_by_signature(signature: &str) -> RpcResponse {
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
|
||||
// Return the original serialized transaction bytes, not a reconstructed
|
||||
// row, so RPC callers receive the same payload that would enter a block.
|
||||
let result = client
|
||||
.query_opt(
|
||||
r#"
|
||||
SELECT original FROM (
|
||||
SELECT original FROM transfer WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM token WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM issue_token WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM burn WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM nft WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM marketing WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM vanity_address WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM swap WHERE signature1 = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM swap WHERE signature2 = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM loan_contract WHERE signature1 = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM loan_contract WHERE signature2 = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM loan_payment WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM collateral_claim WHERE signature = $1 AND processed = false LIMIT 1
|
||||
) AS subquery LIMIT 1
|
||||
"#,
|
||||
&[&signature],
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Some(row)) => {
|
||||
let bytes: Vec<u8> = row.get(0);
|
||||
RpcResponse::Binary(bytes)
|
||||
}
|
||||
_ => RpcResponse::Binary(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
.query_opt(
|
||||
r#"
|
||||
SELECT original FROM (
|
||||
SELECT original FROM transfer WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM token WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM issue_token WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM burn WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM nft WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM marketing WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM vanity_address WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM swap WHERE signature1 = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM swap WHERE signature2 = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM loan_contract WHERE signature1 = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM loan_contract WHERE signature2 = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM loan_payment WHERE signature = $1 AND processed = false LIMIT 1
|
||||
UNION ALL
|
||||
SELECT original FROM collateral_claim WHERE signature = $1 AND processed = false LIMIT 1
|
||||
) AS subquery LIMIT 1
|
||||
"#,
|
||||
&[&signature],
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Some(row)) => {
|
||||
let bytes: Vec<u8> = row.get(0);
|
||||
RpcResponse::Binary(bytes)
|
||||
}
|
||||
_ => RpcResponse::Binary(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn transactions_by_address(db: &Db, address: &str) -> RpcResponse {
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
// Canonicalize vanity aliases before querying pending rows.
|
||||
|
|
@ -92,97 +92,97 @@ pub async fn transactions_by_address(db: &Db, address: &str) -> RpcResponse {
|
|||
// Concatenate original transaction bytes; the RPC/bin caller can split the
|
||||
// stream by transaction type and fixed byte length.
|
||||
let rows = match client
|
||||
.query(
|
||||
r#"
|
||||
SELECT original FROM (
|
||||
SELECT original FROM transfer WHERE receiver = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM token WHERE creator = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM issue_token WHERE creator = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM burn WHERE address = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM nft WHERE creator = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM marketing WHERE advertiser = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM vanity_address WHERE address = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM swap WHERE sender1 = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM swap WHERE sender2 = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM loan_contract WHERE lender = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM loan_contract WHERE borrower = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM loan_payment WHERE address = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM collateral_claim WHERE address = ANY($1) AND processed = false
|
||||
) AS subquery;
|
||||
"#,
|
||||
&[&addresses],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(_) => return RpcResponse::Binary(Vec::new()),
|
||||
};
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
for row in rows {
|
||||
let chunk: Vec<u8> = row.get(0);
|
||||
bytes.extend(chunk);
|
||||
}
|
||||
|
||||
RpcResponse::Binary(bytes)
|
||||
}
|
||||
|
||||
.query(
|
||||
r#"
|
||||
SELECT original FROM (
|
||||
SELECT original FROM transfer WHERE receiver = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM token WHERE creator = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM issue_token WHERE creator = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM burn WHERE address = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM nft WHERE creator = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM marketing WHERE advertiser = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM vanity_address WHERE address = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM swap WHERE sender1 = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM swap WHERE sender2 = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM loan_contract WHERE lender = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM loan_contract WHERE borrower = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM loan_payment WHERE address = ANY($1) AND processed = false
|
||||
UNION ALL
|
||||
SELECT original FROM collateral_claim WHERE address = ANY($1) AND processed = false
|
||||
) AS subquery;
|
||||
"#,
|
||||
&[&addresses],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(_) => return RpcResponse::Binary(Vec::new()),
|
||||
};
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
for row in rows {
|
||||
let chunk: Vec<u8> = row.get(0);
|
||||
bytes.extend(chunk);
|
||||
}
|
||||
|
||||
RpcResponse::Binary(bytes)
|
||||
}
|
||||
|
||||
pub async fn largest_fee() -> RpcResponse {
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
|
||||
// Swaps have two possible fees, so both sides are included in the max.
|
||||
let row = match client
|
||||
.query_one(
|
||||
r#"
|
||||
SELECT MAX(fee) AS largest_txid FROM (
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM transfer
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM token
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM issue_token
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM burn
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM nft
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM marketing
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM vanity_address
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee1) AS BIGINT) AS fee FROM swap
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee2) AS BIGINT) AS fee FROM swap
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_contract
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_payment
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM collateral_claim
|
||||
) AS combined_max_txids;
|
||||
"#,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(_) => return RpcResponse::Binary(0u32.to_le_bytes().to_vec()),
|
||||
};
|
||||
|
||||
let max_fee: Option<i64> = row.get(0);
|
||||
let fee = (max_fee.unwrap_or(0) as u64).to_le_bytes().to_vec();
|
||||
|
||||
.query_one(
|
||||
r#"
|
||||
SELECT MAX(fee) AS largest_txid FROM (
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM transfer
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM token
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM issue_token
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM burn
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM nft
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM marketing
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM vanity_address
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee1) AS BIGINT) AS fee FROM swap
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee2) AS BIGINT) AS fee FROM swap
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_contract
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_payment
|
||||
UNION ALL
|
||||
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM collateral_claim
|
||||
) AS combined_max_txids;
|
||||
"#,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(_) => return RpcResponse::Binary(0u32.to_le_bytes().to_vec()),
|
||||
};
|
||||
|
||||
let max_fee: Option<i64> = row.get(0);
|
||||
let fee = (max_fee.unwrap_or(0) as u64).to_le_bytes().to_vec();
|
||||
|
||||
RpcResponse::Binary(fee)
|
||||
}
|
||||
|
||||
|
|
@ -242,49 +242,49 @@ async fn pending_saved_loan_payment_balance(
|
|||
pub async fn get_coin_balance(
|
||||
db: &Db,
|
||||
address: &str,
|
||||
coin: &str,
|
||||
coin: &str,
|
||||
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
// Pending-balance checks use canonical addresses so vanity and short
|
||||
// address inputs see the same outgoing obligations.
|
||||
let addresses = canonical_mempool_addresses(db, address);
|
||||
let (asset_name, nft_series) = nft_asset_parts(coin);
|
||||
let nft_series = nft_series as i32;
|
||||
|
||||
let nft_series = nft_series as i32;
|
||||
|
||||
let row = client
|
||||
.query_one(
|
||||
r#"
|
||||
SELECT CAST((
|
||||
COALESCE((SELECT SUM(t.value)
|
||||
FROM transfer t
|
||||
WHERE t.sender = ANY($1) AND t.coin = $2 AND t.nft_series = $3 AND t.processed = false), 0)
|
||||
|
||||
+ COALESCE((SELECT SUM(tok.number)
|
||||
FROM token tok
|
||||
WHERE tok.creator = ANY($1) AND tok.ticker = $2 AND tok.processed = false), 0)
|
||||
|
||||
+ COALESCE((SELECT SUM(it.number)
|
||||
FROM issue_token it
|
||||
WHERE it.creator = ANY($1) AND it.ticker = $2 AND it.processed = false), 0)
|
||||
|
||||
SELECT CAST((
|
||||
COALESCE((SELECT SUM(t.value)
|
||||
FROM transfer t
|
||||
WHERE t.sender = ANY($1) AND t.coin = $2 AND t.nft_series = $3 AND t.processed = false), 0)
|
||||
|
||||
+ COALESCE((SELECT SUM(tok.number)
|
||||
FROM token tok
|
||||
WHERE tok.creator = ANY($1) AND tok.ticker = $2 AND tok.processed = false), 0)
|
||||
|
||||
+ COALESCE((SELECT SUM(it.number)
|
||||
FROM issue_token it
|
||||
WHERE it.creator = ANY($1) AND it.ticker = $2 AND it.processed = false), 0)
|
||||
|
||||
+ COALESCE((SELECT SUM(b.value)
|
||||
FROM burn b
|
||||
WHERE b.address = ANY($1) AND b.coin = $2 AND b.nft_series = $3 AND b.processed = false), 0)
|
||||
|
||||
+ COALESCE((SELECT SUM(s.value1)
|
||||
FROM swap s
|
||||
WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.tip1)
|
||||
FROM swap s
|
||||
WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0)
|
||||
|
||||
+ COALESCE((SELECT SUM(s.value2)
|
||||
FROM swap s
|
||||
WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.nft_series2 = $3 AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.tip2)
|
||||
FROM swap s
|
||||
WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.nft_series2 = $3 AND s.processed = false), 0)
|
||||
|
||||
|
||||
+ COALESCE((SELECT SUM(s.value1)
|
||||
FROM swap s
|
||||
WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.tip1)
|
||||
FROM swap s
|
||||
WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0)
|
||||
|
||||
+ COALESCE((SELECT SUM(s.value2)
|
||||
FROM swap s
|
||||
WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.nft_series2 = $3 AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.tip2)
|
||||
FROM swap s
|
||||
WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.nft_series2 = $3 AND s.processed = false), 0)
|
||||
|
||||
+ COALESCE((SELECT SUM(lc.loan_amount)
|
||||
FROM loan_contract lc
|
||||
WHERE lc.lender = ANY($1) AND lc.loan_coin = $2 AND lc.processed = false), 0)
|
||||
|
|
@ -292,25 +292,25 @@ pub async fn get_coin_balance(
|
|||
+ COALESCE((SELECT SUM(lc.collateral_amount)
|
||||
FROM loan_contract lc
|
||||
WHERE lc.borrower = ANY($1) AND lc.collateral = $2 AND lc.processed = false), 0)
|
||||
|
||||
+ COALESCE((
|
||||
SELECT SUM(lp.payback_amount)
|
||||
FROM loan_payment lp
|
||||
JOIN loan_contract lc ON lc.txid = lp.contract_hash
|
||||
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
|
||||
), 0)
|
||||
|
||||
+ COALESCE((
|
||||
SELECT SUM(lp.tip)
|
||||
FROM loan_payment lp
|
||||
JOIN loan_contract lc ON lc.txid = lp.contract_hash
|
||||
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
|
||||
), 0)
|
||||
|
||||
) AS BIGINT) AS total
|
||||
"#,
|
||||
&[&addresses, &asset_name, &nft_series],
|
||||
)
|
||||
|
||||
+ COALESCE((
|
||||
SELECT SUM(lp.payback_amount)
|
||||
FROM loan_payment lp
|
||||
JOIN loan_contract lc ON lc.txid = lp.contract_hash
|
||||
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
|
||||
), 0)
|
||||
|
||||
+ COALESCE((
|
||||
SELECT SUM(lp.tip)
|
||||
FROM loan_payment lp
|
||||
JOIN loan_contract lc ON lc.txid = lp.contract_hash
|
||||
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
|
||||
), 0)
|
||||
|
||||
) AS BIGINT) AS total
|
||||
"#,
|
||||
&[&addresses, &asset_name, &nft_series],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Negative projections are clamped because callers only need the amount
|
||||
|
|
@ -319,10 +319,10 @@ pub async fn get_coin_balance(
|
|||
let chain_loan_payments = pending_saved_loan_payment_balance(db, &addresses, coin).await?;
|
||||
Ok((total.max(0) as u64).saturating_add(chain_loan_payments))
|
||||
}
|
||||
|
||||
pub async fn get_basecoin_balance(
|
||||
db: &Db,
|
||||
address: &str,
|
||||
|
||||
pub async fn get_basecoin_balance(
|
||||
db: &Db,
|
||||
address: &str,
|
||||
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
let addresses = canonical_mempool_addresses(db, address);
|
||||
|
|
@ -330,9 +330,9 @@ pub async fn get_basecoin_balance(
|
|||
// Base coin projection includes direct base transfers plus all fees and
|
||||
// any pending loan/swap movements denominated in the base coin.
|
||||
let row = client
|
||||
.query_one(
|
||||
r#"
|
||||
SELECT CAST((
|
||||
.query_one(
|
||||
r#"
|
||||
SELECT CAST((
|
||||
COALESCE((SELECT SUM(t.value)
|
||||
FROM transfer t
|
||||
WHERE t.sender = ANY($1) AND t.coin = $2 AND t.processed = false), 0)
|
||||
|
|
@ -341,50 +341,49 @@ pub async fn get_basecoin_balance(
|
|||
FROM burn b
|
||||
WHERE b.address = ANY($1) AND b.coin = $2 AND b.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(x.fee) FROM token x WHERE x.creator = ANY($1) AND x.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(it.fee) FROM issue_token it WHERE it.creator = ANY($1) AND it.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(b.fee) FROM burn b WHERE b.address = ANY($1) AND b.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(n.fee) FROM nft n WHERE n.creator = ANY($1) AND n.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(m.fee) FROM marketing m WHERE m.advertiser = ANY($1) AND m.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(v.fee) FROM vanity_address v WHERE v.address = ANY($1) AND v.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(cc.fee) FROM collateral_claim cc WHERE cc.address = ANY($1) AND cc.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(lc.fee) FROM loan_contract lc WHERE lc.lender = ANY($1) AND lc.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(lp.fee) FROM loan_payment lp WHERE lp.address = ANY($1) AND lp.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(it.fee) FROM issue_token it WHERE it.creator = ANY($1) AND it.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(b.fee) FROM burn b WHERE b.address = ANY($1) AND b.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(n.fee) FROM nft n WHERE n.creator = ANY($1) AND n.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(m.fee) FROM marketing m WHERE m.advertiser = ANY($1) AND m.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(v.fee) FROM vanity_address v WHERE v.address = ANY($1) AND v.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(cc.fee) FROM collateral_claim cc WHERE cc.address = ANY($1) AND cc.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(lc.fee) FROM loan_contract lc WHERE lc.lender = ANY($1) AND lc.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(lp.fee) FROM loan_payment lp WHERE lp.address = ANY($1) AND lp.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(lc.loan_amount)
|
||||
FROM loan_contract lc
|
||||
WHERE lc.lender = ANY($1) AND lc.loan_coin = $2 AND lc.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(lc.collateral_amount)
|
||||
FROM loan_contract lc
|
||||
WHERE lc.borrower = ANY($1) AND lc.collateral = $2 AND lc.processed = false), 0)
|
||||
+ COALESCE((
|
||||
SELECT SUM(lp.payback_amount)
|
||||
FROM loan_payment lp
|
||||
JOIN loan_contract lc ON lc.txid = lp.contract_hash
|
||||
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
|
||||
), 0)
|
||||
+ COALESCE((
|
||||
SELECT SUM(lp.tip)
|
||||
FROM loan_payment lp
|
||||
JOIN loan_contract lc ON lc.txid = lp.contract_hash
|
||||
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
|
||||
), 0)
|
||||
+ COALESCE((SELECT SUM(s.fee1) FROM swap s WHERE s.sender1 = ANY($1) AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.fee2) FROM swap s WHERE s.sender2 = ANY($1) AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.value1) FROM swap s WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.tip1) FROM swap s WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.value2) FROM swap s WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.tip2) FROM swap s WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.processed = false), 0)
|
||||
) AS BIGINT) AS total
|
||||
"#,
|
||||
&[&addresses, &*BASECOIN],
|
||||
)
|
||||
.await?;
|
||||
|
||||
+ COALESCE((
|
||||
SELECT SUM(lp.payback_amount)
|
||||
FROM loan_payment lp
|
||||
JOIN loan_contract lc ON lc.txid = lp.contract_hash
|
||||
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
|
||||
), 0)
|
||||
+ COALESCE((
|
||||
SELECT SUM(lp.tip)
|
||||
FROM loan_payment lp
|
||||
JOIN loan_contract lc ON lc.txid = lp.contract_hash
|
||||
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
|
||||
), 0)
|
||||
+ COALESCE((SELECT SUM(s.fee1) FROM swap s WHERE s.sender1 = ANY($1) AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.fee2) FROM swap s WHERE s.sender2 = ANY($1) AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.value1) FROM swap s WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.tip1) FROM swap s WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.value2) FROM swap s WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.processed = false), 0)
|
||||
+ COALESCE((SELECT SUM(s.tip2) FROM swap s WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.processed = false), 0)
|
||||
) AS BIGINT) AS total
|
||||
"#,
|
||||
&[&addresses, &*BASECOIN],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let total: i64 = row.get(0);
|
||||
let chain_loan_payments =
|
||||
pending_saved_loan_payment_balance(db, &addresses, &BASECOIN).await?;
|
||||
let chain_loan_payments = pending_saved_loan_payment_balance(db, &addresses, &BASECOIN).await?;
|
||||
Ok((total.max(0) as u64).saturating_add(chain_loan_payments))
|
||||
}
|
||||
|
||||
|
||||
pub async fn get_pending_payments_for_contract(
|
||||
contract_hash: &str,
|
||||
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
|
||||
|
|
@ -393,63 +392,62 @@ pub async fn get_pending_payments_for_contract(
|
|||
// Loan verification uses this to prevent pending payments from exceeding
|
||||
// what the contract still owes.
|
||||
let row = client
|
||||
.query_one(
|
||||
r#"
|
||||
SELECT CAST(COALESCE(SUM(lp.payback_amount), 0) AS BIGINT) AS total
|
||||
FROM loan_payment lp
|
||||
WHERE lp.contract_hash = $1
|
||||
AND lp.processed = false
|
||||
"#,
|
||||
&[&contract_hash],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let total: i64 = row.get(0);
|
||||
Ok(total.max(0) as u64)
|
||||
}
|
||||
|
||||
.query_one(
|
||||
r#"
|
||||
SELECT CAST(COALESCE(SUM(lp.payback_amount), 0) AS BIGINT) AS total
|
||||
FROM loan_payment lp
|
||||
WHERE lp.contract_hash = $1
|
||||
AND lp.processed = false
|
||||
"#,
|
||||
&[&contract_hash],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let total: i64 = row.get(0);
|
||||
Ok(total.max(0) as u64)
|
||||
}
|
||||
|
||||
pub async fn total_transactions() -> RpcResponse {
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
// Count rows across all mempool tables, including processed rows that may
|
||||
// still be retained briefly for orphan rollback.
|
||||
let row = match client
|
||||
.query_one(
|
||||
r#"
|
||||
SELECT CAST(SUM(row_count) AS BIGINT) AS total_rows FROM (
|
||||
SELECT COUNT(*) AS row_count FROM transfer
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM token
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM issue_token
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM burn
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM nft
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM marketing
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM vanity_address
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM swap
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM loan_contract
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM loan_payment
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM collateral_claim
|
||||
) AS combined;
|
||||
"#,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(_) => return RpcResponse::Binary(vec![0; 8]),
|
||||
};
|
||||
|
||||
let total: Option<i64> = row.get(0);
|
||||
let result = (total.unwrap_or(0) as u32).to_le_bytes().to_vec();
|
||||
|
||||
RpcResponse::Binary(result)
|
||||
}
|
||||
|
||||
.query_one(
|
||||
r#"
|
||||
SELECT CAST(SUM(row_count) AS BIGINT) AS total_rows FROM (
|
||||
SELECT COUNT(*) AS row_count FROM transfer
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM token
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM issue_token
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM burn
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM nft
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM marketing
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM vanity_address
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM swap
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM loan_contract
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM loan_payment
|
||||
UNION ALL
|
||||
SELECT COUNT(*) AS row_count FROM collateral_claim
|
||||
) AS combined;
|
||||
"#,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(_) => return RpcResponse::Binary(vec![0; 8]),
|
||||
};
|
||||
|
||||
let total: Option<i64> = row.get(0);
|
||||
let result = (total.unwrap_or(0) as u32).to_le_bytes().to_vec();
|
||||
|
||||
RpcResponse::Binary(result)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,330 +1,326 @@
|
|||
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::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::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,
|
||||
};
|
||||
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::{
|
||||
mark_processed_by_signatures, mark_selected_transactions_processed,
|
||||
delete_by_signatures, mark_processed_by_signatures, mark_selected_transactions_processed,
|
||||
restore_processed_by_signatures, restore_selected_transactions_processed,
|
||||
spawn_processed_cleanup, delete_by_signatures,
|
||||
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> {
|
||||
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 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()
|
||||
}
|
||||
|
||||
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(())
|
||||
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<()> {
|
||||
|
|
@ -332,75 +328,76 @@ async fn unmark_rows_by_ids(client: &Client, table: &str, ids: &[i64]) -> Result
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let statement =
|
||||
format!("UPDATE {table} SET processed=false, processed_block_number=NULL WHERE id = ANY($1)");
|
||||
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?)
|
||||
}
|
||||
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?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,57 +1,57 @@
|
|||
use super::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub async fn mark_selected_transactions_processed(
|
||||
batch: &SelectedMempoolBatch,
|
||||
block_number: u32,
|
||||
) -> Result<()> {
|
||||
// Mark each selected mempool row as processed under the saved block
|
||||
// number so it can be cleaned up or restored later if needed.
|
||||
// Mark each selected mempool row as processed under the saved block
|
||||
// number so it can be cleaned up or restored later if needed.
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
let bn = block_number as i32;
|
||||
|
||||
// Selected batches are grouped by table, then marked with one UPDATE per
|
||||
// table instead of touching rows one at a time.
|
||||
mark_rows_by_ids(client, "transfer", &ids_for_table(batch, "transfer"), bn).await?;
|
||||
mark_rows_by_ids(client, "token", &ids_for_table(batch, "token"), bn).await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
"issue_token",
|
||||
&ids_for_table(batch, "issue_token"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
mark_rows_by_ids(client, "burn", &ids_for_table(batch, "burn"), bn).await?;
|
||||
mark_rows_by_ids(client, "nft", &ids_for_table(batch, "nft"), bn).await?;
|
||||
mark_rows_by_ids(client, "marketing", &ids_for_table(batch, "marketing"), bn).await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
"vanity_address",
|
||||
&ids_for_table(batch, "vanity_address"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
mark_rows_by_ids(client, "swap", &ids_for_table(batch, "swap"), bn).await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
"loan_contract",
|
||||
&ids_for_table(batch, "loan_contract"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
"loan_payment",
|
||||
&ids_for_table(batch, "loan_payment"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
"collateral_claim",
|
||||
&ids_for_table(batch, "collateral_claim"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
mark_rows_by_ids(client, "token", &ids_for_table(batch, "token"), bn).await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
"issue_token",
|
||||
&ids_for_table(batch, "issue_token"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
mark_rows_by_ids(client, "burn", &ids_for_table(batch, "burn"), bn).await?;
|
||||
mark_rows_by_ids(client, "nft", &ids_for_table(batch, "nft"), bn).await?;
|
||||
mark_rows_by_ids(client, "marketing", &ids_for_table(batch, "marketing"), bn).await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
"vanity_address",
|
||||
&ids_for_table(batch, "vanity_address"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
mark_rows_by_ids(client, "swap", &ids_for_table(batch, "swap"), bn).await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
"loan_contract",
|
||||
&ids_for_table(batch, "loan_contract"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
"loan_payment",
|
||||
&ids_for_table(batch, "loan_payment"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
"collateral_claim",
|
||||
&ids_for_table(batch, "collateral_claim"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -63,12 +63,7 @@ pub async fn restore_selected_transactions_processed(batch: &SelectedMempoolBatc
|
|||
|
||||
unmark_rows_by_ids(client, "transfer", &ids_for_table(batch, "transfer")).await?;
|
||||
unmark_rows_by_ids(client, "token", &ids_for_table(batch, "token")).await?;
|
||||
unmark_rows_by_ids(
|
||||
client,
|
||||
"issue_token",
|
||||
&ids_for_table(batch, "issue_token"),
|
||||
)
|
||||
.await?;
|
||||
unmark_rows_by_ids(client, "issue_token", &ids_for_table(batch, "issue_token")).await?;
|
||||
unmark_rows_by_ids(client, "burn", &ids_for_table(batch, "burn")).await?;
|
||||
unmark_rows_by_ids(client, "nft", &ids_for_table(batch, "nft")).await?;
|
||||
unmark_rows_by_ids(client, "marketing", &ids_for_table(batch, "marketing")).await?;
|
||||
|
|
@ -102,233 +97,231 @@ pub async fn restore_selected_transactions_processed(batch: &SelectedMempoolBatc
|
|||
}
|
||||
|
||||
pub async fn restore_processed_by_signatures(signatures: &[String]) -> Result<bool> {
|
||||
// Orphan correction can revive recently processed mempool rows by
|
||||
// signature when a saved block is rolled back out of the chain.
|
||||
if signatures.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Orphan correction can revive recently processed mempool rows by
|
||||
// signature when a saved block is rolled back out of the chain.
|
||||
if signatures.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
let mut restored = 0_u64;
|
||||
|
||||
// Each table keeps its own signature columns, so rollback unmarks every
|
||||
// column that could contain one of the rolled-back signatures.
|
||||
restored += unmark_by_signatures(client, "transfer", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "token", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "issue_token", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "burn", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "nft", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "marketing", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "vanity_address", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "swap", "signature1", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "swap", "signature2", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "loan_contract", "signature1", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "loan_contract", "signature2", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "loan_payment", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "collateral_claim", "signature", signatures).await?;
|
||||
|
||||
Ok(restored > 0)
|
||||
}
|
||||
|
||||
pub fn spawn_processed_cleanup(saved_block_number: u32) {
|
||||
// Cleanup trails the chain tip by a small depth so recent processed
|
||||
// mempool rows can still be restored during short orphan events.
|
||||
if saved_block_number <= CLEANUP_DEPTH {
|
||||
return;
|
||||
}
|
||||
|
||||
if CLEANUP_RUNNING
|
||||
.compare_exchange(
|
||||
false,
|
||||
true,
|
||||
crate::AtomicOrdering::SeqCst,
|
||||
crate::AtomicOrdering::SeqCst,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
restored += unmark_by_signatures(client, "token", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "issue_token", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "burn", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "nft", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "marketing", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "vanity_address", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "swap", "signature1", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "swap", "signature2", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "loan_contract", "signature1", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "loan_contract", "signature2", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "loan_payment", "signature", signatures).await?;
|
||||
restored += unmark_by_signatures(client, "collateral_claim", "signature", signatures).await?;
|
||||
|
||||
Ok(restored > 0)
|
||||
}
|
||||
|
||||
pub fn spawn_processed_cleanup(saved_block_number: u32) {
|
||||
// Cleanup trails the chain tip by a small depth so recent processed
|
||||
// mempool rows can still be restored during short orphan events.
|
||||
if saved_block_number <= CLEANUP_DEPTH {
|
||||
return;
|
||||
}
|
||||
|
||||
if CLEANUP_RUNNING
|
||||
.compare_exchange(
|
||||
false,
|
||||
true,
|
||||
crate::AtomicOrdering::SeqCst,
|
||||
crate::AtomicOrdering::SeqCst,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
task::spawn(async move {
|
||||
let safe_block = saved_block_number.saturating_sub(CLEANUP_DEPTH);
|
||||
// Cleanup is deliberately delayed behind the tip so short reorgs can
|
||||
// still restore recently processed rows.
|
||||
if let Err(err) = delete_processed_before_or_at(safe_block, CLEANUP_BATCH_LIMIT).await {
|
||||
eprintln!(
|
||||
"[mempool_cleanup] failed: saved_block={saved_block_number} safe_block={safe_block} err={err}"
|
||||
);
|
||||
}
|
||||
CLEANUP_RUNNING.store(false, crate::AtomicOrdering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
pub async fn mark_processed_by_signatures(signatures: &[String], block_number: u32) -> Result<()> {
|
||||
// Synced blocks arrive with signatures instead of selected-row IDs,
|
||||
// so processed marking on the updating path works by signature.
|
||||
if signatures.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[mempool_cleanup] failed: saved_block={saved_block_number} safe_block={safe_block} err={err}"
|
||||
);
|
||||
}
|
||||
CLEANUP_RUNNING.store(false, crate::AtomicOrdering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn mark_processed_by_signatures(signatures: &[String], block_number: u32) -> Result<()> {
|
||||
// Synced blocks arrive with signatures instead of selected-row IDs,
|
||||
// so processed marking on the updating path works by signature.
|
||||
if signatures.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
let bn = block_number as i32;
|
||||
|
||||
// Remote/synced blocks do not know local row IDs, so they mark by
|
||||
// transaction signatures instead.
|
||||
client
|
||||
.execute(
|
||||
"UPDATE transfer SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE issue_token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE burn SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE nft SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE marketing SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE vanity_address SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE loan_payment SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE collateral_claim SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_by_signatures(signatures: &[String]) -> Result<()> {
|
||||
// Some validation failures need to remove mempool rows directly by
|
||||
// signature regardless of which table the transaction lives in.
|
||||
if signatures.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
.execute(
|
||||
"UPDATE transfer SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE issue_token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE burn SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE nft SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE marketing SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE vanity_address SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE loan_payment SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"UPDATE collateral_claim SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
|
||||
&[&bn, &signatures],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_by_signatures(signatures: &[String]) -> Result<()> {
|
||||
// Some validation failures need to remove mempool rows directly by
|
||||
// signature regardless of which table the transaction lives in.
|
||||
if signatures.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
|
||||
// Failed validation removes every matching pending row no matter which
|
||||
// transaction table currently owns the signature.
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM transfer WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM token WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM issue_token WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute("DELETE FROM burn WHERE signature = ANY($1)", &[&signatures])
|
||||
.await?;
|
||||
client
|
||||
.execute("DELETE FROM nft WHERE signature = ANY($1)", &[&signatures])
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM marketing WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM vanity_address WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM swap WHERE signature1 = ANY($1) OR signature2 = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM loan_contract WHERE signature1 = ANY($1) OR signature2 = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM loan_payment WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM collateral_claim WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
.execute(
|
||||
"DELETE FROM transfer WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM token WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM issue_token WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute("DELETE FROM burn WHERE signature = ANY($1)", &[&signatures])
|
||||
.await?;
|
||||
client
|
||||
.execute("DELETE FROM nft WHERE signature = ANY($1)", &[&signatures])
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM marketing WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM vanity_address WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM swap WHERE signature1 = ANY($1) OR signature2 = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM loan_contract WHERE signature1 = ANY($1) OR signature2 = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM loan_payment WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM collateral_claim WHERE signature = ANY($1)",
|
||||
&[&signatures],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
use super::*;
|
||||
|
||||
pub async fn init_db() -> Result<()> {
|
||||
// Initialize the shared Postgres client used by the mempool tables.
|
||||
if DB.get().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let password = SETTINGS
|
||||
.pg_password
|
||||
.as_deref()
|
||||
.expect("Postgres password must be set in settings.ini");
|
||||
|
||||
let conn_str = format!(
|
||||
"host={} port={} user={} password={} dbname={}",
|
||||
SETTINGS.pg_host, SETTINGS.pg_port, SETTINGS.pg_user, password, SETTINGS.pg_dbname
|
||||
);
|
||||
|
||||
use super::*;
|
||||
|
||||
pub async fn init_db() -> Result<()> {
|
||||
// Initialize the shared Postgres client used by the mempool tables.
|
||||
if DB.get().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let password = SETTINGS
|
||||
.pg_password
|
||||
.as_deref()
|
||||
.expect("Postgres password must be set in settings.ini");
|
||||
|
||||
let conn_str = format!(
|
||||
"host={} port={} user={} password={} dbname={}",
|
||||
SETTINGS.pg_host, SETTINGS.pg_port, SETTINGS.pg_user, password, SETTINGS.pg_dbname
|
||||
);
|
||||
|
||||
let (client, connection) = tokio_postgres::connect(&conn_str, NoTls)
|
||||
.await
|
||||
.map_err(|err| anyhow!("Failed to connect to Postgres: {err}"))?;
|
||||
|
|
@ -23,228 +23,228 @@ pub async fn init_db() -> Result<()> {
|
|||
// Keep the Postgres connection driver alive in the background for the
|
||||
// lifetime of the shared client.
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Postgres connection error: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
DB.set(client)
|
||||
.map_err(|_| anyhow!("DB already initialized"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn setup_mempool() -> Result<()> {
|
||||
// Create or migrate the mempool schema, deduplicate any stale rows,
|
||||
// add the selection indexes, and start from an empty live mempool.
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Postgres connection error: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
DB.set(client)
|
||||
.map_err(|_| anyhow!("DB already initialized"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn setup_mempool() -> Result<()> {
|
||||
// Create or migrate the mempool schema, deduplicate any stale rows,
|
||||
// add the selection indexes, and start from an empty live mempool.
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
|
||||
let schema = r#"
|
||||
CREATE TABLE IF NOT EXISTS transfer (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
sender TEXT,
|
||||
value BIGINT,
|
||||
coin VARCHAR(15),
|
||||
nft_series INTEGER NOT NULL DEFAULT 0,
|
||||
receiver TEXT NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS token (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
creator TEXT NOT NULL,
|
||||
number BIGINT NOT NULL,
|
||||
hard_limit SMALLINT NOT NULL DEFAULT 1,
|
||||
ticker VARCHAR(15) NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS issue_token (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
creator TEXT NOT NULL,
|
||||
number BIGINT NOT NULL,
|
||||
ticker VARCHAR(15) NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS burn (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
coin VARCHAR(15) NOT NULL,
|
||||
nft_series INTEGER NOT NULL DEFAULT 0,
|
||||
value BIGINT NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS nft (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fee BIGINT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
creator TEXT NOT NULL,
|
||||
nft_name VARCHAR(15),
|
||||
series SMALLINT NOT NULL,
|
||||
count BIGINT NOT NULL DEFAULT 1,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS marketing (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
advertiser TEXT NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vanity_address (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
vanity_address TEXT NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS swap (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fee1 BIGINT NOT NULL,
|
||||
fee2 BIGINT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
ticker1 VARCHAR(15),
|
||||
nft_series1 INTEGER NOT NULL DEFAULT 0,
|
||||
ticker2 VARCHAR(15),
|
||||
nft_series2 INTEGER NOT NULL DEFAULT 0,
|
||||
value1 BIGINT NOT NULL,
|
||||
value2 BIGINT NOT NULL,
|
||||
sender1 TEXT NOT NULL,
|
||||
tip1 BIGINT NOT NULL,
|
||||
tip2 BIGINT NOT NULL,
|
||||
sender2 TEXT NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature1 TEXT NOT NULL,
|
||||
signature2 TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS loan_contract (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fee BIGINT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
loan_coin VARCHAR(15),
|
||||
loan_amount BIGINT NOT NULL,
|
||||
lender TEXT NOT NULL,
|
||||
collateral VARCHAR(15),
|
||||
collateral_amount BIGINT NOT NULL,
|
||||
borrower TEXT NOT NULL,
|
||||
txid VARCHAR(64) NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature1 TEXT NOT NULL,
|
||||
signature2 TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS loan_payment (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fee BIGINT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
payback_amount BIGINT NOT NULL,
|
||||
contract_hash VARCHAR(64) NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
tip BIGINT NOT NULL,
|
||||
txid VARCHAR(64),
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collateral_claim (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
contract_hash VARCHAR(64) NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE loan_payment ADD COLUMN IF NOT EXISTS txid VARCHAR(64);
|
||||
CREATE TABLE IF NOT EXISTS transfer (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
sender TEXT,
|
||||
value BIGINT,
|
||||
coin VARCHAR(15),
|
||||
nft_series INTEGER NOT NULL DEFAULT 0,
|
||||
receiver TEXT NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS token (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
creator TEXT NOT NULL,
|
||||
number BIGINT NOT NULL,
|
||||
hard_limit SMALLINT NOT NULL DEFAULT 1,
|
||||
ticker VARCHAR(15) NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS issue_token (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
creator TEXT NOT NULL,
|
||||
number BIGINT NOT NULL,
|
||||
ticker VARCHAR(15) NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS burn (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
coin VARCHAR(15) NOT NULL,
|
||||
nft_series INTEGER NOT NULL DEFAULT 0,
|
||||
value BIGINT NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS nft (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fee BIGINT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
creator TEXT NOT NULL,
|
||||
nft_name VARCHAR(15),
|
||||
series SMALLINT NOT NULL,
|
||||
count BIGINT NOT NULL DEFAULT 1,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS marketing (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
advertiser TEXT NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vanity_address (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
vanity_address TEXT NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS swap (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fee1 BIGINT NOT NULL,
|
||||
fee2 BIGINT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
ticker1 VARCHAR(15),
|
||||
nft_series1 INTEGER NOT NULL DEFAULT 0,
|
||||
ticker2 VARCHAR(15),
|
||||
nft_series2 INTEGER NOT NULL DEFAULT 0,
|
||||
value1 BIGINT NOT NULL,
|
||||
value2 BIGINT NOT NULL,
|
||||
sender1 TEXT NOT NULL,
|
||||
tip1 BIGINT NOT NULL,
|
||||
tip2 BIGINT NOT NULL,
|
||||
sender2 TEXT NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature1 TEXT NOT NULL,
|
||||
signature2 TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS loan_contract (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fee BIGINT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
loan_coin VARCHAR(15),
|
||||
loan_amount BIGINT NOT NULL,
|
||||
lender TEXT NOT NULL,
|
||||
collateral VARCHAR(15),
|
||||
collateral_amount BIGINT NOT NULL,
|
||||
borrower TEXT NOT NULL,
|
||||
txid VARCHAR(64) NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature1 TEXT NOT NULL,
|
||||
signature2 TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS loan_payment (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fee BIGINT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
payback_amount BIGINT NOT NULL,
|
||||
contract_hash VARCHAR(64) NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
tip BIGINT NOT NULL,
|
||||
txid VARCHAR(64),
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collateral_claim (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
fee BIGINT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
contract_hash VARCHAR(64) NOT NULL,
|
||||
hash VARCHAR(64) NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
processed bool DEFAULT false,
|
||||
processed_block_number INTEGER DEFAULT NULL,
|
||||
original BYTEA NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE loan_payment ADD COLUMN IF NOT EXISTS txid VARCHAR(64);
|
||||
ALTER TABLE transfer ADD COLUMN IF NOT EXISTS nft_series INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE nft ADD COLUMN IF NOT EXISTS count BIGINT NOT NULL DEFAULT 1;
|
||||
ALTER TABLE token ADD COLUMN IF NOT EXISTS hard_limit SMALLINT NOT NULL DEFAULT 1;
|
||||
ALTER TABLE swap ADD COLUMN IF NOT EXISTS nft_series1 INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE swap ADD COLUMN IF NOT EXISTS nft_series2 INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE transfer ALTER COLUMN sender TYPE TEXT;
|
||||
ALTER TABLE transfer ALTER COLUMN receiver TYPE TEXT;
|
||||
ALTER TABLE transfer ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE token ALTER COLUMN creator TYPE TEXT;
|
||||
ALTER TABLE token ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE issue_token ALTER COLUMN creator TYPE TEXT;
|
||||
ALTER TABLE issue_token ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE burn ALTER COLUMN address TYPE TEXT;
|
||||
ALTER TABLE burn ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE nft ALTER COLUMN creator TYPE TEXT;
|
||||
ALTER TABLE nft ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE marketing ALTER COLUMN advertiser TYPE TEXT;
|
||||
ALTER TABLE marketing ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE vanity_address ALTER COLUMN address TYPE TEXT;
|
||||
ALTER TABLE vanity_address ALTER COLUMN vanity_address TYPE TEXT;
|
||||
ALTER TABLE vanity_address ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE swap ALTER COLUMN sender1 TYPE TEXT;
|
||||
ALTER TABLE swap ALTER COLUMN sender2 TYPE TEXT;
|
||||
ALTER TABLE swap ALTER COLUMN signature1 TYPE TEXT;
|
||||
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 signature1 TYPE TEXT;
|
||||
ALTER TABLE loan_contract ALTER COLUMN signature2 TYPE TEXT;
|
||||
ALTER TABLE loan_payment ALTER COLUMN address TYPE TEXT;
|
||||
ALTER TABLE loan_payment ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE collateral_claim ALTER COLUMN address TYPE TEXT;
|
||||
ALTER TABLE collateral_claim ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE swap ADD COLUMN IF NOT EXISTS nft_series2 INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE transfer ALTER COLUMN sender TYPE TEXT;
|
||||
ALTER TABLE transfer ALTER COLUMN receiver TYPE TEXT;
|
||||
ALTER TABLE transfer ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE token ALTER COLUMN creator TYPE TEXT;
|
||||
ALTER TABLE token ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE issue_token ALTER COLUMN creator TYPE TEXT;
|
||||
ALTER TABLE issue_token ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE burn ALTER COLUMN address TYPE TEXT;
|
||||
ALTER TABLE burn ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE nft ALTER COLUMN creator TYPE TEXT;
|
||||
ALTER TABLE nft ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE marketing ALTER COLUMN advertiser TYPE TEXT;
|
||||
ALTER TABLE marketing ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE vanity_address ALTER COLUMN address TYPE TEXT;
|
||||
ALTER TABLE vanity_address ALTER COLUMN vanity_address TYPE TEXT;
|
||||
ALTER TABLE vanity_address ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE swap ALTER COLUMN sender1 TYPE TEXT;
|
||||
ALTER TABLE swap ALTER COLUMN sender2 TYPE TEXT;
|
||||
ALTER TABLE swap ALTER COLUMN signature1 TYPE TEXT;
|
||||
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 signature1 TYPE TEXT;
|
||||
ALTER TABLE loan_contract ALTER COLUMN signature2 TYPE TEXT;
|
||||
ALTER TABLE loan_payment ALTER COLUMN address TYPE TEXT;
|
||||
ALTER TABLE loan_payment ALTER COLUMN signature TYPE TEXT;
|
||||
ALTER TABLE collateral_claim ALTER COLUMN address TYPE TEXT;
|
||||
ALTER TABLE collateral_claim ALTER COLUMN signature TYPE TEXT;
|
||||
"#;
|
||||
|
||||
// The schema block creates fresh installs and also carries small migrations
|
||||
|
|
@ -252,57 +252,57 @@ pub async fn setup_mempool() -> Result<()> {
|
|||
client.batch_execute(schema).await?;
|
||||
|
||||
let dedupe = r#"
|
||||
DELETE FROM transfer a
|
||||
USING transfer b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM token a
|
||||
USING token b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM issue_token a
|
||||
USING issue_token b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM burn a
|
||||
USING burn b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM nft a
|
||||
USING nft b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM marketing a
|
||||
USING marketing b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM vanity_address a
|
||||
USING vanity_address b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM swap a
|
||||
USING swap b
|
||||
WHERE a.id > b.id AND a.signature1 = b.signature1;
|
||||
|
||||
DELETE FROM swap a
|
||||
USING swap b
|
||||
WHERE a.id > b.id AND a.signature2 = b.signature2;
|
||||
|
||||
DELETE FROM loan_contract a
|
||||
USING loan_contract b
|
||||
WHERE a.id > b.id AND a.signature1 = b.signature1;
|
||||
|
||||
DELETE FROM loan_contract a
|
||||
USING loan_contract b
|
||||
WHERE a.id > b.id AND a.signature2 = b.signature2;
|
||||
|
||||
DELETE FROM loan_payment a
|
||||
USING loan_payment b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM collateral_claim a
|
||||
USING collateral_claim b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
DELETE FROM transfer a
|
||||
USING transfer b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM token a
|
||||
USING token b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM issue_token a
|
||||
USING issue_token b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM burn a
|
||||
USING burn b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM nft a
|
||||
USING nft b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM marketing a
|
||||
USING marketing b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM vanity_address a
|
||||
USING vanity_address b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM swap a
|
||||
USING swap b
|
||||
WHERE a.id > b.id AND a.signature1 = b.signature1;
|
||||
|
||||
DELETE FROM swap a
|
||||
USING swap b
|
||||
WHERE a.id > b.id AND a.signature2 = b.signature2;
|
||||
|
||||
DELETE FROM loan_contract a
|
||||
USING loan_contract b
|
||||
WHERE a.id > b.id AND a.signature1 = b.signature1;
|
||||
|
||||
DELETE FROM loan_contract a
|
||||
USING loan_contract b
|
||||
WHERE a.id > b.id AND a.signature2 = b.signature2;
|
||||
|
||||
DELETE FROM loan_payment a
|
||||
USING loan_payment b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
|
||||
DELETE FROM collateral_claim a
|
||||
USING collateral_claim b
|
||||
WHERE a.id > b.id AND a.signature = b.signature;
|
||||
"#;
|
||||
|
||||
// Remove duplicate rows before unique indexes are created, otherwise stale
|
||||
|
|
@ -310,53 +310,53 @@ pub async fn setup_mempool() -> Result<()> {
|
|||
client.batch_execute(dedupe).await?;
|
||||
|
||||
let indexes = r#"
|
||||
CREATE INDEX IF NOT EXISTS transfer_pick_idx ON transfer (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS transfer_sig_idx ON transfer (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS transfer_sig_unique_idx ON transfer (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS token_pick_idx ON token (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS token_sig_idx ON token (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS token_sig_unique_idx ON token (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS issue_token_pick_idx ON issue_token (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS issue_token_sig_idx ON issue_token (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS issue_token_sig_unique_idx ON issue_token (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS burn_pick_idx ON burn (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS burn_sig_idx ON burn (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS burn_sig_unique_idx ON burn (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS nft_pick_idx ON nft (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS nft_sig_idx ON nft (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS nft_sig_unique_idx ON nft (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS marketing_pick_idx ON marketing (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS marketing_sig_idx ON marketing (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS marketing_sig_unique_idx ON marketing (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS vanity_address_pick_idx ON vanity_address (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS vanity_address_sig_idx ON vanity_address (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS vanity_address_sig_unique_idx ON vanity_address (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS swap_pick_idx ON swap (processed, GREATEST(fee1, fee2) DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS swap_sig1_idx ON swap (signature1);
|
||||
CREATE INDEX IF NOT EXISTS swap_sig2_idx ON swap (signature2);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS swap_sig1_unique_idx ON swap (signature1);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS swap_sig2_unique_idx ON swap (signature2);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS loan_contract_pick_idx ON loan_contract (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS loan_contract_sig1_idx ON loan_contract (signature1);
|
||||
CREATE INDEX IF NOT EXISTS loan_contract_sig2_idx ON loan_contract (signature2);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS loan_contract_sig1_unique_idx ON loan_contract (signature1);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS loan_contract_sig2_unique_idx ON loan_contract (signature2);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS loan_payment_pick_idx ON loan_payment (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS loan_payment_sig_idx ON loan_payment (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS loan_payment_sig_unique_idx ON loan_payment (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS collateral_claim_pick_idx ON collateral_claim (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS collateral_claim_sig_idx ON collateral_claim (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS collateral_claim_sig_unique_idx ON collateral_claim (signature);
|
||||
CREATE INDEX IF NOT EXISTS transfer_pick_idx ON transfer (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS transfer_sig_idx ON transfer (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS transfer_sig_unique_idx ON transfer (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS token_pick_idx ON token (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS token_sig_idx ON token (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS token_sig_unique_idx ON token (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS issue_token_pick_idx ON issue_token (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS issue_token_sig_idx ON issue_token (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS issue_token_sig_unique_idx ON issue_token (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS burn_pick_idx ON burn (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS burn_sig_idx ON burn (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS burn_sig_unique_idx ON burn (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS nft_pick_idx ON nft (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS nft_sig_idx ON nft (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS nft_sig_unique_idx ON nft (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS marketing_pick_idx ON marketing (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS marketing_sig_idx ON marketing (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS marketing_sig_unique_idx ON marketing (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS vanity_address_pick_idx ON vanity_address (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS vanity_address_sig_idx ON vanity_address (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS vanity_address_sig_unique_idx ON vanity_address (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS swap_pick_idx ON swap (processed, GREATEST(fee1, fee2) DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS swap_sig1_idx ON swap (signature1);
|
||||
CREATE INDEX IF NOT EXISTS swap_sig2_idx ON swap (signature2);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS swap_sig1_unique_idx ON swap (signature1);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS swap_sig2_unique_idx ON swap (signature2);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS loan_contract_pick_idx ON loan_contract (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS loan_contract_sig1_idx ON loan_contract (signature1);
|
||||
CREATE INDEX IF NOT EXISTS loan_contract_sig2_idx ON loan_contract (signature2);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS loan_contract_sig1_unique_idx ON loan_contract (signature1);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS loan_contract_sig2_unique_idx ON loan_contract (signature2);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS loan_payment_pick_idx ON loan_payment (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS loan_payment_sig_idx ON loan_payment (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS loan_payment_sig_unique_idx ON loan_payment (signature);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS collateral_claim_pick_idx ON collateral_claim (processed, fee DESC, time ASC, id ASC);
|
||||
CREATE INDEX IF NOT EXISTS collateral_claim_sig_idx ON collateral_claim (signature);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS collateral_claim_sig_unique_idx ON collateral_claim (signature);
|
||||
"#;
|
||||
// Pick indexes speed up block selection; signature indexes enforce one
|
||||
// pending copy of each transaction.
|
||||
|
|
@ -364,35 +364,34 @@ pub async fn setup_mempool() -> Result<()> {
|
|||
|
||||
// Live mempool data is not restored across startup.
|
||||
clear_mempool().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn clear_mempool() -> Result<()> {
|
||||
// Startup clears any leftover mempool rows so a node restart begins
|
||||
// from a clean pending-transaction state.
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
|
||||
client
|
||||
.batch_execute(
|
||||
r#"
|
||||
TRUNCATE TABLE
|
||||
transfer,
|
||||
token,
|
||||
issue_token,
|
||||
burn,
|
||||
nft,
|
||||
marketing,
|
||||
vanity_address,
|
||||
swap,
|
||||
loan_contract,
|
||||
loan_payment,
|
||||
collateral_claim
|
||||
RESTART IDENTITY;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn clear_mempool() -> Result<()> {
|
||||
// Startup clears any leftover mempool rows so a node restart begins
|
||||
// from a clean pending-transaction state.
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
|
||||
client
|
||||
.batch_execute(
|
||||
r#"
|
||||
TRUNCATE TABLE
|
||||
transfer,
|
||||
token,
|
||||
issue_token,
|
||||
burn,
|
||||
nft,
|
||||
marketing,
|
||||
vanity_address,
|
||||
swap,
|
||||
loan_contract,
|
||||
loan_payment,
|
||||
collateral_claim
|
||||
RESTART IDENTITY;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,9 +3,9 @@
|
|||
pub mod averages;
|
||||
pub mod connections;
|
||||
pub mod enums;
|
||||
pub mod response_channels;
|
||||
pub mod mempool;
|
||||
pub mod network_mapping;
|
||||
pub mod response_channels;
|
||||
pub mod structs;
|
||||
pub mod torrent_status;
|
||||
pub mod torrentmap;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
use super::*;
|
||||
|
||||
impl NodeInfo {
|
||||
pub async fn broadcast_node(
|
||||
map: Arc<Mutex<Command>>,
|
||||
edit: &SignedNodeEdit,
|
||||
remote_ip: &str,
|
||||
edittype: NodeEditType,
|
||||
connections_key: &str,
|
||||
) {
|
||||
// Re-broadcast signed node-map edits to connected peers while
|
||||
// skipping the source peer that already sent the update.
|
||||
let message_type = edittype.message_type();
|
||||
use super::*;
|
||||
|
||||
impl NodeInfo {
|
||||
pub async fn broadcast_node(
|
||||
map: Arc<Mutex<Command>>,
|
||||
edit: &SignedNodeEdit,
|
||||
remote_ip: &str,
|
||||
edittype: NodeEditType,
|
||||
connections_key: &str,
|
||||
) {
|
||||
// Re-broadcast signed node-map edits to connected peers while
|
||||
// skipping the source peer that already sent the update.
|
||||
let message_type = edittype.message_type();
|
||||
let ip_bytes = ip_to_binary(&edit.ip);
|
||||
let address_bytes = match Wallet::short_address_to_bytes(&edit.address) {
|
||||
Some(bytes) => bytes,
|
||||
None => {
|
||||
warn!(
|
||||
"[network_map] skipping broadcast for invalid short node address {}",
|
||||
edit.address
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let modified_by_bytes = Wallet::long_address_to_bytes(edit.modified_by.clone());
|
||||
let modified_timestamp_bytes = edit.modified_timestamp.to_le_bytes();
|
||||
let modified_signature_bytes = decode(&edit.modified_signature).unwrap();
|
||||
let address_bytes = match Wallet::short_address_to_bytes(&edit.address) {
|
||||
Some(bytes) => bytes,
|
||||
None => {
|
||||
warn!(
|
||||
"[network_map] skipping broadcast for invalid short node address {}",
|
||||
edit.address
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let modified_by_bytes = Wallet::long_address_to_bytes(edit.modified_by.clone());
|
||||
let modified_timestamp_bytes = edit.modified_timestamp.to_le_bytes();
|
||||
let modified_signature_bytes = decode(&edit.modified_signature).unwrap();
|
||||
let connections_lock = CONNECTIONS.read().await;
|
||||
let streams: Option<Vec<Arc<Mutex<TcpStream>>>> = connections_lock
|
||||
.as_ref()
|
||||
|
|
@ -35,12 +35,12 @@ impl NodeInfo {
|
|||
let (hashmap_key, _hashmap_tx, hashmap_rx) = reserve_entry(map.clone()).await;
|
||||
let mut message: Vec<u8> = Vec::new();
|
||||
message.push(message_type);
|
||||
message.extend_from_slice(&hashmap_key);
|
||||
message.extend_from_slice(&address_bytes);
|
||||
message.extend_from_slice(&ip_bytes);
|
||||
message.extend_from_slice(&modified_by_bytes);
|
||||
message.extend_from_slice(&modified_timestamp_bytes);
|
||||
message.extend_from_slice(&modified_signature_bytes);
|
||||
message.extend_from_slice(&hashmap_key);
|
||||
message.extend_from_slice(&address_bytes);
|
||||
message.extend_from_slice(&ip_bytes);
|
||||
message.extend_from_slice(&modified_by_bytes);
|
||||
message.extend_from_slice(&modified_timestamp_bytes);
|
||||
message.extend_from_slice(&modified_signature_bytes);
|
||||
let peer_addr = {
|
||||
let stream = unlocked_stream.lock().await;
|
||||
stream.peer_addr()
|
||||
|
|
@ -65,182 +65,182 @@ impl NodeInfo {
|
|||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("No active connections found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_address(params: AddAddressParams) -> RpcResponse {
|
||||
None => {
|
||||
warn!("No active connections found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_address(params: AddAddressParams) -> RpcResponse {
|
||||
let AddAddressParams {
|
||||
map,
|
||||
mut edit,
|
||||
mut blocks_mined,
|
||||
remote_ip,
|
||||
db,
|
||||
remote_ip,
|
||||
db,
|
||||
wallet_key,
|
||||
connections_key,
|
||||
} = params;
|
||||
let current_timestamp = Utc::now().timestamp_millis() as u64;
|
||||
let direct_peer_announcement = !remote_ip.is_empty() && edit.ip == remote_ip;
|
||||
|
||||
if !is_public_network_address(&edit.ip) {
|
||||
return RpcResponse::Binary(b"Error: Invalid network address".to_vec());
|
||||
}
|
||||
|
||||
// Locally initiated edits are re-signed with the local wallet and
|
||||
// current timestamp so they can be propagated as fresh node events.
|
||||
|
||||
if !is_public_network_address(&edit.ip) {
|
||||
return RpcResponse::Binary(b"Error: Invalid network address".to_vec());
|
||||
}
|
||||
|
||||
// Locally initiated edits are re-signed with the local wallet and
|
||||
// current timestamp so they can be propagated as fresh node events.
|
||||
if edit.ip == remote_ip {
|
||||
edit.modified_timestamp = current_timestamp;
|
||||
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
|
||||
Ok(wallet) => wallet,
|
||||
Err(err) => {
|
||||
error!("Wallet decryption failed while adding node address: {err}");
|
||||
return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec());
|
||||
}
|
||||
};
|
||||
edit.modified_by = wallet.saved.long_address;
|
||||
edit.modified_signature =
|
||||
Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key)
|
||||
.await;
|
||||
}
|
||||
|
||||
if !remote_ip.is_empty() {
|
||||
blocks_mined = 0;
|
||||
}
|
||||
|
||||
let data = format!(
|
||||
"{}{}{}{}",
|
||||
edit.address, edit.ip, edit.modified_by, edit.modified_timestamp
|
||||
);
|
||||
let hashed_data = skein_256_hash_data(&data);
|
||||
|
||||
// Every add/delete edit is signed, so the network map accepts
|
||||
// only node changes backed by a valid wallet signature.
|
||||
if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by)
|
||||
.await
|
||||
{
|
||||
return RpcResponse::Binary(b"Error: Could not validate signature".to_vec());
|
||||
}
|
||||
|
||||
let mut penalize_duplicate_ip = false;
|
||||
|
||||
{
|
||||
let mut address_map = ADDRESS_MAP.lock().await;
|
||||
|
||||
// Once the chain is mature, adding nodes is restricted to older
|
||||
// active participants with sufficient mined history.
|
||||
if get_height(&db) > 10000 {
|
||||
let signer_key = Wallet::normalize_to_short_address(&edit.modified_by)
|
||||
.unwrap_or_else(|| edit.modified_by.clone());
|
||||
let signer_node = address_map.get(&signer_key);
|
||||
let valid_added_by = signer_node
|
||||
.map(|node| {
|
||||
(current_timestamp - node.added_timestamp) >= 3600
|
||||
&& node.deleted_by.is_empty()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !valid_added_by {
|
||||
return RpcResponse::Binary(
|
||||
b"Error: This address cannot add nodes. It must exist for at least 60 minutes and not be marked for deletion"
|
||||
.to_vec(),
|
||||
);
|
||||
}
|
||||
let mined_count = signer_node.map(|node| node.blocks_mined).unwrap_or(0);
|
||||
if mined_count < 100 {
|
||||
return RpcResponse::Binary(
|
||||
b"Error: This address cannot add nodes. It must mined 100 blocks before adding new nodes to the network"
|
||||
.to_vec(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let added_by_count_in_last_hour = address_map
|
||||
.values()
|
||||
.filter(|node| {
|
||||
node.added_by == edit.modified_by
|
||||
&& (current_timestamp - node.added_timestamp) <= 3600
|
||||
})
|
||||
.count();
|
||||
|
||||
if added_by_count_in_last_hour >= 10 {
|
||||
return RpcResponse::Binary(
|
||||
b"Error: Cannot add more than 10 nodes in 60 minutes".to_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
// Existing deleted entries can be revived in place when the same
|
||||
// address/IP pair is re-announced, otherwise the older record is
|
||||
// discarded and replaced.
|
||||
if let Some(existing_node) = address_map.get_mut(&edit.address) {
|
||||
if !existing_node.deleted_by.is_empty() {
|
||||
if existing_node.ip == edit.ip {
|
||||
existing_node.deleted_by = "".to_string();
|
||||
existing_node.deleted_timestamp = 0_u64;
|
||||
existing_node.deleted_block = 0_u32;
|
||||
existing_node.deleted_signature = "".to_string();
|
||||
return RpcResponse::Binary(b"Success".to_vec());
|
||||
} else {
|
||||
address_map.remove(&edit.address);
|
||||
}
|
||||
} else {
|
||||
if edit.modified_timestamp < existing_node.added_timestamp {
|
||||
*existing_node = NodeInfo::new(
|
||||
edit.ip.clone(),
|
||||
blocks_mined,
|
||||
edit.modified_by.clone(),
|
||||
edit.modified_timestamp,
|
||||
edit.modified_signature.clone(),
|
||||
);
|
||||
}
|
||||
return RpcResponse::Binary(b"Success".to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(existing_node) = address_map.values_mut().find(|node| node.ip == edit.ip) {
|
||||
if !existing_node.deleted_by.is_empty() {
|
||||
address_map.retain(|_, node| node.ip != edit.ip);
|
||||
} else if edit.ip != GENESIS_IP {
|
||||
penalize_duplicate_ip = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !penalize_duplicate_ip {
|
||||
edit.modified_timestamp = current_timestamp;
|
||||
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
|
||||
Ok(wallet) => wallet,
|
||||
Err(err) => {
|
||||
error!("Wallet decryption failed while adding node address: {err}");
|
||||
return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec());
|
||||
}
|
||||
};
|
||||
edit.modified_by = wallet.saved.long_address;
|
||||
edit.modified_signature =
|
||||
Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key)
|
||||
.await;
|
||||
}
|
||||
|
||||
if !remote_ip.is_empty() {
|
||||
blocks_mined = 0;
|
||||
}
|
||||
|
||||
let data = format!(
|
||||
"{}{}{}{}",
|
||||
edit.address, edit.ip, edit.modified_by, edit.modified_timestamp
|
||||
);
|
||||
let hashed_data = skein_256_hash_data(&data);
|
||||
|
||||
// Every add/delete edit is signed, so the network map accepts
|
||||
// only node changes backed by a valid wallet signature.
|
||||
if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by)
|
||||
.await
|
||||
{
|
||||
return RpcResponse::Binary(b"Error: Could not validate signature".to_vec());
|
||||
}
|
||||
|
||||
let mut penalize_duplicate_ip = false;
|
||||
|
||||
{
|
||||
let mut address_map = ADDRESS_MAP.lock().await;
|
||||
|
||||
// Once the chain is mature, adding nodes is restricted to older
|
||||
// active participants with sufficient mined history.
|
||||
if get_height(&db) > 10000 {
|
||||
let signer_key = Wallet::normalize_to_short_address(&edit.modified_by)
|
||||
.unwrap_or_else(|| edit.modified_by.clone());
|
||||
let signer_node = address_map.get(&signer_key);
|
||||
let valid_added_by = signer_node
|
||||
.map(|node| {
|
||||
(current_timestamp - node.added_timestamp) >= 3600
|
||||
&& node.deleted_by.is_empty()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !valid_added_by {
|
||||
return RpcResponse::Binary(
|
||||
b"Error: This address cannot add nodes. It must exist for at least 60 minutes and not be marked for deletion"
|
||||
.to_vec(),
|
||||
);
|
||||
}
|
||||
let mined_count = signer_node.map(|node| node.blocks_mined).unwrap_or(0);
|
||||
if mined_count < 100 {
|
||||
return RpcResponse::Binary(
|
||||
b"Error: This address cannot add nodes. It must mined 100 blocks before adding new nodes to the network"
|
||||
.to_vec(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let added_by_count_in_last_hour = address_map
|
||||
.values()
|
||||
.filter(|node| {
|
||||
node.added_by == edit.modified_by
|
||||
&& (current_timestamp - node.added_timestamp) <= 3600
|
||||
})
|
||||
.count();
|
||||
|
||||
if added_by_count_in_last_hour >= 10 {
|
||||
return RpcResponse::Binary(
|
||||
b"Error: Cannot add more than 10 nodes in 60 minutes".to_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
// Existing deleted entries can be revived in place when the same
|
||||
// address/IP pair is re-announced, otherwise the older record is
|
||||
// discarded and replaced.
|
||||
if let Some(existing_node) = address_map.get_mut(&edit.address) {
|
||||
if !existing_node.deleted_by.is_empty() {
|
||||
if existing_node.ip == edit.ip {
|
||||
existing_node.deleted_by = "".to_string();
|
||||
existing_node.deleted_timestamp = 0_u64;
|
||||
existing_node.deleted_block = 0_u32;
|
||||
existing_node.deleted_signature = "".to_string();
|
||||
return RpcResponse::Binary(b"Success".to_vec());
|
||||
} else {
|
||||
address_map.remove(&edit.address);
|
||||
}
|
||||
} else {
|
||||
if edit.modified_timestamp < existing_node.added_timestamp {
|
||||
*existing_node = NodeInfo::new(
|
||||
edit.ip.clone(),
|
||||
blocks_mined,
|
||||
edit.modified_by.clone(),
|
||||
edit.modified_timestamp,
|
||||
edit.modified_signature.clone(),
|
||||
);
|
||||
}
|
||||
return RpcResponse::Binary(b"Success".to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(existing_node) = address_map.values_mut().find(|node| node.ip == edit.ip) {
|
||||
if !existing_node.deleted_by.is_empty() {
|
||||
address_map.retain(|_, node| node.ip != edit.ip);
|
||||
} else if edit.ip != GENESIS_IP {
|
||||
penalize_duplicate_ip = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !penalize_duplicate_ip {
|
||||
// Persist the new node locally. Network-map entries are bare
|
||||
// IP membership records, separate from live socket keys.
|
||||
address_map.insert(
|
||||
edit.address.clone(),
|
||||
NodeInfo::new(
|
||||
edit.ip.clone(),
|
||||
blocks_mined,
|
||||
edit.modified_by.clone(),
|
||||
edit.modified_timestamp,
|
||||
edit.modified_signature.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if penalize_duplicate_ip {
|
||||
let now = Utc::now().timestamp() as u32;
|
||||
let _ = update_ip_score(
|
||||
edit.ip.clone(),
|
||||
blocks_mined,
|
||||
edit.modified_by.clone(),
|
||||
edit.modified_timestamp,
|
||||
edit.modified_signature.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if penalize_duplicate_ip {
|
||||
let now = Utc::now().timestamp() as u32;
|
||||
let _ = update_ip_score(
|
||||
&remote_ip,
|
||||
"miner",
|
||||
InfractionType::BadMinerIpUpdate,
|
||||
now,
|
||||
&db,
|
||||
&wallet_key,
|
||||
)
|
||||
.await;
|
||||
return RpcResponse::Binary(b"Error: Ip Already exists.".to_vec());
|
||||
}
|
||||
|
||||
Self::broadcast_node(
|
||||
map.clone(),
|
||||
&edit,
|
||||
&remote_ip,
|
||||
NodeEditType::Add,
|
||||
&connections_key,
|
||||
now,
|
||||
&db,
|
||||
&wallet_key,
|
||||
)
|
||||
.await;
|
||||
return RpcResponse::Binary(b"Error: Ip Already exists.".to_vec());
|
||||
}
|
||||
|
||||
Self::broadcast_node(
|
||||
map.clone(),
|
||||
&edit,
|
||||
&remote_ip,
|
||||
NodeEditType::Add,
|
||||
&connections_key,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
|
@ -259,5 +259,4 @@ impl NodeInfo {
|
|||
|
||||
RpcResponse::Binary(b"Success".to_vec())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use super::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl NodeInfo {
|
||||
pub fn ping(params: PingMonitorParams) {
|
||||
tokio::spawn(async move {
|
||||
|
|
@ -37,37 +37,37 @@ impl NodeInfo {
|
|||
}
|
||||
|
||||
let mut failures = 0;
|
||||
|
||||
// Periodically ping the node and remove it after repeated
|
||||
// failures, then recursively reattach any inherited children.
|
||||
loop {
|
||||
sleep(Duration::from_secs(120)).await;
|
||||
|
||||
|
||||
// Periodically ping the node and remove it after repeated
|
||||
// failures, then recursively reattach any inherited children.
|
||||
loop {
|
||||
sleep(Duration::from_secs(120)).await;
|
||||
|
||||
{
|
||||
let monitors = PING_MONITORS.lock().await;
|
||||
// Stop this task if another monitor replaced it.
|
||||
if !monitors
|
||||
.get(&task_addr)
|
||||
.map(|current| current == &task_signature)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
.get(&task_addr)
|
||||
.map(|current| current == &task_signature)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let map_lock = ADDRESS_MAP.lock().await;
|
||||
if let Some(node) = map_lock.get(&task_addr) {
|
||||
// Stop monitoring stale node data after the map entry
|
||||
// has been replaced by a newer edit.
|
||||
if node.added_by != task_wallet || node.added_signature != task_signature {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(unlocked_stream) =
|
||||
Connection::get_stream_from_memory(&task_connections_key).await
|
||||
{
|
||||
|
|
@ -77,45 +77,45 @@ impl NodeInfo {
|
|||
// Liveness uses a normal block-height RPC and waits for the
|
||||
// reserved reply channel to receive any response.
|
||||
let mut message = vec![command];
|
||||
message.extend_from_slice(&ping_key);
|
||||
RpcResponse::send_raw(&unlocked_stream, Some(&task_connections_key), &message)
|
||||
.await;
|
||||
|
||||
let response = timeout(Duration::from_secs(10), async {
|
||||
let mut rx = ping_rx.lock().await;
|
||||
rx.recv().await
|
||||
})
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(Some(_buffer)) => {
|
||||
failures = 0;
|
||||
}
|
||||
message.extend_from_slice(&ping_key);
|
||||
RpcResponse::send_raw(&unlocked_stream, Some(&task_connections_key), &message)
|
||||
.await;
|
||||
|
||||
let response = timeout(Duration::from_secs(10), async {
|
||||
let mut rx = ping_rx.lock().await;
|
||||
rx.recv().await
|
||||
})
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(Some(_buffer)) => {
|
||||
failures = 0;
|
||||
}
|
||||
_ => {
|
||||
failures += 1;
|
||||
warn!("[network_map] ping failure: address={task_addr} ip={task_ip} failures={failures}");
|
||||
if failures >= 3 {
|
||||
warn!("[network_map] deleting node after ping failures: address={task_addr} ip={task_ip} responsible_by={task_wallet}");
|
||||
let _ = Self::delete_address(DeleteAddressParams {
|
||||
map: map.clone(),
|
||||
edit: SignedNodeEdit {
|
||||
address: task_addr.clone(),
|
||||
ip: task_ip.clone(),
|
||||
modified_by: task_wallet.clone(),
|
||||
modified_timestamp: added_timestamp,
|
||||
modified_signature: task_signature.clone(),
|
||||
},
|
||||
remote_ip: task_remote_ip.clone(),
|
||||
db: task_db.clone(),
|
||||
wallet_key: task_wallet_key.clone(),
|
||||
connections_key: task_connections_key.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
let _ = Self::delete_address(DeleteAddressParams {
|
||||
map: map.clone(),
|
||||
edit: SignedNodeEdit {
|
||||
address: task_addr.clone(),
|
||||
ip: task_ip.clone(),
|
||||
modified_by: task_wallet.clone(),
|
||||
modified_timestamp: added_timestamp,
|
||||
modified_signature: task_signature.clone(),
|
||||
},
|
||||
remote_ip: task_remote_ip.clone(),
|
||||
db: task_db.clone(),
|
||||
wallet_key: task_wallet_key.clone(),
|
||||
connections_key: task_connections_key.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The direct socket disappeared before this monitor fired.
|
||||
// Connection cleanup is owned by the connection manager, so
|
||||
|
|
@ -123,151 +123,151 @@ impl NodeInfo {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Self::release_ping_monitor(&task_addr, &task_signature).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn delete_address(params: DeleteAddressParams) -> RpcResponse {
|
||||
let DeleteAddressParams {
|
||||
map,
|
||||
mut edit,
|
||||
remote_ip,
|
||||
db,
|
||||
wallet_key,
|
||||
connections_key,
|
||||
} = params;
|
||||
let current_timestamp = Utc::now().timestamp_millis() as u64;
|
||||
|
||||
// Locally initiated deletions are re-signed with fresh metadata
|
||||
// before they are applied and broadcast.
|
||||
if remote_ip.is_empty() {
|
||||
edit.modified_timestamp = current_timestamp;
|
||||
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
|
||||
Ok(wallet) => wallet,
|
||||
Err(err) => {
|
||||
error!("Wallet decryption failed while deleting node address: {err}");
|
||||
return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec());
|
||||
}
|
||||
};
|
||||
edit.modified_by = wallet.saved.long_address;
|
||||
edit.modified_signature =
|
||||
Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key)
|
||||
.await;
|
||||
}
|
||||
|
||||
let data = format!(
|
||||
"{}{}{}{}",
|
||||
edit.address, edit.ip, edit.modified_by, edit.modified_timestamp
|
||||
);
|
||||
let hashed_data = skein_256_hash_data(&data);
|
||||
|
||||
if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by)
|
||||
.await
|
||||
{
|
||||
return RpcResponse::Binary(b"Error: Could not validate signature".to_vec());
|
||||
}
|
||||
|
||||
{
|
||||
let mut address_map = ADDRESS_MAP.lock().await;
|
||||
|
||||
|
||||
Self::release_ping_monitor(&task_addr, &task_signature).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn delete_address(params: DeleteAddressParams) -> RpcResponse {
|
||||
let DeleteAddressParams {
|
||||
map,
|
||||
mut edit,
|
||||
remote_ip,
|
||||
db,
|
||||
wallet_key,
|
||||
connections_key,
|
||||
} = params;
|
||||
let current_timestamp = Utc::now().timestamp_millis() as u64;
|
||||
|
||||
// Locally initiated deletions are re-signed with fresh metadata
|
||||
// before they are applied and broadcast.
|
||||
if remote_ip.is_empty() {
|
||||
edit.modified_timestamp = current_timestamp;
|
||||
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
|
||||
Ok(wallet) => wallet,
|
||||
Err(err) => {
|
||||
error!("Wallet decryption failed while deleting node address: {err}");
|
||||
return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec());
|
||||
}
|
||||
};
|
||||
edit.modified_by = wallet.saved.long_address;
|
||||
edit.modified_signature =
|
||||
Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key)
|
||||
.await;
|
||||
}
|
||||
|
||||
let data = format!(
|
||||
"{}{}{}{}",
|
||||
edit.address, edit.ip, edit.modified_by, edit.modified_timestamp
|
||||
);
|
||||
let hashed_data = skein_256_hash_data(&data);
|
||||
|
||||
if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by)
|
||||
.await
|
||||
{
|
||||
return RpcResponse::Binary(b"Error: Could not validate signature".to_vec());
|
||||
}
|
||||
|
||||
{
|
||||
let mut address_map = ADDRESS_MAP.lock().await;
|
||||
|
||||
if get_height(&db) > 10_000 {
|
||||
// Mature chains only allow established miners to remove nodes.
|
||||
let signer_key = Wallet::normalize_to_short_address(&edit.modified_by)
|
||||
.unwrap_or_else(|| edit.modified_by.clone());
|
||||
let signer_node = address_map.get(&signer_key);
|
||||
let valid_added_by = signer_node
|
||||
.map(|node| {
|
||||
(current_timestamp - node.added_timestamp) >= 3600
|
||||
&& node.deleted_by.is_empty()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if !valid_added_by {
|
||||
return RpcResponse::Binary(
|
||||
b"Error: Address must exist for 60m and not be deleted".to_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
let mined_count = signer_node.map(|node| node.blocks_mined).unwrap_or(0);
|
||||
if mined_count < 100 {
|
||||
return RpcResponse::Binary(
|
||||
b"Error: Must mine 100 blocks to remove nodes".to_vec(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.unwrap_or_else(|| edit.modified_by.clone());
|
||||
let signer_node = address_map.get(&signer_key);
|
||||
let valid_added_by = signer_node
|
||||
.map(|node| {
|
||||
(current_timestamp - node.added_timestamp) >= 3600
|
||||
&& node.deleted_by.is_empty()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if !valid_added_by {
|
||||
return RpcResponse::Binary(
|
||||
b"Error: Address must exist for 60m and not be deleted".to_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
let mined_count = signer_node.map(|node| node.blocks_mined).unwrap_or(0);
|
||||
if mined_count < 100 {
|
||||
return RpcResponse::Binary(
|
||||
b"Error: Must mine 100 blocks to remove nodes".to_vec(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let deleted_count_last_hour = address_map
|
||||
.values()
|
||||
.filter(|node| {
|
||||
node.deleted_by == edit.modified_by
|
||||
&& (current_timestamp - node.deleted_timestamp) <= 3600
|
||||
node.deleted_by == edit.modified_by
|
||||
&& (current_timestamp - node.deleted_timestamp) <= 3600
|
||||
})
|
||||
.count();
|
||||
|
||||
// Rate limit delete events per signer to prevent churn in the
|
||||
// shared network map.
|
||||
if deleted_count_last_hour >= 10 {
|
||||
return RpcResponse::Binary(b"Error: Max 10 deletions in 60m".to_vec());
|
||||
}
|
||||
|
||||
return RpcResponse::Binary(b"Error: Max 10 deletions in 60m".to_vec());
|
||||
}
|
||||
|
||||
if let Some(existing_node) = address_map.get_mut(&edit.address) {
|
||||
if !existing_node.deleted_by.is_empty() {
|
||||
return RpcResponse::Binary(
|
||||
b"Error: This address has already been deleted".to_vec(),
|
||||
);
|
||||
}
|
||||
return RpcResponse::Binary(
|
||||
b"Error: This address has already been deleted".to_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
// Deletion is recorded as metadata rather than immediately
|
||||
// removing the node, preserving historical validation context.
|
||||
existing_node.deleted_by = edit.modified_by.clone();
|
||||
existing_node.deleted_timestamp = current_timestamp;
|
||||
existing_node.deleted_block = get_height(&db) + 1;
|
||||
existing_node.deleted_signature = edit.modified_signature.clone();
|
||||
info!(
|
||||
"[network_map] node marked deleted: address={} ip={} deleted_by={} timestamp={} deleted_block={}",
|
||||
edit.address,
|
||||
edit.ip,
|
||||
edit.modified_by,
|
||||
current_timestamp,
|
||||
existing_node.deleted_block
|
||||
);
|
||||
} else {
|
||||
return RpcResponse::Binary(b"Error: Address not found".to_vec());
|
||||
}
|
||||
existing_node.deleted_timestamp = current_timestamp;
|
||||
existing_node.deleted_block = get_height(&db) + 1;
|
||||
existing_node.deleted_signature = edit.modified_signature.clone();
|
||||
info!(
|
||||
"[network_map] node marked deleted: address={} ip={} deleted_by={} timestamp={} deleted_block={}",
|
||||
edit.address,
|
||||
edit.ip,
|
||||
edit.modified_by,
|
||||
current_timestamp,
|
||||
existing_node.deleted_block
|
||||
);
|
||||
} else {
|
||||
return RpcResponse::Binary(b"Error: Address not found".to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
// Stop any ping task owned by the deleted node record.
|
||||
Self::release_ping_monitor(&edit.address, &edit.modified_signature).await;
|
||||
|
||||
// Deletions propagate to peers and also tear down any live
|
||||
// outgoing connection so bootstrap can recover a replacement.
|
||||
Self::broadcast_node(
|
||||
map.clone(),
|
||||
&edit,
|
||||
&remote_ip,
|
||||
NodeEditType::Delete,
|
||||
&connections_key,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
// Deletions propagate to peers and also tear down any live
|
||||
// outgoing connection so bootstrap can recover a replacement.
|
||||
Self::broadcast_node(
|
||||
map.clone(),
|
||||
&edit,
|
||||
&remote_ip,
|
||||
NodeEditType::Delete,
|
||||
&connections_key,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(port) = CONNECTIONS
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.and_then(|conn| conn.find_outgoing_port(&edit.ip))
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.and_then(|conn| conn.find_outgoing_port(&edit.ip))
|
||||
{
|
||||
let mut writer = CONNECTIONS.write().await;
|
||||
if let Some(conn) = writer.as_mut() {
|
||||
// Drop the live outgoing socket after marking the node deleted.
|
||||
conn.drop_connection(ConnectionType::Outgoing, edit.ip.clone(), port);
|
||||
info!(
|
||||
"[connection_manager] dropped dead outgoing connection: {}:{}",
|
||||
edit.ip, port
|
||||
);
|
||||
}
|
||||
drop(writer);
|
||||
|
||||
info!(
|
||||
"[connection_manager] dropped dead outgoing connection: {}:{}",
|
||||
edit.ip, port
|
||||
);
|
||||
}
|
||||
drop(writer);
|
||||
|
||||
let live_connection = {
|
||||
let guard = CONNECTIONS.read().await;
|
||||
guard.as_ref().and_then(|conn| {
|
||||
|
|
@ -287,15 +287,14 @@ impl NodeInfo {
|
|||
wallet_key: wallet_key.clone(),
|
||||
db: db.clone(),
|
||||
map: map.clone(),
|
||||
first: false,
|
||||
};
|
||||
spawn_reconnect_bootstrap(bootstrap_params);
|
||||
} else {
|
||||
warn!("[reconnect] No live stream found to bootstrap from");
|
||||
}
|
||||
}
|
||||
|
||||
RpcResponse::Binary(b"Success: Node marked as deleted".to_vec())
|
||||
}
|
||||
|
||||
}
|
||||
first: false,
|
||||
};
|
||||
spawn_reconnect_bootstrap(bootstrap_params);
|
||||
} else {
|
||||
warn!("[reconnect] No live stream found to bootstrap from");
|
||||
}
|
||||
}
|
||||
|
||||
RpcResponse::Binary(b"Success: Node marked as deleted".to_vec())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,50 +1,50 @@
|
|||
use super::*;
|
||||
|
||||
impl NodeInfo {
|
||||
use super::*;
|
||||
|
||||
impl NodeInfo {
|
||||
pub async fn increment_mined(address: &str) {
|
||||
let mut map = ADDRESS_MAP.lock().await;
|
||||
if let Some(node_info) = map.get_mut(address) {
|
||||
// Counts are capped at u8-safe policy maximum used by node rules.
|
||||
if node_info.blocks_mined < 250 {
|
||||
node_info.blocks_mined += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node_info.blocks_mined += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn decrement_mined(address: &str) {
|
||||
let mut map = ADDRESS_MAP.lock().await;
|
||||
if let Some(node_info) = map.get_mut(address) {
|
||||
// Rollback can undo mined credit, but never below zero.
|
||||
if node_info.blocks_mined > 0 {
|
||||
node_info.blocks_mined -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_mined_count(address: &str) -> u8 {
|
||||
let map = ADDRESS_MAP.lock().await;
|
||||
if let Some(node_info) = map.get(address) {
|
||||
node_info.blocks_mined
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
node_info.blocks_mined -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_mined_count(address: &str) -> u8 {
|
||||
let map = ADDRESS_MAP.lock().await;
|
||||
if let Some(node_info) = map.get(address) {
|
||||
node_info.blocks_mined
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_deleted_block_from_mapping(address: &str, deleted_block: u32) {
|
||||
let mut map = ADDRESS_MAP.lock().await;
|
||||
if let Some(node_info) = map.get_mut(address) {
|
||||
// The deletion height is filled in once the chain knows the block
|
||||
// where the delete action becomes active.
|
||||
node_info.deleted_block = deleted_block;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rebuild_mined_counts_from_chain(db: &Db) -> Result<(), String> {
|
||||
// Recompute node mined counts directly from saved block headers
|
||||
// so startup and recovery can rebuild memory-only state.
|
||||
let current_height = get_height(db);
|
||||
let mut mined_counts: HashMap<String, u8> = HashMap::new();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rebuild_mined_counts_from_chain(db: &Db) -> Result<(), String> {
|
||||
// Recompute node mined counts directly from saved block headers
|
||||
// so startup and recovery can rebuild memory-only state.
|
||||
let current_height = get_height(db);
|
||||
let mut mined_counts: HashMap<String, u8> = HashMap::new();
|
||||
|
||||
let start_height = if current_height > 0 { 1 } else { 0 };
|
||||
for block_number in start_height..=current_height {
|
||||
let header = load_block_header(block_number).await?;
|
||||
|
|
@ -52,26 +52,25 @@ impl NodeInfo {
|
|||
let entry = mined_counts.entry(miner).or_insert(0);
|
||||
// Keep the rebuilt value under the same cap as live increments.
|
||||
if *entry < 250 {
|
||||
*entry += 1;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut map = ADDRESS_MAP.lock().await;
|
||||
|
||||
*entry += 1;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut map = ADDRESS_MAP.lock().await;
|
||||
|
||||
for node_info in map.values_mut() {
|
||||
// Clear memory-only counts before applying the rebuilt chain
|
||||
// totals so removed or inactive miners do not keep stale data.
|
||||
node_info.blocks_mined = 0;
|
||||
}
|
||||
|
||||
for (address, mined_count) in mined_counts {
|
||||
if let Some(node_info) = map.get_mut(&address) {
|
||||
node_info.blocks_mined = mined_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (address, mined_count) in mined_counts {
|
||||
if let Some(node_info) = map.get_mut(&address) {
|
||||
node_info.blocks_mined = mined_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,83 +1,83 @@
|
|||
use crate::common::binary_conversions::ip_to_binary;
|
||||
use crate::common::network_startup::is_public_network_address;
|
||||
use crate::common::skein::skein_256_hash_data;
|
||||
use crate::common::types::GENESIS_IP;
|
||||
use crate::decode;
|
||||
use crate::lazy_static;
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::records::block_height::get_block_height::get_height;
|
||||
use crate::records::ip_score::enums::InfractionType;
|
||||
use crate::records::ip_score::score::update_ip_score;
|
||||
use crate::common::network_startup::is_public_network_address;
|
||||
use crate::common::skein::skein_256_hash_data;
|
||||
use crate::common::types::GENESIS_IP;
|
||||
use crate::decode;
|
||||
use crate::lazy_static;
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::records::block_height::get_block_height::get_height;
|
||||
use crate::records::ip_score::enums::InfractionType;
|
||||
use crate::records::ip_score::score::update_ip_score;
|
||||
use crate::records::memory::connections::{spawn_reconnect_bootstrap, CONNECTIONS};
|
||||
use crate::records::memory::enums::ConnectionType;
|
||||
use crate::records::memory::response_channels::{reserve_entry, Command};
|
||||
use crate::records::memory::network_mapping::enums::NodeEditType;
|
||||
use crate::records::memory::network_mapping::structs::{
|
||||
AddAddressParams, DeleteAddressParams, PingMonitorParams, SignedNodeEdit, NODE_RECORD_BYTES,
|
||||
};
|
||||
use crate::records::memory::structs::Connection;
|
||||
use crate::records::unpack_block::unpack_header::load_block_header;
|
||||
use crate::rpc::client::handshake_processing::BootstrapParams;
|
||||
use crate::rpc::command_maps::RPC_BLOCK_HEIGHT;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::sleep;
|
||||
use crate::timeout;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::Arc;
|
||||
use crate::Duration;
|
||||
use crate::HashMap;
|
||||
use crate::Mutex;
|
||||
use crate::TcpStream;
|
||||
use crate::Utc;
|
||||
|
||||
lazy_static! {
|
||||
static ref ADDRESS_MAP: Mutex<HashMap<String, NodeInfo>> = Mutex::new(HashMap::new());
|
||||
static ref PING_MONITORS: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeInfo {
|
||||
ip: String,
|
||||
blocks_mined: u8,
|
||||
added_by: String,
|
||||
added_timestamp: u64,
|
||||
added_signature: String,
|
||||
deleted_by: String,
|
||||
deleted_timestamp: u64,
|
||||
deleted_block: u32,
|
||||
deleted_signature: String,
|
||||
}
|
||||
|
||||
impl NodeInfo {
|
||||
async fn release_ping_monitor(address: &str, signature: &str) {
|
||||
let mut monitors = PING_MONITORS.lock().await;
|
||||
if monitors
|
||||
.get(address)
|
||||
.map(|current| current == signature)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
monitors.remove(address);
|
||||
}
|
||||
}
|
||||
|
||||
fn new(
|
||||
ip: String,
|
||||
blocks_mined: u8,
|
||||
added_by: String,
|
||||
added_timestamp: u64,
|
||||
added_signature: String,
|
||||
) -> Self {
|
||||
NodeInfo {
|
||||
ip,
|
||||
blocks_mined,
|
||||
added_by,
|
||||
added_timestamp,
|
||||
added_signature,
|
||||
deleted_by: "".to_string(),
|
||||
deleted_timestamp: 0_u64,
|
||||
deleted_block: 0_u32,
|
||||
deleted_signature: "".to_string(),
|
||||
use crate::records::memory::response_channels::{reserve_entry, Command};
|
||||
use crate::records::memory::structs::Connection;
|
||||
use crate::records::unpack_block::unpack_header::load_block_header;
|
||||
use crate::rpc::client::handshake_processing::BootstrapParams;
|
||||
use crate::rpc::command_maps::RPC_BLOCK_HEIGHT;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::sleep;
|
||||
use crate::timeout;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::Arc;
|
||||
use crate::Duration;
|
||||
use crate::HashMap;
|
||||
use crate::Mutex;
|
||||
use crate::TcpStream;
|
||||
use crate::Utc;
|
||||
|
||||
lazy_static! {
|
||||
static ref ADDRESS_MAP: Mutex<HashMap<String, NodeInfo>> = Mutex::new(HashMap::new());
|
||||
static ref PING_MONITORS: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeInfo {
|
||||
ip: String,
|
||||
blocks_mined: u8,
|
||||
added_by: String,
|
||||
added_timestamp: u64,
|
||||
added_signature: String,
|
||||
deleted_by: String,
|
||||
deleted_timestamp: u64,
|
||||
deleted_block: u32,
|
||||
deleted_signature: String,
|
||||
}
|
||||
|
||||
impl NodeInfo {
|
||||
async fn release_ping_monitor(address: &str, signature: &str) {
|
||||
let mut monitors = PING_MONITORS.lock().await;
|
||||
if monitors
|
||||
.get(address)
|
||||
.map(|current| current == signature)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
monitors.remove(address);
|
||||
}
|
||||
}
|
||||
|
||||
fn new(
|
||||
ip: String,
|
||||
blocks_mined: u8,
|
||||
added_by: String,
|
||||
added_timestamp: u64,
|
||||
added_signature: String,
|
||||
) -> Self {
|
||||
NodeInfo {
|
||||
ip,
|
||||
blocks_mined,
|
||||
added_by,
|
||||
added_timestamp,
|
||||
added_signature,
|
||||
deleted_by: "".to_string(),
|
||||
deleted_timestamp: 0_u64,
|
||||
deleted_block: 0_u32,
|
||||
deleted_signature: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -88,4 +88,3 @@ pub mod enums;
|
|||
mod mined_counts;
|
||||
mod queries;
|
||||
pub mod structs;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,24 +11,23 @@ impl NodeInfo {
|
|||
false
|
||||
}
|
||||
|
||||
|
||||
pub async fn find_address_by_ip(ip: &str) -> Option<String> {
|
||||
let map = ADDRESS_MAP.lock().await;
|
||||
for (address, node_info) in map.iter() {
|
||||
// Reverse lookup is needed when a peer is identified by socket IP
|
||||
// but the node map is keyed by wallet short address.
|
||||
if node_info.ip == ip {
|
||||
return Some(address.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn find_ip_by_address(address: &str) -> Option<String> {
|
||||
let map = ADDRESS_MAP.lock().await;
|
||||
map.get(address).map(|node_info| node_info.ip.clone())
|
||||
}
|
||||
|
||||
return Some(address.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn find_ip_by_address(address: &str) -> Option<String> {
|
||||
let map = ADDRESS_MAP.lock().await;
|
||||
map.get(address).map(|node_info| node_info.ip.clone())
|
||||
}
|
||||
|
||||
pub async fn active_node_ips() -> Vec<String> {
|
||||
let map = ADDRESS_MAP.lock().await;
|
||||
map.values()
|
||||
|
|
@ -37,8 +36,8 @@ impl NodeInfo {
|
|||
.filter(|node_info| node_info.deleted_by.is_empty())
|
||||
.map(|node_info| node_info.ip.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub async fn get_deleted_addresses() -> Vec<u8> {
|
||||
let map = ADDRESS_MAP.lock().await;
|
||||
map.iter()
|
||||
|
|
@ -46,25 +45,25 @@ impl NodeInfo {
|
|||
// The RPC response is a packed list of deleted short-address
|
||||
// bytes, so invalid address keys are skipped.
|
||||
if node_info.deleted_timestamp > 0 {
|
||||
Wallet::short_address_to_bytes(address)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn request_valid_nodes() -> RpcResponse {
|
||||
// Serialize the in-memory node map into the fixed binary layout
|
||||
// used by peer bootstrap and node-list synchronization.
|
||||
let map = ADDRESS_MAP.lock().await;
|
||||
let mut data: Vec<u8> = Vec::with_capacity(map.len() * NODE_RECORD_BYTES);
|
||||
|
||||
Wallet::short_address_to_bytes(address)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn request_valid_nodes() -> RpcResponse {
|
||||
// Serialize the in-memory node map into the fixed binary layout
|
||||
// used by peer bootstrap and node-list synchronization.
|
||||
let map = ADDRESS_MAP.lock().await;
|
||||
let mut data: Vec<u8> = Vec::with_capacity(map.len() * NODE_RECORD_BYTES);
|
||||
|
||||
for (address, node_info) in map.iter() {
|
||||
let address_bytes = match Wallet::short_address_to_bytes(address) {
|
||||
Some(bytes) => bytes,
|
||||
None => continue,
|
||||
Some(bytes) => bytes,
|
||||
None => continue,
|
||||
};
|
||||
let ip_bytes = ip_to_binary(&node_info.ip);
|
||||
let blocks_mined = node_info.blocks_mined;
|
||||
|
|
@ -74,58 +73,58 @@ impl NodeInfo {
|
|||
// Empty deletion fields serialize as zero-filled fixed-width values
|
||||
// so every node record stays the same size on the wire.
|
||||
let deleted_by_bytes = if node_info.deleted_by.is_empty() {
|
||||
vec![0u8; Wallet::ADDRESS_BYTES_LENGTH]
|
||||
} else {
|
||||
Wallet::long_address_to_bytes(node_info.deleted_by.to_string())
|
||||
};
|
||||
vec![0u8; Wallet::ADDRESS_BYTES_LENGTH]
|
||||
} else {
|
||||
Wallet::long_address_to_bytes(node_info.deleted_by.to_string())
|
||||
};
|
||||
|
||||
let deleted_timestamp_bytes = node_info.deleted_timestamp.to_le_bytes();
|
||||
let deleted_block_bytes = node_info.deleted_block.to_le_bytes();
|
||||
|
||||
let deleted_signature_bytes = if node_info.deleted_signature.is_empty() {
|
||||
vec![0u8; Wallet::SIGNATURE_LENGTH]
|
||||
} else {
|
||||
decode(node_info.deleted_signature.clone()).unwrap()
|
||||
};
|
||||
|
||||
vec![0u8; Wallet::SIGNATURE_LENGTH]
|
||||
} else {
|
||||
decode(node_info.deleted_signature.clone()).unwrap()
|
||||
};
|
||||
|
||||
let added_signature_bytes = decode(node_info.added_signature.clone()).unwrap();
|
||||
|
||||
// Field order here must match the parser used by node-list
|
||||
// synchronization.
|
||||
data.extend_from_slice(&address_bytes);
|
||||
data.extend_from_slice(&ip_bytes);
|
||||
data.push(blocks_mined);
|
||||
data.extend_from_slice(&added_by_bytes);
|
||||
data.extend_from_slice(&added_timestamp_bytes);
|
||||
data.extend_from_slice(&added_signature_bytes);
|
||||
data.extend_from_slice(&deleted_by_bytes);
|
||||
data.extend_from_slice(&deleted_timestamp_bytes);
|
||||
data.extend_from_slice(&deleted_block_bytes);
|
||||
data.extend_from_slice(&deleted_signature_bytes);
|
||||
}
|
||||
RpcResponse::Binary(data)
|
||||
}
|
||||
|
||||
pub async fn added_signature(
|
||||
address: &str,
|
||||
ip: &str,
|
||||
current_timestamp: u64,
|
||||
wallet_key: &str,
|
||||
) -> String {
|
||||
// Node edits are signed over address, IP, signer, and timestamp
|
||||
// so peers can independently verify the advertised change.
|
||||
let wallet = match Wallet::try_obtain_wallet(wallet_key.to_string(), None).await {
|
||||
Ok(wallet) => wallet,
|
||||
Err(err) => {
|
||||
error!("Wallet decryption failed while signing node edit: {err}");
|
||||
return String::new();
|
||||
}
|
||||
};
|
||||
let added_by = wallet.saved.long_address;
|
||||
let private_key = wallet.saved.private_key;
|
||||
|
||||
let data = format!("{address}{ip}{added_by}{current_timestamp}");
|
||||
let hashed_data = skein_256_hash_data(&data);
|
||||
Wallet::sign_transaction(&hashed_data, &private_key).await
|
||||
}
|
||||
}
|
||||
data.extend_from_slice(&ip_bytes);
|
||||
data.push(blocks_mined);
|
||||
data.extend_from_slice(&added_by_bytes);
|
||||
data.extend_from_slice(&added_timestamp_bytes);
|
||||
data.extend_from_slice(&added_signature_bytes);
|
||||
data.extend_from_slice(&deleted_by_bytes);
|
||||
data.extend_from_slice(&deleted_timestamp_bytes);
|
||||
data.extend_from_slice(&deleted_block_bytes);
|
||||
data.extend_from_slice(&deleted_signature_bytes);
|
||||
}
|
||||
RpcResponse::Binary(data)
|
||||
}
|
||||
|
||||
pub async fn added_signature(
|
||||
address: &str,
|
||||
ip: &str,
|
||||
current_timestamp: u64,
|
||||
wallet_key: &str,
|
||||
) -> String {
|
||||
// Node edits are signed over address, IP, signer, and timestamp
|
||||
// so peers can independently verify the advertised change.
|
||||
let wallet = match Wallet::try_obtain_wallet(wallet_key.to_string(), None).await {
|
||||
Ok(wallet) => wallet,
|
||||
Err(err) => {
|
||||
error!("Wallet decryption failed while signing node edit: {err}");
|
||||
return String::new();
|
||||
}
|
||||
};
|
||||
let added_by = wallet.saved.long_address;
|
||||
let private_key = wallet.saved.private_key;
|
||||
|
||||
let data = format!("{address}{ip}{added_by}{current_timestamp}");
|
||||
let hashed_data = skein_256_hash_data(&data);
|
||||
Wallet::sign_transaction(&hashed_data, &private_key).await
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ fn random_3_byte_number() -> [u8; 3] {
|
|||
let num: u32 = rng.gen_range(0..=0xFFFFFF);
|
||||
// The protocol UID is three bytes on the wire, so the random u32 is sliced
|
||||
// down to the same fixed-width little-endian layout used by requests.
|
||||
|
||||
|
||||
num.to_le_bytes()[1..4].try_into().unwrap()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,13 +80,21 @@ pub async fn process_lender(
|
|||
pending_effects.append_tree_if_key_exists(
|
||||
"nfts",
|
||||
"nft_history",
|
||||
transaction.unsigned_loan_contract.collateral.as_bytes().to_vec(),
|
||||
transaction
|
||||
.unsigned_loan_contract
|
||||
.collateral
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
txhash_bytes.clone(),
|
||||
);
|
||||
pending_effects.append_tree_if_key_exists(
|
||||
"nfts",
|
||||
"nft_history",
|
||||
transaction.unsigned_loan_contract.loan_coin.as_bytes().to_vec(),
|
||||
transaction
|
||||
.unsigned_loan_contract
|
||||
.loan_coin
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
txhash_bytes,
|
||||
);
|
||||
Ok(binary_data)
|
||||
|
|
|
|||
|
|
@ -60,8 +60,16 @@ pub async fn process_nft(
|
|||
BalanceOperand::Addition,
|
||||
);
|
||||
pending_effects.set_tree("nfts", nft_save_name.as_bytes().to_vec(), b"1".to_vec());
|
||||
pending_effects.set_tree("nft_origins", nft_save_name.as_bytes().to_vec(), txhash_bytes.clone());
|
||||
pending_effects.append_tree("nft_history", nft_save_name.into_bytes(), txhash_bytes.clone());
|
||||
pending_effects.set_tree(
|
||||
"nft_origins",
|
||||
nft_save_name.as_bytes().to_vec(),
|
||||
txhash_bytes.clone(),
|
||||
);
|
||||
pending_effects.append_tree(
|
||||
"nft_history",
|
||||
nft_save_name.into_bytes(),
|
||||
txhash_bytes.clone(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let nft_save_name = transaction.unsigned_create_nft.nft_name.clone();
|
||||
|
|
@ -72,8 +80,16 @@ pub async fn process_nft(
|
|||
BalanceOperand::Addition,
|
||||
);
|
||||
pending_effects.set_tree("nfts", nft_save_name.as_bytes().to_vec(), b"1".to_vec());
|
||||
pending_effects.set_tree("nft_origins", nft_save_name.as_bytes().to_vec(), txhash_bytes.clone());
|
||||
pending_effects.append_tree("nft_history", nft_save_name.into_bytes(), txhash_bytes.clone());
|
||||
pending_effects.set_tree(
|
||||
"nft_origins",
|
||||
nft_save_name.as_bytes().to_vec(),
|
||||
txhash_bytes.clone(),
|
||||
);
|
||||
pending_effects.append_tree(
|
||||
"nft_history",
|
||||
nft_save_name.into_bytes(),
|
||||
txhash_bytes.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
// Record the txid location so RPC lookups can resolve the saved
|
||||
|
|
|
|||
|
|
@ -373,12 +373,8 @@ fn apply_effect(db: &Db, effect: &PendingEffect) -> Result<AppliedEffect, String
|
|||
contract_id,
|
||||
payment,
|
||||
} => {
|
||||
let previous = append_tree_value(
|
||||
db,
|
||||
"contract_payments",
|
||||
contract_id,
|
||||
&payment.to_le_bytes(),
|
||||
)?;
|
||||
let previous =
|
||||
append_tree_value(db, "contract_payments", contract_id, &payment.to_le_bytes())?;
|
||||
Ok(AppliedEffect::TreeMutation {
|
||||
tree: "contract_payments",
|
||||
key: contract_id.clone(),
|
||||
|
|
@ -400,8 +396,10 @@ fn rollback_effect(db: &Db, effect: AppliedEffect) -> Result<(), String> {
|
|||
amount,
|
||||
coin,
|
||||
operand,
|
||||
} => balance_sheet_operation_with_db(db, &address, amount, &coin, operand.inverse().as_str())
|
||||
.map_err(|err| format!("Failed to roll back balance effect: {err}")),
|
||||
} => {
|
||||
balance_sheet_operation_with_db(db, &address, amount, &coin, operand.inverse().as_str())
|
||||
.map_err(|err| format!("Failed to roll back balance effect: {err}"))
|
||||
}
|
||||
AppliedEffect::TreeMutation {
|
||||
tree,
|
||||
key,
|
||||
|
|
@ -428,7 +426,12 @@ fn rollback_effect(db: &Db, effect: AppliedEffect) -> Result<(), String> {
|
|||
previous_rollback,
|
||||
} => {
|
||||
restore_vanity_mapping(db, &owner_address, previous_vanity)?;
|
||||
restore_tree_value(db, WALLET_VANITY_ROLLBACK_TREE, &rollback_key, previous_rollback)
|
||||
restore_tree_value(
|
||||
db,
|
||||
WALLET_VANITY_ROLLBACK_TREE,
|
||||
&rollback_key,
|
||||
previous_rollback,
|
||||
)
|
||||
}
|
||||
AppliedEffect::Noop => Ok(()),
|
||||
}
|
||||
|
|
@ -581,8 +584,8 @@ fn apply_vanity_effect(
|
|||
) -> Result<AppliedEffect, String> {
|
||||
let previous_vanity = get_registered_vanity_for_owner(db, owner_address)
|
||||
.map_err(|err| format!("Could not read existing vanity mapping: {err}"))?;
|
||||
let rollback_key =
|
||||
crate::decode(txhash).map_err(|_| "Could not decode vanity transaction hash".to_string())?;
|
||||
let rollback_key = crate::decode(txhash)
|
||||
.map_err(|_| "Could not decode vanity transaction hash".to_string())?;
|
||||
let rollback_tree = db
|
||||
.open_tree(WALLET_VANITY_ROLLBACK_TREE)
|
||||
.map_err(|err| format!("Could not open vanity rollback tree: {err}"))?;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
use crate::common::check_genesis::genesis_checkup;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::miner::flag::{is_mining_running, is_normal_mode, is_reorganizing_mode, is_syncing_mode};
|
||||
use crate::decode;
|
||||
use crate::fs;
|
||||
use crate::log::{error, info};
|
||||
use crate::miner::flag::{
|
||||
is_mining_running, is_normal_mode, is_reorganizing_mode, is_syncing_mode,
|
||||
};
|
||||
use crate::orphans::snapshot_check::{snapshot_height, update_snapshot};
|
||||
use crate::records::block_height::get_block_height::get_height;
|
||||
use crate::records::block_height::increase_block_height::increase_height;
|
||||
|
|
@ -8,8 +13,8 @@ use crate::records::memory::averages::{calculate_averages, update_block_data};
|
|||
use crate::records::memory::mempool::{
|
||||
apply_selected_transaction_math, mark_processed_by_signatures,
|
||||
mark_selected_transactions_processed, restore_processed_by_signatures,
|
||||
restore_selected_transactions_processed, select_transactions_for_block, spawn_processed_cleanup,
|
||||
stream_selected_transaction_originals,
|
||||
restore_selected_transactions_processed, select_transactions_for_block,
|
||||
spawn_processed_cleanup, stream_selected_transaction_originals,
|
||||
};
|
||||
use crate::records::memory::network_mapping::NodeInfo;
|
||||
use crate::records::memory::torrent_status::prune_torrent_statuses_through_height;
|
||||
|
|
@ -25,10 +30,7 @@ use crate::records::unpack_block::unpack_header::load_block_header;
|
|||
use crate::torrent::create_metadata::{broadcast_new_torrent_to_peers, metadata_from_file};
|
||||
use crate::torrent::torrenting_system::save_torrent::prune_staged_torrents;
|
||||
use crate::torrent::torrenting_system::torrent_cache::prune_recent_torrents;
|
||||
use crate::log::{error, info};
|
||||
use crate::Arc;
|
||||
use crate::decode;
|
||||
use crate::fs;
|
||||
use crate::Mutex;
|
||||
use crate::PathBuf;
|
||||
use crate::Utc;
|
||||
|
|
@ -337,7 +339,9 @@ async fn save_binary_data_with_mempool_stream(
|
|||
let _ = update_snapshot(db, next_number).await;
|
||||
if let Some(snapshot_height) = snapshot_height(db).await {
|
||||
if let Err(err) = finalize_rewards_through_height(db, snapshot_height).await {
|
||||
error!("Failed to finalize rewards through snapshot height {snapshot_height}: {err}");
|
||||
error!(
|
||||
"Failed to finalize rewards through snapshot height {snapshot_height}: {err}"
|
||||
);
|
||||
}
|
||||
prune_recent_torrents(snapshot_height).await;
|
||||
prune_torrent_statuses_through_height(snapshot_height).await;
|
||||
|
|
@ -465,7 +469,9 @@ async fn save_binary_data(params: SaveBinaryDataParams<'_>) -> Result<(), String
|
|||
let _ = update_snapshot(db, next_number).await;
|
||||
if let Some(snapshot_height) = snapshot_height(db).await {
|
||||
if let Err(err) = finalize_rewards_through_height(db, snapshot_height).await {
|
||||
error!("Failed to finalize rewards through snapshot height {snapshot_height}: {err}");
|
||||
error!(
|
||||
"Failed to finalize rewards through snapshot height {snapshot_height}: {err}"
|
||||
);
|
||||
}
|
||||
prune_recent_torrents(snapshot_height).await;
|
||||
prune_torrent_statuses_through_height(snapshot_height).await;
|
||||
|
|
|
|||
|
|
@ -23,8 +23,7 @@ pub async fn process_token(
|
|||
// Serialize the token-creation transaction and compute its txid
|
||||
// before applying the balance-sheet and token-registry updates.
|
||||
let txhash = transaction.unsigned_create_token.hash().await;
|
||||
let txhash_bytes =
|
||||
decode(&txhash).map_err(|e| format!("Error decoding token txhash: {e}"))?;
|
||||
let txhash_bytes = decode(&txhash).map_err(|e| format!("Error decoding token txhash: {e}"))?;
|
||||
let transaction_bytes = match transaction.to_bytes().await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => return Err(e.to_string()),
|
||||
|
|
@ -63,7 +62,11 @@ pub async fn process_token(
|
|||
|
||||
let origin_key = transaction.unsigned_create_token.ticker.clone();
|
||||
let origin_value = txhash.as_bytes();
|
||||
pending_effects.set_tree("token_origins", origin_key.into_bytes(), origin_value.to_vec());
|
||||
pending_effects.set_tree(
|
||||
"token_origins",
|
||||
origin_key.into_bytes(),
|
||||
origin_value.to_vec(),
|
||||
);
|
||||
pending_effects.append_tree(
|
||||
"token_history",
|
||||
transaction.unsigned_create_token.ticker.as_bytes().to_vec(),
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::decode;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
|
||||
pub(super) const WALLET_REGISTRY_TREE: &str = "wallet_registry";
|
||||
pub(super) const WALLET_VANITY_ADDRESS_TREE: &str = "wallet_vanity_address";
|
||||
pub(super) const WALLET_VANITY_OWNER_TREE: &str = "wallet_vanity_owner";
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::decode;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
|
||||
pub(super) const WALLET_REGISTRY_TREE: &str = "wallet_registry";
|
||||
pub(super) const WALLET_VANITY_ADDRESS_TREE: &str = "wallet_vanity_address";
|
||||
pub(super) const WALLET_VANITY_OWNER_TREE: &str = "wallet_vanity_owner";
|
||||
pub(crate) const WALLET_VANITY_ROLLBACK_TREE: &str = "wallet_vanity_rollback";
|
||||
|
||||
mod helpers;
|
||||
mod mappings;
|
||||
mod storage;
|
||||
pub mod structs;
|
||||
|
||||
pub use helpers::{get_registered_pubkey, is_registered_short_address, short_address_exists};
|
||||
pub use mappings::{
|
||||
get_registered_vanity_for_owner, list_registered_wallets, require_canonical_registered_short_address,
|
||||
resolve_canonical_registered_short_address, resolve_local_input_short_address,
|
||||
resolve_owner_from_vanity_address, resolve_pubkey_from_short_address, take_previous_vanity_for_txid,
|
||||
};
|
||||
pub use storage::{
|
||||
register_or_update_vanity_address, register_short_address, remove_registered_vanity_for_owner,
|
||||
store_previous_vanity_for_txid,
|
||||
};
|
||||
pub use structs::{VanityRegistrationResult, WalletRegistrationResult};
|
||||
|
||||
mod helpers;
|
||||
mod mappings;
|
||||
mod storage;
|
||||
pub mod structs;
|
||||
|
||||
pub use helpers::{get_registered_pubkey, is_registered_short_address, short_address_exists};
|
||||
pub use mappings::{
|
||||
get_registered_vanity_for_owner, list_registered_wallets,
|
||||
require_canonical_registered_short_address, resolve_canonical_registered_short_address,
|
||||
resolve_local_input_short_address, resolve_owner_from_vanity_address,
|
||||
resolve_pubkey_from_short_address, take_previous_vanity_for_txid,
|
||||
};
|
||||
pub use storage::{
|
||||
register_or_update_vanity_address, register_short_address, remove_registered_vanity_for_owner,
|
||||
store_previous_vanity_for_txid,
|
||||
};
|
||||
pub use structs::{VanityRegistrationResult, WalletRegistrationResult};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
use crate::common::network_startup::get_listen_ip;
|
||||
use crate::io;
|
||||
use crate::rpc::client::handshake_message::prepare_handshake_message;
|
||||
use crate::rpc::client::handshake_processing::process_handshake_response;
|
||||
use crate::rpc::client::structs::{Connect, Handshake};
|
||||
use crate::rpc::command_maps::{MAX_RPC_REPLY_BYTES, RPC_REPLY};
|
||||
use crate::rpc::handshake_constants::HANDSHAKE_RESPONSE_BYTES;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::{AsyncReadExt, AsyncWriteExt};
|
||||
use crate::io;
|
||||
use crate::IpAddr;
|
||||
use crate::SocketAddr;
|
||||
use crate::TcpStream;
|
||||
use crate::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpSocket;
|
||||
|
||||
pub async fn connect_and_handshake(params: Connect) -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use crate::common::binary_conversions::ip_port_to_binary;
|
||||
use crate::common::network_startup::get_ip_and_port;
|
||||
use crate::common::skein::skein_256_hash_data;
|
||||
use crate::decode;
|
||||
use crate::io;
|
||||
use crate::rpc::commands::time::request_time;
|
||||
use crate::rpc::handshake_constants::HANDSHAKE_REQUEST_BYTES;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::decode;
|
||||
use crate::io;
|
||||
|
||||
pub async fn prepare_handshake_message(wallet: &Wallet, message: &str) -> io::Result<Vec<u8>> {
|
||||
// Client handshakes are assembled from the signed message, wallet identity, timestamp,
|
||||
|
|
|
|||
|
|
@ -1,40 +1,46 @@
|
|||
use crate::common::binary_conversions::binary_to_ip_port;
|
||||
use crate::common::check_genesis::genesis_checkup;
|
||||
use crate::common::network_startup::get_ip_and_port;
|
||||
use crate::common::skein::skein_256_hash_data;
|
||||
use crate::config::SETTINGS;
|
||||
use crate::miner::flag::{clear_mining_stop_request, request_mining_stop, set_mining_state, set_node_mode, MiningState, NodeMode};
|
||||
use crate::encode;
|
||||
use crate::io;
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::miner::flag::{
|
||||
clear_mining_stop_request, request_mining_stop, set_mining_state, set_node_mode, MiningState,
|
||||
NodeMode,
|
||||
};
|
||||
use crate::orphans::structs::OrphanCheckup2;
|
||||
use crate::orphans::sync_check::sync_checkup;
|
||||
use crate::orphans::torrent_candidates::hydrate_torrent_candidates;
|
||||
use crate::records::block_height::get_block_height::get_height;
|
||||
use crate::records::memory::connections::{set_reconnect_context, CONNECTIONS};
|
||||
use crate::records::memory::enums::{ClientType, ConnectionType};
|
||||
use crate::records::memory::response_channels::{reserve_entry, Command};
|
||||
use crate::records::memory::network_mapping::NodeInfo;
|
||||
use crate::records::memory::response_channels::{reserve_entry, Command};
|
||||
use crate::records::memory::structs::{Connection, StoreConnectionParams};
|
||||
use crate::rpc::client::handshake::connect_and_handshake;
|
||||
use crate::rpc::client::register_wallet::register_connected_wallet;
|
||||
use crate::rpc::client::structs::{Connect, Handshake};
|
||||
use crate::rpc::client::syncing::node_syncing;
|
||||
use crate::rpc::client::register_wallet::register_connected_wallet;
|
||||
use crate::rpc::client::wallet_registry_sync::sync_wallet_registry;
|
||||
use crate::rpc::command_maps::RPC_RANDOM_NODE;
|
||||
use crate::rpc::handshake_constants::{HANDSHAKE_ADDRESS_OFFSET, HANDSHAKE_MESSAGE_BYTES, HANDSHAKE_RESPONSE_BYTES, HANDSHAKE_SIGNATURE_OFFSET,};
|
||||
use crate::rpc::handshake_constants::{
|
||||
HANDSHAKE_ADDRESS_OFFSET, HANDSHAKE_MESSAGE_BYTES, HANDSHAKE_RESPONSE_BYTES,
|
||||
HANDSHAKE_SIGNATURE_OFFSET,
|
||||
};
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::rpc::server::rpc_command_loop::start_loop;
|
||||
use crate::common::network_startup::get_ip_and_port;
|
||||
use crate::sled::Db;
|
||||
use crate::startup::network_broadcast::announce_self_to_network;
|
||||
use crate::startup::remote_height::request_remote_height;
|
||||
use crate::timeout;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::sled::Db;
|
||||
use crate::Arc;
|
||||
use crate::Duration;
|
||||
use crate::encode;
|
||||
use crate::io;
|
||||
use crate::Mutex;
|
||||
use crate::SocketAddr;
|
||||
use crate::TcpStream;
|
||||
use crate::timeout;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BootstrapParams {
|
||||
|
|
@ -104,7 +110,10 @@ pub async fn bootstrap_peer_discovery(mut params: BootstrapParams) -> Result<(),
|
|||
continue;
|
||||
}
|
||||
|
||||
if Connection::get_stream_from_memory(&addr_string).await.is_some() {
|
||||
if Connection::get_stream_from_memory(&addr_string)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
no_progress_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
use crate::common::skein::skein_256_hash_bytes;
|
||||
use crate::decode;
|
||||
use crate::log::warn;
|
||||
use crate::records::memory::response_channels::{reserve_entry, Command};
|
||||
use crate::rpc::command_maps::RPC_REGISTER_WALLET;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::timeout;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::log::warn;
|
||||
use crate::Arc;
|
||||
use crate::decode;
|
||||
use crate::Duration;
|
||||
use crate::Mutex;
|
||||
use crate::TcpStream;
|
||||
use crate::timeout;
|
||||
|
||||
pub async fn register_connected_wallet(
|
||||
stream: Arc<Mutex<TcpStream>>,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
use crate::common::check_genesis::genesis_checkup;
|
||||
use crate::io;
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::orphans::structs::OrphanCheckup2;
|
||||
use crate::orphans::sync_check::sync_checkup;
|
||||
use crate::records::block_height::get_block_height::get_height;
|
||||
use crate::records::memory::response_channels::reserve_entry;
|
||||
use crate::records::memory::response_channels::Command;
|
||||
use crate::torrent::structs::Torrent;
|
||||
use crate::torrent::torrenting_system::torrent_requests::{handle_response_and_save_torrent, send_request_torrent_message};
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::sled::Db;
|
||||
use crate::timeout;
|
||||
use crate::torrent::structs::Torrent;
|
||||
use crate::torrent::torrenting_system::torrent_requests::{
|
||||
handle_response_and_save_torrent, send_request_torrent_message,
|
||||
};
|
||||
use crate::Arc;
|
||||
use crate::Duration;
|
||||
use crate::io;
|
||||
use crate::Mutex;
|
||||
use crate::TcpStream;
|
||||
use crate::timeout;
|
||||
|
||||
pub async fn node_syncing(
|
||||
stream: Arc<Mutex<TcpStream>>,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
use crate::log::warn;
|
||||
use crate::records::memory::response_channels::{reserve_entry, Command};
|
||||
use crate::records::wallet_registry::{register_short_address, WalletRegistrationResult};
|
||||
use crate::rpc::commands::wallet_registry_sync::WALLET_REGISTRY_RECORD_BYTES;
|
||||
use crate::rpc::command_maps::RPC_WALLET_REGISTRY_SYNC;
|
||||
use crate::rpc::commands::wallet_registry_sync::WALLET_REGISTRY_RECORD_BYTES;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::log::warn;
|
||||
use crate::sled::Db;
|
||||
use crate::timeout;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::Arc;
|
||||
use crate::Duration;
|
||||
use crate::Mutex;
|
||||
use crate::TcpStream;
|
||||
use crate::timeout;
|
||||
|
||||
pub async fn sync_wallet_registry(
|
||||
stream: Arc<Mutex<TcpStream>>,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use crate::records::memory::response_channels::Command;
|
||||
use crate::records::memory::network_mapping::NodeInfo;
|
||||
use crate::records::memory::network_mapping::structs::{AddAddressParams, SignedNodeEdit};
|
||||
use crate::records::memory::network_mapping::NodeInfo;
|
||||
use crate::records::memory::response_channels::Command;
|
||||
use crate::rpc::read_bytes_from_stream;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::Arc;
|
||||
use crate::Mutex;
|
||||
use crate::TcpStream;
|
||||
|
|
@ -21,9 +21,11 @@ pub async fn add_network_node(
|
|||
let (uid, _) =
|
||||
read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone())
|
||||
.await?;
|
||||
let address_bytes =
|
||||
read_bytes_from_stream::read_short_address_from_stream(connections_key, stream_locked.clone())
|
||||
.await?;
|
||||
let address_bytes = read_bytes_from_stream::read_short_address_from_stream(
|
||||
connections_key,
|
||||
stream_locked.clone(),
|
||||
)
|
||||
.await?;
|
||||
let address = Wallet::bytes_to_short_address(&address_bytes)
|
||||
.ok_or_else(|| "error: Invalid short address bytes".to_string())?;
|
||||
let ip =
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use crate::records::balance_sheet::get_wallet_balance::get_balance;
|
||||
use crate::records::wallet_registry::resolve_canonical_registered_short_address;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
|
||||
pub async fn lookup_wallet_coin(db: &Db, address: String, coin: String) -> RpcResponse {
|
||||
// Return the saved confirmed balance for a specific address/asset pair.
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
use crate::common::nft_assets::nft_asset_parts;
|
||||
use crate::log::error;
|
||||
use crate::read_dir;
|
||||
use crate::records::balance_sheet::pathing::{address_root_path, asset_name_from_relative_path};
|
||||
use crate::records::wallet_registry::resolve_canonical_registered_short_address;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::log::error;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::AsyncReadExt;
|
||||
use crate::File;
|
||||
use crate::Path;
|
||||
use crate::read_dir;
|
||||
|
||||
pub async fn get_token_balances(db: &Db, address: String) -> RpcResponse {
|
||||
// Walk the hierarchical balance-sheet tree for one address and emit
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ pub async fn request_block(db: &Db, hash: &str) -> RpcResponse {
|
|||
let hash_bytes = match decode(hash) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => {
|
||||
return RpcResponse::Binary(
|
||||
format!("error: Failed to decode hash: {err}").into_bytes(),
|
||||
)
|
||||
return RpcResponse::Binary(format!("error: Failed to decode hash: {err}").into_bytes())
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -57,8 +55,6 @@ pub async fn request_block(db: &Db, hash: &str) -> RpcResponse {
|
|||
format!("error: Failed to convert block to bytes: {err}").into_bytes(),
|
||||
),
|
||||
},
|
||||
Err(err) => {
|
||||
RpcResponse::Binary(format!("error: Failed to load block: {err}").into_bytes())
|
||||
}
|
||||
Err(err) => RpcResponse::Binary(format!("error: Failed to load block: {err}").into_bytes()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::decode;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::decode;
|
||||
|
||||
pub async fn lookup_by_hash(db: &Db, hash: &str) -> RpcResponse {
|
||||
// Resolve the block hash through the block-hash index and then fetch
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
|
||||
pub async fn block_peer(db: &Db, ip: String, signature: String, wallet_key: String) -> RpcResponse {
|
||||
// Peer blocking is restricted to the local node owner, proven by a
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use crate::blocks::collateral::CollateralClaimTransaction;
|
||||
use crate::blocks::loan_payment::ContractPaymentTransaction;
|
||||
use crate::blocks::loans::LoanContractTransaction;
|
||||
use crate::encode;
|
||||
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::{DateTime, Datelike, Local, TimeZone, Utc};
|
||||
use crate::encode;
|
||||
|
||||
fn format_amount(value: u64) -> f64 {
|
||||
// Contract RPC output presents coin amounts as user-facing decimal values.
|
||||
|
|
@ -103,8 +103,7 @@ async fn collect_contract_activity(
|
|||
let mut collateral_claim: Option<CollateralClaimTransaction> = None;
|
||||
|
||||
for entry in tree.iter() {
|
||||
let (txid_bytes, _) =
|
||||
entry.map_err(|e| format!("error: Failed to read txid tree: {e}"))?;
|
||||
let (txid_bytes, _) = entry.map_err(|e| format!("error: Failed to read txid tree: {e}"))?;
|
||||
let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, txid_bytes.to_vec()).await;
|
||||
if bytes.is_empty() {
|
||||
continue;
|
||||
|
|
@ -243,8 +242,7 @@ pub async fn contract_details(hash: Vec<u8>, db: &Db) -> RpcResponse {
|
|||
pub async fn contract_details_by_address(address: String, db: &Db) -> RpcResponse {
|
||||
// Return every saved contract where the address appears as either the
|
||||
// lender or borrower, each expanded into the same text summary view.
|
||||
let Some(address) = Wallet::normalize_to_short_address(&address)
|
||||
else {
|
||||
let Some(address) = Wallet::normalize_to_short_address(&address) else {
|
||||
return RpcResponse::Binary(b"error: Invalid wallet address".to_vec());
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use crate::records::memory::response_channels::Command;
|
||||
use crate::records::memory::network_mapping::NodeInfo;
|
||||
use crate::records::memory::network_mapping::structs::{DeleteAddressParams, SignedNodeEdit};
|
||||
use crate::records::memory::network_mapping::NodeInfo;
|
||||
use crate::records::memory::response_channels::Command;
|
||||
use crate::rpc::read_bytes_from_stream;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::Arc;
|
||||
use crate::Mutex;
|
||||
use crate::TcpStream;
|
||||
|
|
@ -21,9 +21,11 @@ pub async fn delete_network_node(
|
|||
let (uid, _) =
|
||||
read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone())
|
||||
.await?;
|
||||
let address_bytes =
|
||||
read_bytes_from_stream::read_short_address_from_stream(connections_key, stream_locked.clone())
|
||||
.await?;
|
||||
let address_bytes = read_bytes_from_stream::read_short_address_from_stream(
|
||||
connections_key,
|
||||
stream_locked.clone(),
|
||||
)
|
||||
.await?;
|
||||
let address = Wallet::bytes_to_short_address(&address_bytes)
|
||||
.ok_or_else(|| "error: Invalid short address bytes".to_string())?;
|
||||
let ip =
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// The rpc commands module groups the request handlers that run after a client handshake succeeds.
|
||||
pub mod add_network_node;
|
||||
pub mod address_coin_lookup;
|
||||
pub mod address_complete_balance_sheet;
|
||||
pub mod add_network_node;
|
||||
pub mod bad_rpc_call;
|
||||
pub mod block_by_hash;
|
||||
pub mod block_by_height;
|
||||
|
|
@ -11,10 +11,10 @@ pub mod block_headers;
|
|||
pub mod block_height;
|
||||
pub mod block_peer_ip;
|
||||
pub mod contract;
|
||||
pub mod difficulty;
|
||||
pub mod delete_network_node;
|
||||
pub mod latest_block;
|
||||
pub mod difficulty;
|
||||
pub mod largest_tx_fee;
|
||||
pub mod latest_block;
|
||||
pub mod memory_by_signature;
|
||||
pub mod network_info;
|
||||
pub mod nft_list;
|
||||
|
|
@ -31,14 +31,14 @@ pub mod torrent;
|
|||
pub mod torrent_by_block;
|
||||
pub mod torrent_candidates;
|
||||
pub mod transaction_by_txid;
|
||||
pub mod transactions_by_address;
|
||||
pub mod tx_count;
|
||||
pub mod tx_count_from_mempool;
|
||||
pub mod tx_submit;
|
||||
pub mod transactions_by_address;
|
||||
pub mod unblock_peer_ip;
|
||||
pub mod validate_address;
|
||||
pub mod validate_torrent;
|
||||
pub mod validate_message;
|
||||
pub mod validate_torrent;
|
||||
pub mod wallet_register;
|
||||
pub mod wallet_registry_sync;
|
||||
pub mod wallet_vanity_lookup;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::common::nft_assets::nft_asset_parts;
|
||||
use crate::encode;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::encode;
|
||||
|
||||
pub async fn get_nfts(db: &Db) -> RpcResponse {
|
||||
// Serialize every NFT asset as origin hash, padded asset name, and
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@ use crate::blocks::swap::SwapTransaction;
|
|||
use crate::blocks::transfer::TransferTransaction;
|
||||
use crate::common::binary_conversions::binary_to_string;
|
||||
use crate::common::nft_assets::{nft_asset_name, nft_asset_parts};
|
||||
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::rpc::commands::transaction_by_txid::{request_transaction_by_txid, request_transaction_by_txid_with_block};
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::sled::Db;
|
||||
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::rpc::commands::transaction_by_txid::{
|
||||
request_transaction_by_txid, request_transaction_by_txid_with_block,
|
||||
};
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
|
||||
const ACTION_CREATE: u8 = 1;
|
||||
const ACTION_TRANSFER: u8 = 2;
|
||||
|
|
@ -102,7 +104,8 @@ async fn find_nft_origin(
|
|||
let txid_tree = db.open_tree("txid").ok()?;
|
||||
for entry in txid_tree.iter() {
|
||||
let (txid_bytes, _) = entry.ok()?;
|
||||
let RpcResponse::Binary(tx_bytes) = request_transaction_by_txid(db, txid_bytes.to_vec()).await;
|
||||
let RpcResponse::Binary(tx_bytes) =
|
||||
request_transaction_by_txid(db, txid_bytes.to_vec()).await;
|
||||
|
||||
if tx_bytes.is_empty() || tx_bytes[0] != 4 {
|
||||
continue;
|
||||
|
|
@ -173,7 +176,8 @@ async fn find_current_holder(asset_name: &str) -> String {
|
|||
async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Option<Vec<u8>> {
|
||||
// Expand a raw NFT history txid into a fixed-width history entry that
|
||||
// captures the action, involved wallets, and any received asset details.
|
||||
let RpcResponse::Binary(response) = request_transaction_by_txid_with_block(db, txid_bytes.to_vec()).await;
|
||||
let RpcResponse::Binary(response) =
|
||||
request_transaction_by_txid_with_block(db, txid_bytes.to_vec()).await;
|
||||
|
||||
if response.len() < 5 {
|
||||
return None;
|
||||
|
|
@ -267,7 +271,8 @@ async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Op
|
|||
.await
|
||||
.ok()?;
|
||||
let contract_hash = decode(&tx.unsigned_contract_payment.contract_hash).ok()?;
|
||||
let RpcResponse::Binary(contract_bytes) = request_transaction_by_txid(db, contract_hash).await;
|
||||
let RpcResponse::Binary(contract_bytes) =
|
||||
request_transaction_by_txid(db, contract_hash).await;
|
||||
let contract = LoanContractTransaction::from_bytes(7, &contract_bytes)
|
||||
.await
|
||||
.ok()?;
|
||||
|
|
@ -285,7 +290,8 @@ async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Op
|
|||
.await
|
||||
.ok()?;
|
||||
let contract_hash = decode(&tx.unsigned_collateral_claim.contract_hash).ok()?;
|
||||
let RpcResponse::Binary(contract_bytes) = request_transaction_by_txid(db, contract_hash).await;
|
||||
let RpcResponse::Binary(contract_bytes) =
|
||||
request_transaction_by_txid(db, contract_hash).await;
|
||||
let contract = LoanContractTransaction::from_bytes(7, &contract_bytes)
|
||||
.await
|
||||
.ok()?;
|
||||
|
|
@ -339,7 +345,8 @@ pub async fn lookup_nft_details(db: &Db, nft_name: String, item_number: u32) ->
|
|||
return RpcResponse::Binary(b"error: NFT genesis not found".to_vec());
|
||||
};
|
||||
|
||||
let RpcResponse::Binary(genesis_response) = request_transaction_by_txid(db, genesis_bytes.clone()).await;
|
||||
let RpcResponse::Binary(genesis_response) =
|
||||
request_transaction_by_txid(db, genesis_bytes.clone()).await;
|
||||
if genesis_response.is_empty() || genesis_response[0] != 4 {
|
||||
return RpcResponse::Binary(b"error: NFT genesis transaction not found".to_vec());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use crate::common::check_genesis::genesis_checkup;
|
||||
use crate::common::skein::skein_128_hash_bytes;
|
||||
use crate::lazy_static;
|
||||
use crate::log::{error, warn};
|
||||
use crate::miner::flag::{is_reorganizing_mode, is_syncing_mode};
|
||||
use crate::orphans::structs::OrphanCheckup2;
|
||||
use crate::orphans::sync_check::sync_checkup;
|
||||
|
|
@ -8,17 +10,19 @@ use crate::records::memory::response_channels::Command;
|
|||
use crate::rpc::read_bytes_from_stream;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::rpc::server::flood_protection::MAX_TORRENT_METADATA_BYTES;
|
||||
use crate::startup::remote_height::request_remote_height;
|
||||
use crate::torrent::structs::Torrent;
|
||||
use crate::torrent::create_metadata::broadcast_new_torrent_to_peers;
|
||||
use crate::torrent::torrenting_system::torrent_requests::{setup_download_for_torrent, stage_and_verify_torrent};
|
||||
use crate::torrent::torrenting_system::torrent_cache::{has_recent_torrent, remember_recent_torrent};
|
||||
use crate::log::{error, warn};
|
||||
use crate::sled::Db;
|
||||
use crate::startup::remote_height::request_remote_height;
|
||||
use crate::torrent::create_metadata::broadcast_new_torrent_to_peers;
|
||||
use crate::torrent::structs::Torrent;
|
||||
use crate::torrent::torrenting_system::torrent_cache::{
|
||||
has_recent_torrent, remember_recent_torrent,
|
||||
};
|
||||
use crate::torrent::torrenting_system::torrent_requests::{
|
||||
setup_download_for_torrent, stage_and_verify_torrent,
|
||||
};
|
||||
use crate::Arc;
|
||||
use crate::AtomicBool;
|
||||
use crate::AtomicOrdering;
|
||||
use crate::lazy_static;
|
||||
use crate::Mutex;
|
||||
|
||||
lazy_static! {
|
||||
|
|
@ -156,8 +160,7 @@ pub async fn torrent_submission(
|
|||
return TorrentSubmissionOutcome::Rejected(RpcResponse::Binary(msg));
|
||||
}
|
||||
|
||||
match stage_and_verify_torrent(height, db, torrent, wallet_key, process_now).await
|
||||
{
|
||||
match stage_and_verify_torrent(height, db, torrent, wallet_key, process_now).await {
|
||||
Ok(stage_result) => {
|
||||
let _ = remember_recent_torrent(&torrent_hash, height).await;
|
||||
if let Some((torrent, staged_path)) = stage_result {
|
||||
|
|
@ -273,7 +276,8 @@ pub async fn receive_torrent(
|
|||
) -> Result<(u32, RpcResponse), String> {
|
||||
let (uid, _) =
|
||||
read_bytes_from_stream::read_uid_from_stream(connections_key, stream.clone()).await?;
|
||||
let size = read_bytes_from_stream::read_u32_from_stream(connections_key, stream.clone()).await?;
|
||||
let size =
|
||||
read_bytes_from_stream::read_u32_from_stream(connections_key, stream.clone()).await?;
|
||||
|
||||
// The size includes the block-height field, so the remaining bytes
|
||||
// are the torrent metadata that will be parsed and staged.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use crate::records::memory::enums::ClientType;
|
||||
use crate::records::memory::response_channels::{delete_entry, get_entry, is_retired_entry, Command};
|
||||
use crate::rpc::commands::bad_rpc_call;
|
||||
use crate::rpc::command_maps::MAX_RPC_REPLY_BYTES;
|
||||
use crate::rpc::read_bytes_from_stream;
|
||||
use crate::log::warn;
|
||||
use crate::records::memory::enums::ClientType;
|
||||
use crate::records::memory::response_channels::{
|
||||
delete_entry, get_entry, is_retired_entry, Command,
|
||||
};
|
||||
use crate::rpc::command_maps::MAX_RPC_REPLY_BYTES;
|
||||
use crate::rpc::commands::bad_rpc_call;
|
||||
use crate::rpc::read_bytes_from_stream;
|
||||
use crate::sled::Db;
|
||||
use crate::Arc;
|
||||
use crate::Mutex;
|
||||
|
|
@ -24,8 +26,8 @@ pub async fn route_reply(
|
|||
read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone())
|
||||
.await?;
|
||||
let message_length =
|
||||
read_bytes_from_stream::read_u32_from_stream(connections_key, stream_locked.clone())
|
||||
.await? as usize;
|
||||
read_bytes_from_stream::read_u32_from_stream(connections_key, stream_locked.clone()).await?
|
||||
as usize;
|
||||
if message_length > MAX_RPC_REPLY_BYTES {
|
||||
bad_rpc_call::record(ip, client_type, db, wallet_key).await;
|
||||
return Err(format!(
|
||||
|
|
@ -44,9 +46,7 @@ pub async fn route_reply(
|
|||
)
|
||||
.await?;
|
||||
if tx.send(buffer).await.is_err() {
|
||||
warn!(
|
||||
"[rpc] reply receiver dropped before payload delivery: {uid:?}"
|
||||
);
|
||||
warn!("[rpc] reply receiver dropped before payload delivery: {uid:?}");
|
||||
}
|
||||
|
||||
delete_entry(map, uid).await;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
use crate::blocks::token::CreateTokenTransaction;
|
||||
use crate::common::binary_conversions::binary_to_string;
|
||||
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::rpc::commands::transaction_by_txid::request_transaction_by_txid;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::sled::{Db, Tree};
|
||||
use crate::{decode, encode};
|
||||
use crate::fs;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::PathBuf;
|
||||
use crate::{decode, encode};
|
||||
|
||||
fn parse_token_supply(value: &[u8]) -> Option<u64> {
|
||||
// Token supply may be stored either as raw bytes or as a decimal string.
|
||||
|
|
@ -85,7 +85,8 @@ async fn find_origin_hash(
|
|||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let RpcResponse::Binary(tx_bytes) = request_transaction_by_txid(db, txid_bytes.to_vec()).await;
|
||||
let RpcResponse::Binary(tx_bytes) =
|
||||
request_transaction_by_txid(db, txid_bytes.to_vec()).await;
|
||||
|
||||
// The fallback only cares about create-token transactions because
|
||||
// those define the origin hash for a token.
|
||||
|
|
@ -221,7 +222,8 @@ pub async fn lookup_token_details(db: &Db, token_name: String) -> RpcResponse {
|
|||
None => return RpcResponse::Binary(b"error: Token origin not found".to_vec()),
|
||||
};
|
||||
|
||||
let RpcResponse::Binary(tx_bytes) = request_transaction_by_txid(db, decode(&origin_hash).unwrap_or_default()).await;
|
||||
let RpcResponse::Binary(tx_bytes) =
|
||||
request_transaction_by_txid(db, decode(&origin_hash).unwrap_or_default()).await;
|
||||
|
||||
if tx_bytes.is_empty() {
|
||||
return RpcResponse::Binary(b"error: Token contract transaction not found".to_vec());
|
||||
|
|
|
|||
|
|
@ -1,211 +1,184 @@
|
|||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::common::skein::skein_128_hash_bytes;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::torrent::structs::Torrent;
|
||||
use crate::sled::Db;
|
||||
use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom};
|
||||
use crate::{decode, encode};
|
||||
use crate::File;
|
||||
use crate::Path;
|
||||
use crate::PathBuf;
|
||||
|
||||
fn remove_block_pieces_from_db(db: &Db, block_number: u32, info_hash: &str) {
|
||||
// When the canonical torrent exists, temporary cached pieces for that
|
||||
// block are no longer needed and can be dropped from the piece cache.
|
||||
let Ok(tree) = db.open_tree("block_pieces") else {
|
||||
return;
|
||||
};
|
||||
let prefix = format!("{block_number}-{info_hash}-");
|
||||
let iter = tree.range(prefix.as_bytes()..);
|
||||
|
||||
for (key, _value) in iter.flatten() {
|
||||
if !key.starts_with(prefix.as_bytes()) {
|
||||
break;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::common::skein::skein_128_hash_bytes;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::torrent::structs::Torrent;
|
||||
use crate::File;
|
||||
use crate::PathBuf;
|
||||
use crate::{decode, encode};
|
||||
use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom};
|
||||
|
||||
pub async fn request_block_piece(
|
||||
db: &Db,
|
||||
block_number: u32,
|
||||
requested_piece: u8,
|
||||
requested_info_hash: u128,
|
||||
) -> RpcResponse {
|
||||
// Serve a block piece either from the temporary cached-piece tree or
|
||||
// by slicing it directly from the canonical block file using torrent metadata.
|
||||
let tree = match db.open_tree("block_pieces") {
|
||||
Ok(tree) => tree,
|
||||
Err(err) => {
|
||||
let msg = format!("error: Failed to open block_pieces tree: {err}")
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
let _ = tree.remove(key);
|
||||
};
|
||||
let requested_info_hash_hex = encode(requested_info_hash.to_le_bytes());
|
||||
let key = format!("{block_number}-{requested_info_hash_hex}-{requested_piece}");
|
||||
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
block_ext,
|
||||
torrent_path,
|
||||
_wallet_path,
|
||||
block_path,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let block_filename = PathBuf::from(&block_path)
|
||||
.join(format!("{block_number}.{block_ext}"))
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let torrent_filename = PathBuf::from(&torrent_path)
|
||||
.join(format!("{block_number}.torrent"))
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
if let Some(piece_data) = tree.get(&key).ok().and_then(|result| result) {
|
||||
// Cached pieces are used for in-progress downloads before the
|
||||
// canonical torrent file is available locally. During an orphan
|
||||
// fight this must be checked before the canonical file because a
|
||||
// node can have cached pieces for a competing candidate at the
|
||||
// same height.
|
||||
RpcResponse::Binary(piece_data.to_vec())
|
||||
} else if let Ok(mut torrent_file) = File::open(&torrent_filename).await {
|
||||
let mut torrent_contents = Vec::new();
|
||||
if let Err(err) = torrent_file.read_to_end(&mut torrent_contents).await {
|
||||
let msg = format!("error: Failed to read torrent file: {err}")
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
let torrent = match Torrent::from_bytes(&torrent_contents).await {
|
||||
Ok(torrent) => torrent,
|
||||
Err(err) => {
|
||||
let msg = format!("error: {err}").to_string().as_bytes().to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
};
|
||||
let torrent_info_hash_bytes = match decode(&torrent.info.info_hash) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => {
|
||||
let msg = format!("error: Invalid torrent info hash: {err}")
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
};
|
||||
let torrent_info_hash = match <[u8; 16]>::try_from(torrent_info_hash_bytes.as_slice()) {
|
||||
Ok(bytes) => u128::from_le_bytes(bytes),
|
||||
Err(_) => {
|
||||
let msg = "error: Invalid torrent info hash length"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
};
|
||||
if torrent_info_hash != requested_info_hash {
|
||||
// A peer can ask for a specific candidate; reject the request
|
||||
// if our canonical torrent is for a different info hash.
|
||||
let msg = "error: Requested candidate not found"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
let pieces = torrent.info.pieces;
|
||||
|
||||
// Use the torrent piece map to locate the expected hash, read the
|
||||
// matching byte range from the block file, and verify the piece hash.
|
||||
if let Some(piece_object) = pieces
|
||||
.iter()
|
||||
.find(|piece| piece.contains_key(&requested_piece))
|
||||
{
|
||||
if let Some(expected_hash) = piece_object.get(&requested_piece) {
|
||||
let piece_length = torrent.info.piece_length as u64;
|
||||
if let Ok(mut block_file) = File::open(&block_filename).await {
|
||||
let file_size = match block_file.metadata().await {
|
||||
Ok(meta) => meta.len(),
|
||||
Err(_) => {
|
||||
let msg = "error: Error reading block file metadata"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
};
|
||||
|
||||
let start_byte = (requested_piece as u64 - 1) * piece_length;
|
||||
if start_byte >= file_size {
|
||||
let msg = "error: Requested piece is out of bounds"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
|
||||
// The last piece may be shorter than the normal
|
||||
// torrent piece length, so cap the read at EOF.
|
||||
let piece_size = std::cmp::min(piece_length, file_size - start_byte) as usize;
|
||||
let mut piece_data = vec![0u8; piece_size];
|
||||
|
||||
if block_file.seek(SeekFrom::Start(start_byte)).await.is_err() {
|
||||
let msg = "error: Error seeking block file"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
|
||||
if block_file.read_exact(&mut piece_data).await.is_err() {
|
||||
let msg = "error: Error reading block file contents"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
|
||||
let calculated_hash = skein_128_hash_bytes(&piece_data);
|
||||
// Never serve a block slice that does not match the
|
||||
// piece hash advertised in the torrent metadata.
|
||||
if &calculated_hash == expected_hash {
|
||||
return RpcResponse::Binary(piece_data);
|
||||
} else {
|
||||
let msg = "error: Hash mismatch".to_string().as_bytes().to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
} else {
|
||||
let msg = "error: Block not found".to_string().as_bytes().to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
} else {
|
||||
let msg = "error: Expected hash not found"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
} else {
|
||||
let msg = "error: Requested piece is out of bounds"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
} else {
|
||||
let msg = "error: piece not found".to_string().as_bytes().to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_block_piece(
|
||||
db: &Db,
|
||||
block_number: u32,
|
||||
requested_piece: u8,
|
||||
requested_info_hash: u128,
|
||||
) -> RpcResponse {
|
||||
// Serve a block piece either from the temporary cached-piece tree or
|
||||
// by slicing it directly from the canonical block file using torrent metadata.
|
||||
let tree = match db.open_tree("block_pieces") {
|
||||
Ok(tree) => tree,
|
||||
Err(err) => {
|
||||
let msg = format!("error: Failed to open block_pieces tree: {err}")
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
};
|
||||
let requested_info_hash_hex = encode(requested_info_hash.to_le_bytes());
|
||||
let key = format!(
|
||||
"{block_number}-{requested_info_hash_hex}-{requested_piece}"
|
||||
);
|
||||
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
block_ext,
|
||||
torrent_path,
|
||||
_wallet_path,
|
||||
block_path,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let block_filename = PathBuf::from(&block_path)
|
||||
.join(format!("{block_number}.{block_ext}"))
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let torrent_filename = PathBuf::from(&torrent_path)
|
||||
.join(format!("{block_number}.torrent"))
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
let file_exists = Path::new(&torrent_filename).exists();
|
||||
let prefix = format!("{block_number}-{requested_info_hash_hex}-");
|
||||
let pieces_exist = tree.range(prefix.as_bytes()..).peekable().peek().is_some();
|
||||
|
||||
// Once the canonical torrent exists, cached block pieces for the same
|
||||
// height can be purged so the canonical file becomes the source of truth.
|
||||
if file_exists && pieces_exist {
|
||||
remove_block_pieces_from_db(db, block_number, &requested_info_hash_hex);
|
||||
}
|
||||
|
||||
if let Some(piece_data) = tree.get(&key).ok().and_then(|result| result) {
|
||||
// Cached pieces are used for in-progress downloads before the
|
||||
// canonical torrent file is available locally.
|
||||
RpcResponse::Binary(piece_data.to_vec())
|
||||
} else if let Ok(mut torrent_file) = File::open(&torrent_filename).await {
|
||||
let mut torrent_contents = Vec::new();
|
||||
if let Err(err) = torrent_file.read_to_end(&mut torrent_contents).await {
|
||||
let msg = format!("error: Failed to read torrent file: {err}")
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
let torrent = match Torrent::from_bytes(&torrent_contents).await {
|
||||
Ok(torrent) => torrent,
|
||||
Err(err) => {
|
||||
let msg = format!("error: {err}").to_string().as_bytes().to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
};
|
||||
let torrent_info_hash_bytes = match decode(&torrent.info.info_hash) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => {
|
||||
let msg = format!("error: Invalid torrent info hash: {err}")
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
};
|
||||
let torrent_info_hash = match <[u8; 16]>::try_from(torrent_info_hash_bytes.as_slice()) {
|
||||
Ok(bytes) => u128::from_le_bytes(bytes),
|
||||
Err(_) => {
|
||||
let msg = "error: Invalid torrent info hash length"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
};
|
||||
if torrent_info_hash != requested_info_hash {
|
||||
// A peer can ask for a specific candidate; reject the request
|
||||
// if our canonical torrent is for a different info hash.
|
||||
let msg = "error: Requested candidate not found"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
let pieces = torrent.info.pieces;
|
||||
|
||||
// Use the torrent piece map to locate the expected hash, read the
|
||||
// matching byte range from the block file, and verify the piece hash.
|
||||
if let Some(piece_object) = pieces
|
||||
.iter()
|
||||
.find(|piece| piece.contains_key(&requested_piece))
|
||||
{
|
||||
if let Some(expected_hash) = piece_object.get(&requested_piece) {
|
||||
let piece_length = torrent.info.piece_length as u64;
|
||||
if let Ok(mut block_file) = File::open(&block_filename).await {
|
||||
let file_size = match block_file.metadata().await {
|
||||
Ok(meta) => meta.len(),
|
||||
Err(_) => {
|
||||
let msg = "error: Error reading block file metadata"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
};
|
||||
|
||||
let start_byte = (requested_piece as u64 - 1) * piece_length;
|
||||
if start_byte >= file_size {
|
||||
let msg = "error: Requested piece is out of bounds"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
|
||||
// The last piece may be shorter than the normal
|
||||
// torrent piece length, so cap the read at EOF.
|
||||
let piece_size = std::cmp::min(piece_length, file_size - start_byte) as usize;
|
||||
let mut piece_data = vec![0u8; piece_size];
|
||||
|
||||
if block_file.seek(SeekFrom::Start(start_byte)).await.is_err() {
|
||||
let msg = "error: Error seeking block file"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
|
||||
if block_file.read_exact(&mut piece_data).await.is_err() {
|
||||
let msg = "error: Error reading block file contents"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
|
||||
let calculated_hash = skein_128_hash_bytes(&piece_data);
|
||||
// Never serve a block slice that does not match the
|
||||
// piece hash advertised in the torrent metadata.
|
||||
if &calculated_hash == expected_hash {
|
||||
return RpcResponse::Binary(piece_data);
|
||||
} else {
|
||||
let msg = "error: Hash mismatch".to_string().as_bytes().to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
} else {
|
||||
let msg = "error: Block not found".to_string().as_bytes().to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
} else {
|
||||
let msg = "error: Expected hash not found"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
} else {
|
||||
let msg = "error: Requested piece is out of bounds"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
} else {
|
||||
let msg = "error: piece not found".to_string().as_bytes().to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,41 @@
|
|||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::Path;
|
||||
use crate::read;
|
||||
|
||||
pub async fn request_block_torrent(height: &u32) -> RpcResponse {
|
||||
// Torrent files live alongside blocks under a predictable
|
||||
// `<height>.torrent` naming convention.
|
||||
let filename = format!("{height}.torrent");
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
_block_ext,
|
||||
torrent_path,
|
||||
_wallet_path,
|
||||
_block_path,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let file_path = Path::new(&torrent_path).join(&filename);
|
||||
|
||||
if !file_path.exists() {
|
||||
let msg = format!("error: Block {height} not found")
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
|
||||
match read(&file_path).await {
|
||||
Ok(binary_data) => RpcResponse::Binary(binary_data),
|
||||
Err(_) => {
|
||||
let msg = "error: Error reading torrent file"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
RpcResponse::Binary(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::read;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::Path;
|
||||
|
||||
pub async fn request_block_torrent(height: &u32) -> RpcResponse {
|
||||
// Torrent files live alongside blocks under a predictable
|
||||
// `<height>.torrent` naming convention.
|
||||
let filename = format!("{height}.torrent");
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
_block_ext,
|
||||
torrent_path,
|
||||
_wallet_path,
|
||||
_block_path,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
let file_path = Path::new(&torrent_path).join(&filename);
|
||||
|
||||
if !file_path.exists() {
|
||||
let msg = format!("error: Block {height} not found")
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return RpcResponse::Binary(msg);
|
||||
}
|
||||
|
||||
match read(&file_path).await {
|
||||
Ok(binary_data) => RpcResponse::Binary(binary_data),
|
||||
Err(_) => {
|
||||
let msg = "error: Error reading torrent file"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
RpcResponse::Binary(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,153 +1,153 @@
|
|||
use crate::blocks::block::VRF_BLOCK_BYTES;
|
||||
use crate::common::binary_conversions::binary_to_string;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::rpc::command_maps;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom};
|
||||
use crate::File;
|
||||
use crate::io;
|
||||
use crate::PathBuf;
|
||||
|
||||
const HEADER_SIZE: u64 = VRF_BLOCK_BYTES as u64;
|
||||
|
||||
pub async fn request_transaction_by_txid(db: &Db, txid: Vec<u8>) -> RpcResponse {
|
||||
// Resolve the saved transaction bytes directly from the txid lookup
|
||||
// tree and the referenced block file.
|
||||
match lookup_transaction_location(db, txid).await {
|
||||
Ok((_block, _position, block_filename)) => {
|
||||
let bytes = calculate_offset(&block_filename, _position).await;
|
||||
match bytes {
|
||||
Some(vec) => RpcResponse::Binary(vec),
|
||||
None => {
|
||||
let msg = "error: Error parsing block".to_string().as_bytes().to_vec();
|
||||
RpcResponse::Binary(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(msg) => RpcResponse::Binary(msg.into_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_transaction_by_txid_with_block(db: &Db, txid: Vec<u8>) -> RpcResponse {
|
||||
// Some callers need the block number alongside the raw transaction
|
||||
// bytes, so this variant prefixes the payload with the block height.
|
||||
match lookup_transaction_location(db, txid).await {
|
||||
Ok((block, position, block_filename)) => {
|
||||
let bytes = calculate_offset(&block_filename, position).await;
|
||||
match bytes {
|
||||
Some(vec) => {
|
||||
let mut response = Vec::with_capacity(4 + vec.len());
|
||||
response.extend_from_slice(&(block as u32).to_le_bytes());
|
||||
response.extend_from_slice(&vec);
|
||||
RpcResponse::Binary(response)
|
||||
}
|
||||
None => {
|
||||
let msg = "error: Error parsing block".to_string().as_bytes().to_vec();
|
||||
RpcResponse::Binary(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(msg) => RpcResponse::Binary(msg.into_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn lookup_transaction_location(db: &Db, txid: Vec<u8>) -> Result<(u64, u32, String), String> {
|
||||
// The txid tree stores `block:index`, which is enough to locate the
|
||||
// transaction inside the saved block file on disk.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let value = match tree.get(txid) {
|
||||
Ok(Some(result)) => result.to_vec(),
|
||||
Ok(None) => {
|
||||
return Err("error: Key not found".to_string());
|
||||
}
|
||||
Err(_) => {
|
||||
return Err("error: Errpr retrieving value".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let value_str = binary_to_string(value.to_vec());
|
||||
|
||||
// Stored txid locations are saved as ASCII `height:index`.
|
||||
let parts: Vec<&str> = value_str.split(':').collect();
|
||||
|
||||
let block: u64 = parts[0].parse().unwrap_or_default();
|
||||
|
||||
let position: u32 = parts[1].parse().unwrap_or_default();
|
||||
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
block_ext,
|
||||
_torrent_path,
|
||||
_wallet_path,
|
||||
block_path,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
|
||||
let block_filename = PathBuf::from(block_path)
|
||||
.join(format!("{block}.{block_ext}"))
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
Ok((block, position, block_filename))
|
||||
}
|
||||
|
||||
async fn read_transaction_type(file_path: &str, position: u64) -> Option<u8> {
|
||||
// Transaction offsets are located by repeatedly reading the type byte
|
||||
// so the fixed encoded size for each saved transaction can be applied.
|
||||
let mut file = match File::open(file_path).await {
|
||||
Ok(file) => file,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
file.seek(SeekFrom::Start(position)).await.ok()?;
|
||||
|
||||
let mut transaction_type_byte = [0u8; 1];
|
||||
file.read_exact(&mut transaction_type_byte).await.ok()?;
|
||||
|
||||
Some(transaction_type_byte[0])
|
||||
}
|
||||
|
||||
async fn calculate_offset(file_path: &str, position: u32) -> Option<Vec<u8>> {
|
||||
// Walk forward through the serialized block body until the requested
|
||||
// transaction index is reached, then read exactly that transaction.
|
||||
let mut total_bytes_to_skip: u64 = HEADER_SIZE;
|
||||
|
||||
let mut current_position: u32 = 1;
|
||||
|
||||
let mut transaction_type = read_transaction_type(file_path, HEADER_SIZE).await?;
|
||||
|
||||
while current_position < position {
|
||||
// Transaction bodies are fixed-size by type, so the type byte at
|
||||
// each offset tells us how far to jump to reach the next record.
|
||||
let size = command_maps::get_bytes(transaction_type) as u64;
|
||||
|
||||
total_bytes_to_skip += size;
|
||||
|
||||
transaction_type = read_transaction_type(file_path, total_bytes_to_skip).await?;
|
||||
|
||||
current_position += 1;
|
||||
}
|
||||
|
||||
let size = command_maps::get_bytes(transaction_type) as u64;
|
||||
|
||||
let mut file = match File::open(file_path).await {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
file.seek(io::SeekFrom::Start(total_bytes_to_skip))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let mut transaction_bytes = vec![0u8; size as usize];
|
||||
file.read_exact(&mut transaction_bytes).await.ok()?;
|
||||
|
||||
// Returned bytes include the transaction type byte at the front so
|
||||
// callers can parse the payload without extra lookup state.
|
||||
Some(transaction_bytes)
|
||||
}
|
||||
use crate::blocks::block::VRF_BLOCK_BYTES;
|
||||
use crate::common::binary_conversions::binary_to_string;
|
||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||
use crate::io;
|
||||
use crate::rpc::command_maps;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
use crate::File;
|
||||
use crate::PathBuf;
|
||||
use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom};
|
||||
|
||||
const HEADER_SIZE: u64 = VRF_BLOCK_BYTES as u64;
|
||||
|
||||
pub async fn request_transaction_by_txid(db: &Db, txid: Vec<u8>) -> RpcResponse {
|
||||
// Resolve the saved transaction bytes directly from the txid lookup
|
||||
// tree and the referenced block file.
|
||||
match lookup_transaction_location(db, txid).await {
|
||||
Ok((_block, _position, block_filename)) => {
|
||||
let bytes = calculate_offset(&block_filename, _position).await;
|
||||
match bytes {
|
||||
Some(vec) => RpcResponse::Binary(vec),
|
||||
None => {
|
||||
let msg = "error: Error parsing block".to_string().as_bytes().to_vec();
|
||||
RpcResponse::Binary(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(msg) => RpcResponse::Binary(msg.into_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_transaction_by_txid_with_block(db: &Db, txid: Vec<u8>) -> RpcResponse {
|
||||
// Some callers need the block number alongside the raw transaction
|
||||
// bytes, so this variant prefixes the payload with the block height.
|
||||
match lookup_transaction_location(db, txid).await {
|
||||
Ok((block, position, block_filename)) => {
|
||||
let bytes = calculate_offset(&block_filename, position).await;
|
||||
match bytes {
|
||||
Some(vec) => {
|
||||
let mut response = Vec::with_capacity(4 + vec.len());
|
||||
response.extend_from_slice(&(block as u32).to_le_bytes());
|
||||
response.extend_from_slice(&vec);
|
||||
RpcResponse::Binary(response)
|
||||
}
|
||||
None => {
|
||||
let msg = "error: Error parsing block".to_string().as_bytes().to_vec();
|
||||
RpcResponse::Binary(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(msg) => RpcResponse::Binary(msg.into_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn lookup_transaction_location(db: &Db, txid: Vec<u8>) -> Result<(u64, u32, String), String> {
|
||||
// The txid tree stores `block:index`, which is enough to locate the
|
||||
// transaction inside the saved block file on disk.
|
||||
let tree = db.open_tree("txid").unwrap();
|
||||
let value = match tree.get(txid) {
|
||||
Ok(Some(result)) => result.to_vec(),
|
||||
Ok(None) => {
|
||||
return Err("error: Key not found".to_string());
|
||||
}
|
||||
Err(_) => {
|
||||
return Err("error: Errpr retrieving value".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let value_str = binary_to_string(value.to_vec());
|
||||
|
||||
// Stored txid locations are saved as ASCII `height:index`.
|
||||
let parts: Vec<&str> = value_str.split(':').collect();
|
||||
|
||||
let block: u64 = parts[0].parse().unwrap_or_default();
|
||||
|
||||
let position: u32 = parts[1].parse().unwrap_or_default();
|
||||
|
||||
let (
|
||||
_network_name,
|
||||
_padded_base_coin,
|
||||
block_ext,
|
||||
_torrent_path,
|
||||
_wallet_path,
|
||||
block_path,
|
||||
_db_path,
|
||||
_balance_path,
|
||||
_log_path,
|
||||
) = block_extension_and_paths();
|
||||
|
||||
let block_filename = PathBuf::from(block_path)
|
||||
.join(format!("{block}.{block_ext}"))
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
Ok((block, position, block_filename))
|
||||
}
|
||||
|
||||
async fn read_transaction_type(file_path: &str, position: u64) -> Option<u8> {
|
||||
// Transaction offsets are located by repeatedly reading the type byte
|
||||
// so the fixed encoded size for each saved transaction can be applied.
|
||||
let mut file = match File::open(file_path).await {
|
||||
Ok(file) => file,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
file.seek(SeekFrom::Start(position)).await.ok()?;
|
||||
|
||||
let mut transaction_type_byte = [0u8; 1];
|
||||
file.read_exact(&mut transaction_type_byte).await.ok()?;
|
||||
|
||||
Some(transaction_type_byte[0])
|
||||
}
|
||||
|
||||
async fn calculate_offset(file_path: &str, position: u32) -> Option<Vec<u8>> {
|
||||
// Walk forward through the serialized block body until the requested
|
||||
// transaction index is reached, then read exactly that transaction.
|
||||
let mut total_bytes_to_skip: u64 = HEADER_SIZE;
|
||||
|
||||
let mut current_position: u32 = 1;
|
||||
|
||||
let mut transaction_type = read_transaction_type(file_path, HEADER_SIZE).await?;
|
||||
|
||||
while current_position < position {
|
||||
// Transaction bodies are fixed-size by type, so the type byte at
|
||||
// each offset tells us how far to jump to reach the next record.
|
||||
let size = command_maps::get_bytes(transaction_type) as u64;
|
||||
|
||||
total_bytes_to_skip += size;
|
||||
|
||||
transaction_type = read_transaction_type(file_path, total_bytes_to_skip).await?;
|
||||
|
||||
current_position += 1;
|
||||
}
|
||||
|
||||
let size = command_maps::get_bytes(transaction_type) as u64;
|
||||
|
||||
let mut file = match File::open(file_path).await {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
file.seek(io::SeekFrom::Start(total_bytes_to_skip))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let mut transaction_bytes = vec![0u8; size as usize];
|
||||
file.read_exact(&mut transaction_bytes).await.ok()?;
|
||||
|
||||
// Returned bytes include the transaction type byte at the front so
|
||||
// callers can parse the payload without extra lookup state.
|
||||
Some(transaction_bytes)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ use crate::records::memory::enums::ClientType;
|
|||
use crate::records::memory::response_channels::generate_uid;
|
||||
use crate::rpc::command_maps::RPC_SUBMIT_TRANSACTION;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::torrent::torrenting_system::get_nodes::get_nodes_from_memory;
|
||||
use crate::sled::Db;
|
||||
use crate::torrent::torrenting_system::get_nodes::get_nodes_from_memory;
|
||||
|
||||
async fn broadcast_tx(tx_bytes: Vec<u8>) {
|
||||
// Broadcast newly accepted mempool transactions only to miner peers,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::wallets::structures::Wallet;
|
||||
use crate::sled::Db;
|
||||
use crate::wallets::structures::Wallet;
|
||||
|
||||
pub async fn unblock_peer(
|
||||
db: &Db,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use crate::rpc::read_bytes_from_stream;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::rpc::server::flood_protection::MAX_TORRENT_METADATA_BYTES;
|
||||
use crate::torrent::structs::Torrent;
|
||||
use crate::sled::Db;
|
||||
use crate::torrent::structs::Torrent;
|
||||
use crate::Arc;
|
||||
use crate::Mutex;
|
||||
use crate::TcpStream;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue