Contractless/src/records/memory/mempool/selection.rs

1044 lines
47 KiB
Rust
Raw Normal View History

2026-05-24 17:56:57 +00:00
use super::*;
use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects};
2026-06-12 16:27:28 +00:00
use crate::records::record_chain::wallet_tx_index::index_wallet_transaction;
fn index_selected_wallet_transaction(
pending_effects: &mut PendingEffects,
addresses: &[&str],
block_height: u32,
entry_index: usize,
txid: &[u8],
) -> Result<()> {
let entry_index =
u32::try_from(entry_index).map_err(|_| anyhow!("Wallet transaction index overflowed"))?;
index_wallet_transaction(pending_effects, addresses, block_height, entry_index, txid)
.map_err(|err| anyhow!(err))
}
2026-05-26 06:24:57 +00:00
pub async fn select_transactions_for_block(limit: i64) -> Result<SelectedMempoolBatch> {
// Pull the highest-priority unprocessed rows across all mempool
// tables, keeping the original bytes for block-file streaming later.
2026-06-23 15:17:12 +00:00
let rows =
pg_query(
2026-05-26 06:24:57 +00:00
r#"
SELECT * FROM (
SELECT
'transfer'::TEXT AS kind, id, fee AS priority_fee, time,
sender, receiver, coin, value, nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2,
NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1,
NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1,
NULL::BIGINT AS tip2, NULL::TEXT AS sender2, NULL::TEXT AS loan_coin,
NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, NULL::TEXT AS collateral,
NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower,
NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address,
hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM transfer
WHERE processed = false
UNION ALL
SELECT
'token'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
number, creator, ticker, NULL::TEXT AS nft_name, NULL::SMALLINT AS series,
NULL::BIGINT AS count, NULL::TEXT AS advertiser, NULL::BIGINT AS fee1,
NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2,
NULL::BIGINT AS value1, NULL::BIGINT AS value2, NULL::TEXT AS sender1,
NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, NULL::TEXT AS sender2,
NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, NULL::TEXT AS lender,
NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower,
NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address,
hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, hard_limit, original
FROM token
WHERE processed = false
UNION ALL
SELECT
'issue_token'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
number, creator, ticker, NULL::TEXT AS nft_name, NULL::SMALLINT AS series,
NULL::BIGINT AS count, NULL::TEXT AS advertiser, NULL::BIGINT AS fee1,
NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2,
NULL::BIGINT AS value1, NULL::BIGINT AS value2, NULL::TEXT AS sender1,
NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, NULL::TEXT AS sender2,
NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, NULL::TEXT AS lender,
NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower,
NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address,
hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM issue_token
WHERE processed = false
UNION ALL
SELECT
'burn'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, coin, value, nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2,
NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2,
NULL::BIGINT AS value1, NULL::BIGINT AS value2, NULL::TEXT AS sender1,
NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, NULL::TEXT AS sender2,
NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, NULL::TEXT AS lender,
NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower,
NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, address,
hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM burn
WHERE processed = false
UNION ALL
SELECT
'nft'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, creator, NULL::TEXT AS ticker, nft_name, series, count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2,
NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1,
NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1,
NULL::BIGINT AS tip2, NULL::TEXT AS sender2, NULL::TEXT AS loan_coin,
NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, NULL::TEXT AS collateral,
NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower,
NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address,
hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM nft
WHERE processed = false
UNION ALL
SELECT
'marketing'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1,
NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, NULL::BIGINT AS value2,
NULL::TEXT AS sender1, NULL::BIGINT AS tip1, NULL::BIGINT AS tip2,
NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount,
NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount,
NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash,
NULL::TEXT AS address, hash, signature AS signature1, NULL::TEXT AS signature2,
NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM marketing
WHERE processed = false
UNION ALL
SELECT
'vanity_address'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1,
NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, NULL::BIGINT AS value2,
NULL::TEXT AS sender1, NULL::BIGINT AS tip1, NULL::BIGINT AS tip2,
NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount,
NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount,
NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash,
address, hash, signature AS signature1, NULL::TEXT AS signature2,
vanity_address AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM vanity_address
WHERE processed = false
UNION ALL
SELECT
'swap'::TEXT AS kind, id, GREATEST(fee1, fee2) AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, fee1, fee2, ticker1, nft_series1, ticker2, nft_series2, value1, value2,
sender1, tip1, tip2, sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount,
NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount,
NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash,
NULL::TEXT AS address, hash, signature1, signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM swap
WHERE processed = false
UNION ALL
SELECT
'loan_contract'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2,
NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1,
NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1,
NULL::BIGINT AS tip2, NULL::TEXT AS sender2, loan_coin, loan_amount, lender,
collateral, collateral_amount, borrower, NULL::BIGINT AS payback_amount,
NULL::TEXT AS contract_hash, NULL::TEXT AS address, hash, signature1, signature2,
txid AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM loan_contract
WHERE processed = false
UNION ALL
SELECT
'loan_payment'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2,
NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1,
NULL::BIGINT AS value2, NULL::TEXT AS sender1, tip AS tip1, NULL::BIGINT AS tip2,
NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount,
NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount,
NULL::TEXT AS borrower, payback_amount, contract_hash, address, hash,
signature AS signature1, NULL::TEXT AS signature2, txid AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM loan_payment
WHERE processed = false
UNION ALL
SELECT
'collateral_claim'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2,
NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1,
NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1,
NULL::BIGINT AS tip2, NULL::TEXT AS sender2, NULL::TEXT AS loan_coin,
NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, NULL::TEXT AS collateral,
NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower,
NULL::BIGINT AS payback_amount, contract_hash, address, hash,
signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM collateral_claim
WHERE processed = false
) AS combined
ORDER BY priority_fee DESC, time ASC, kind ASC, id ASC
LIMIT $1
"#,
&[&limit],
)
.await?;
2026-05-24 17:56:57 +00:00
let mut transactions = Vec::with_capacity(rows.len());
let mut originals = Vec::with_capacity(rows.len());
for row in rows {
// Preserve the original serialized transaction bytes so the miner can
// write exactly what entered the mempool.
originals.push(row.get("original"));
// The UNION query flattens every mempool table into one row shape;
// this tag restores the correct selected-transaction variant.
let kind: String = row.get("kind");
let transaction = match kind.as_str() {
2026-05-26 06:24:57 +00:00
"transfer" => SelectedMempoolTransaction::Transfer {
id: row.get("id"),
fee: row.get("priority_fee"),
sender: required_string(&row, "sender")?,
value: row.get("value"),
coin: required_string(&row, "coin")?,
nft_series: row.get("nft_series"),
receiver: required_string(&row, "receiver")?,
hash: row.get("hash"),
},
"token" => SelectedMempoolTransaction::Token {
id: row.get("id"),
fee: row.get("priority_fee"),
creator: required_string(&row, "creator")?,
number: row.get("number"),
ticker: required_string(&row, "ticker")?,
hash: row.get("hash"),
},
"issue_token" => SelectedMempoolTransaction::IssueToken {
id: row.get("id"),
fee: row.get("priority_fee"),
creator: required_string(&row, "creator")?,
number: row.get("number"),
ticker: required_string(&row, "ticker")?,
hash: row.get("hash"),
},
"burn" => SelectedMempoolTransaction::Burn {
id: row.get("id"),
fee: row.get("priority_fee"),
address: required_string(&row, "address")?,
coin: required_string(&row, "coin")?,
nft_series: row.get("nft_series"),
value: row.get("value"),
hash: row.get("hash"),
},
"nft" => SelectedMempoolTransaction::Nft {
id: row.get("id"),
fee: row.get("priority_fee"),
creator: required_string(&row, "creator")?,
nft_name: required_string(&row, "nft_name")?,
series: row.get("series"),
count: row.get("count"),
hash: row.get("hash"),
},
"marketing" => SelectedMempoolTransaction::Marketing {
id: row.get("id"),
fee: row.get("priority_fee"),
advertiser: required_string(&row, "advertiser")?,
hash: row.get("hash"),
},
"vanity_address" => SelectedMempoolTransaction::Vanity {
id: row.get("id"),
fee: row.get("priority_fee"),
address: required_string(&row, "address")?,
vanity_address: required_string(&row, "stored_txid")?,
hash: row.get("hash"),
},
"swap" => SelectedMempoolTransaction::Swap {
id: row.get("id"),
fee1: row.get("fee1"),
fee2: row.get("fee2"),
ticker1: required_string(&row, "ticker1")?,
nft_series1: row.get("nft_series1"),
ticker2: required_string(&row, "ticker2")?,
nft_series2: row.get("nft_series2"),
value1: row.get("value1"),
value2: row.get("value2"),
sender1: required_string(&row, "sender1")?,
tip1: row.get("tip1"),
tip2: row.get("tip2"),
sender2: required_string(&row, "sender2")?,
hash: row.get("hash"),
},
"loan_contract" => SelectedMempoolTransaction::Lender {
id: row.get("id"),
fee: row.get("priority_fee"),
loan_coin: required_string(&row, "loan_coin")?,
loan_amount: row.get("loan_amount"),
lender: required_string(&row, "lender")?,
collateral: required_string(&row, "collateral")?,
collateral_amount: row.get("collateral_amount"),
borrower: required_string(&row, "borrower")?,
txid: required_string(&row, "stored_txid")?,
hash: row.get("hash"),
},
"loan_payment" => SelectedMempoolTransaction::Borrower {
id: row.get("id"),
fee: row.get("priority_fee"),
payback_amount: row.get("payback_amount"),
contract_hash: required_string(&row, "contract_hash")?,
address: required_string(&row, "address")?,
tip: row.get("tip1"),
hash: row.get("hash"),
},
"collateral_claim" => SelectedMempoolTransaction::Collateral {
id: row.get("id"),
fee: row.get("priority_fee"),
address: required_string(&row, "address")?,
contract_hash: required_string(&row, "contract_hash")?,
hash: row.get("hash"),
},
2026-05-24 17:56:57 +00:00
_ => return Err(anyhow!("Unsupported mempool kind {kind}")),
};
2026-05-26 06:24:57 +00:00
transactions.push(transaction);
}
Ok(SelectedMempoolBatch {
transactions,
originals,
})
}
2026-05-24 17:56:57 +00:00
pub async fn apply_selected_transaction_math(
batch: &SelectedMempoolBatch,
db: &Db,
miner: String,
block_number: u32,
start_index: usize,
pending_effects: &mut PendingEffects,
2026-06-24 17:32:25 +00:00
) -> Result<()> {
2026-05-24 17:56:57 +00:00
// Net balance deltas are collected first so multiple changes to the same
// address/asset pair become one balance-sheet write.
let mut balance_changes: HashMap<BalanceKey, i64> = HashMap::new();
2026-05-26 06:24:57 +00:00
let miner_address = address_key_bytes(db, &miner);
let base_coin = BASECOIN.as_bytes().to_vec();
let mut tx_index = start_index;
2026-05-24 17:56:57 +00:00
for tx in &batch.transactions {
// Transaction index is stored with the block number for later txid
// lookups inside the saved block payload.
tx_index = tx_index.wrapping_add(1);
match tx {
2026-05-26 06:24:57 +00:00
SelectedMempoolTransaction::Transfer {
fee,
sender,
value,
coin,
nft_series,
receiver,
hash,
..
2026-05-24 17:56:57 +00:00
} => {
// Transfers move the asset, charge the base fee, and credit
// that fee to the miner including this transaction.
let transfer_asset = nft_asset_name(coin, *nft_series as u32);
2026-05-26 06:24:57 +00:00
add_balance_change(db, &mut balance_changes, sender, &transfer_asset, -*value);
add_balance_change(db, &mut balance_changes, receiver, &transfer_asset, *value);
add_balance_change(db, &mut balance_changes, sender, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
2026-05-24 17:56:57 +00:00
let nft_tree = db.open_tree("nfts")?;
if nft_tree.contains_key(transfer_asset.as_bytes())? {
pending_effects.append_tree(
"nft_history",
transfer_asset.as_bytes().to_vec(),
decode(hash)?,
);
}
pending_effects.set_tree(
"txid",
decode(hash)?,
format!("{block_number}:{tx_index}").into_bytes(),
);
2026-06-12 16:27:28 +00:00
index_selected_wallet_transaction(
pending_effects,
2026-06-19 18:36:01 +00:00
&[sender, receiver],
2026-06-12 16:27:28 +00:00
block_number,
tx_index,
&decode(hash)?,
)?;
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
SelectedMempoolTransaction::Token {
fee,
creator,
number,
ticker,
hash,
..
2026-05-24 17:56:57 +00:00
} => {
// Token creation mints the initial supply and records metadata
// used by later issue and burn validation.
add_balance_change(db, &mut balance_changes, creator, ticker, *number);
2026-05-26 06:24:57 +00:00
add_balance_change(db, &mut balance_changes, creator, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
2026-05-24 17:56:57 +00:00
let number_bytes = (*number as u64).to_le_bytes();
2026-05-26 06:24:57 +00:00
pending_effects.set_tree(
"tokens",
ticker.as_bytes().to_vec(),
number_bytes.to_vec(),
);
// Local mined blocks need to persist the token hard-limit
// metadata just like the downloaded-block save path does.
2026-06-12 16:27:28 +00:00
let client_handle = db_client().await?;
let client = client_handle.as_ref();
2026-05-26 06:24:57 +00:00
let token_row = client
.query_opt(
"SELECT hard_limit FROM token WHERE hash = $1 LIMIT 1",
&[hash],
)
.await?;
let hard_limit = token_row
.and_then(|row| row.try_get::<_, Option<i16>>("hard_limit").ok().flatten())
.unwrap_or(1) as u8;
pending_effects.set_tree(
"token_limits",
ticker.as_bytes().to_vec(),
vec![hard_limit],
);
2026-05-24 17:56:57 +00:00
pending_effects.set_tree(
"token_origins",
ticker.as_bytes().to_vec(),
hash.as_bytes().to_vec(),
);
2026-05-26 06:24:57 +00:00
pending_effects.append_tree(
"token_history",
ticker.as_bytes().to_vec(),
decode(hash)?,
);
2026-05-24 17:56:57 +00:00
pending_effects.set_tree(
"txid",
decode(hash)?,
format!("{block_number}:{tx_index}").into_bytes(),
);
2026-06-12 16:27:28 +00:00
index_selected_wallet_transaction(
pending_effects,
2026-06-19 18:36:01 +00:00
&[creator],
2026-06-12 16:27:28 +00:00
block_number,
tx_index,
&decode(hash)?,
)?;
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
SelectedMempoolTransaction::IssueToken {
fee,
creator,
number,
ticker,
hash,
..
} => {
add_balance_change(db, &mut balance_changes, creator, ticker, *number);
add_balance_change(db, &mut balance_changes, creator, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
// Additional issuance increases the live fungible supply without
// changing the original token metadata entries.
2026-05-24 17:56:57 +00:00
pending_effects.add_token_supply(ticker, *number as u64);
2026-05-26 06:24:57 +00:00
pending_effects.append_tree(
"token_history",
ticker.as_bytes().to_vec(),
decode(hash)?,
);
2026-05-24 17:56:57 +00:00
pending_effects.set_tree(
"txid",
decode(hash)?,
format!("{block_number}:{tx_index}").into_bytes(),
);
2026-06-12 16:27:28 +00:00
index_selected_wallet_transaction(
pending_effects,
2026-06-19 18:36:01 +00:00
&[creator],
2026-06-12 16:27:28 +00:00
block_number,
tx_index,
&decode(hash)?,
)?;
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
SelectedMempoolTransaction::Burn {
fee,
address,
coin,
nft_series,
value,
hash,
..
} => {
let burned_asset = nft_asset_name(coin, *nft_series as u32);
add_balance_change(db, &mut balance_changes, address, &burned_asset, -*value);
add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
// Live NFT state removes burned assets, while fungible token
// state reduces the stored supply by the burned amount.
2026-05-24 17:56:57 +00:00
pending_effects.burn_asset_state(&burned_asset, coin, decode(hash)?, *value as u64);
pending_effects.set_tree(
"txid",
decode(hash)?,
format!("{block_number}:{tx_index}").into_bytes(),
);
2026-06-12 16:27:28 +00:00
index_selected_wallet_transaction(
pending_effects,
2026-06-19 18:36:01 +00:00
&[address],
2026-06-12 16:27:28 +00:00
block_number,
tx_index,
&decode(hash)?,
)?;
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
SelectedMempoolTransaction::Nft {
fee,
creator,
nft_name,
series,
count,
hash,
..
2026-05-24 17:56:57 +00:00
} => {
// Series-one NFT creation expands into numbered assets; later
// series use the given NFT name as the asset key.
if *series == 1 {
2026-05-26 06:24:57 +00:00
for item_number in 1..=*count {
let nft_save_name = nft_asset_name(nft_name, item_number as u32);
add_balance_change(
db,
&mut balance_changes,
creator,
&nft_save_name,
NFT_UNIT,
);
2026-05-24 17:56:57 +00:00
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(),
decode(hash)?,
);
pending_effects.append_tree(
"nft_history",
nft_save_name.into_bytes(),
decode(hash)?,
);
}
} else {
add_balance_change(db, &mut balance_changes, creator, nft_name, NFT_UNIT);
pending_effects.set_tree("nfts", nft_name.as_bytes().to_vec(), b"1".to_vec());
2026-05-26 06:24:57 +00:00
pending_effects.set_tree(
"nft_origins",
nft_name.as_bytes().to_vec(),
decode(hash)?,
);
pending_effects.append_tree(
"nft_history",
nft_name.as_bytes().to_vec(),
decode(hash)?,
);
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
add_balance_change(db, &mut balance_changes, creator, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
2026-05-24 17:56:57 +00:00
pending_effects.set_tree(
"txid",
decode(hash)?,
format!("{block_number}:{tx_index}").into_bytes(),
);
2026-06-12 16:27:28 +00:00
index_selected_wallet_transaction(
pending_effects,
2026-06-19 18:36:01 +00:00
&[creator],
2026-06-12 16:27:28 +00:00
block_number,
tx_index,
&decode(hash)?,
)?;
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
SelectedMempoolTransaction::Marketing {
fee,
advertiser,
hash,
..
} => {
add_balance_change(db, &mut balance_changes, advertiser, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
2026-05-24 17:56:57 +00:00
pending_effects.set_tree(
"txid",
decode(hash)?,
format!("{block_number}:{tx_index}").into_bytes(),
);
2026-06-12 16:27:28 +00:00
index_selected_wallet_transaction(
pending_effects,
2026-06-19 18:36:01 +00:00
&[advertiser],
2026-06-12 16:27:28 +00:00
block_number,
tx_index,
&decode(hash)?,
)?;
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
SelectedMempoolTransaction::Vanity {
fee,
address,
vanity_address,
hash,
..
2026-05-24 17:56:57 +00:00
} => {
// Vanity transactions update the alias registry during the
// final block-effect commit so rollback can restore it.
pending_effects.update_vanity(address, vanity_address, hash);
add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
2026-05-26 06:24:57 +00:00
miner_address.clone(),
base_coin.clone(),
*fee,
);
2026-05-24 17:56:57 +00:00
pending_effects.set_tree(
"txid",
decode(hash)?,
format!("{block_number}:{tx_index}").into_bytes(),
);
2026-06-12 16:27:28 +00:00
index_selected_wallet_transaction(
pending_effects,
2026-06-19 18:36:01 +00:00
&[address],
2026-06-12 16:27:28 +00:00
block_number,
tx_index,
&decode(hash)?,
)?;
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
SelectedMempoolTransaction::Swap {
fee1,
fee2,
ticker1,
nft_series1,
ticker2,
nft_series2,
value1,
value2,
sender1,
tip1,
tip2,
sender2,
hash,
..
2026-05-24 17:56:57 +00:00
} => {
// Swaps debit both sides first, then credit received assets,
// miner fees, and optional asset tips.
let asset1 = nft_asset_name(ticker1, *nft_series1 as u32);
2026-05-26 06:24:57 +00:00
let asset2 = nft_asset_name(ticker2, *nft_series2 as u32);
add_balance_change(db, &mut balance_changes, sender1, &asset1, -*value1);
add_balance_change(db, &mut balance_changes, sender1, &asset1, -*tip1);
add_balance_change(db, &mut balance_changes, sender2, &asset2, -*value2);
add_balance_change(db, &mut balance_changes, sender2, &asset2, -*tip2);
add_balance_change(db, &mut balance_changes, sender2, &asset1, *value1);
add_balance_change(db, &mut balance_changes, sender1, &asset2, *value2);
add_balance_change(db, &mut balance_changes, sender1, &BASECOIN, -*fee1);
add_balance_change(db, &mut balance_changes, sender2, &BASECOIN, -*fee2);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee1,
);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee2,
);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
asset1.as_bytes().to_vec(),
*tip1,
);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
asset2.as_bytes().to_vec(),
*tip2,
);
2026-05-24 17:56:57 +00:00
let nft_tree = db.open_tree("nfts")?;
if nft_tree.contains_key(asset1.as_bytes())? {
pending_effects.append_tree(
"nft_history",
asset1.as_bytes().to_vec(),
decode(hash)?,
);
}
if nft_tree.contains_key(asset2.as_bytes())? {
pending_effects.append_tree(
"nft_history",
asset2.as_bytes().to_vec(),
decode(hash)?,
);
}
pending_effects.set_tree(
"txid",
decode(hash)?,
format!("{block_number}:{tx_index}").into_bytes(),
);
2026-06-12 16:27:28 +00:00
index_selected_wallet_transaction(
pending_effects,
2026-06-19 18:36:01 +00:00
&[sender1, sender2],
2026-06-12 16:27:28 +00:00
block_number,
tx_index,
&decode(hash)?,
)?;
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
SelectedMempoolTransaction::Lender {
fee,
loan_coin,
loan_amount,
lender,
collateral,
collateral_amount,
borrower,
txid,
hash,
..
2026-05-24 17:56:57 +00:00
} => {
// Loan creation moves the loan asset to the borrower and locks
// collateral under a contract-specific holding key.
add_balance_change(db, &mut balance_changes, lender, &BASECOIN, -*fee);
2026-05-26 06:24:57 +00:00
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
add_balance_change(db, &mut balance_changes, lender, loan_coin, -*loan_amount);
add_balance_change(db, &mut balance_changes, borrower, loan_coin, *loan_amount);
add_balance_change(
db,
&mut balance_changes,
borrower,
collateral,
-*collateral_amount,
);
let collateral_holding = format!("collateral_{txid}");
add_balance_change(
db,
&mut balance_changes,
&collateral_holding,
collateral,
*collateral_amount,
);
2026-05-24 17:56:57 +00:00
pending_effects.set_tree("loan", decode(txid)?, b"true".to_vec());
let nft_tree = db.open_tree("nfts")?;
if nft_tree.contains_key(collateral.as_bytes())? {
pending_effects.append_tree(
"nft_history",
collateral.as_bytes().to_vec(),
decode(hash)?,
);
}
if nft_tree.contains_key(loan_coin.as_bytes())? {
pending_effects.append_tree(
"nft_history",
loan_coin.as_bytes().to_vec(),
decode(hash)?,
);
}
pending_effects.set_tree(
"txid",
decode(hash)?,
format!("{block_number}:{tx_index}").into_bytes(),
);
2026-06-12 16:27:28 +00:00
index_selected_wallet_transaction(
pending_effects,
2026-06-19 18:36:01 +00:00
&[lender, borrower],
2026-06-12 16:27:28 +00:00
block_number,
tx_index,
&decode(hash)?,
)?;
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
SelectedMempoolTransaction::Borrower {
fee,
payback_amount,
contract_hash,
address,
tip,
hash,
..
2026-05-24 17:56:57 +00:00
} => {
// Loan payments resolve the contract, then move repayment from
// borrower to lender while paying any tip to the miner.
let (loan_coin, lender) = resolve_loan_details(db, contract_hash).await?;
2026-05-26 06:24:57 +00:00
add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
add_balance_change_bytes(
&mut balance_changes,
address_key_bytes(db, address),
loan_coin.clone(),
-*payback_amount,
);
add_balance_change_bytes(
&mut balance_changes,
lender.clone(),
loan_coin.clone(),
*payback_amount,
);
add_balance_change_bytes(
&mut balance_changes,
address_key_bytes(db, address),
loan_coin.clone(),
-*tip,
);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
loan_coin.clone(),
*tip,
);
pending_effects
.append_contract_payment(decode(contract_hash)?, *payback_amount as u64);
2026-05-24 17:56:57 +00:00
let loan_coin_name = binary_to_string(loan_coin.clone());
let nft_tree = db.open_tree("nfts")?;
if nft_tree.contains_key(loan_coin_name.as_bytes())? {
pending_effects.append_tree(
"nft_history",
loan_coin_name.as_bytes().to_vec(),
decode(hash)?,
);
}
pending_effects.set_tree(
"txid",
decode(hash)?,
format!("{block_number}:{tx_index}").into_bytes(),
);
2026-06-12 16:27:28 +00:00
let lender_address = binary_to_string(lender.clone());
index_selected_wallet_transaction(
pending_effects,
2026-06-19 18:36:01 +00:00
&[address, &lender_address],
2026-06-12 16:27:28 +00:00
block_number,
tx_index,
&decode(hash)?,
)?;
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
SelectedMempoolTransaction::Collateral {
fee,
address,
contract_hash,
hash,
..
2026-05-24 17:56:57 +00:00
} => {
// Collateral claims release the contract holding to the claimant
// and mark the loan as closed.
let (collateral, collateral_amount) =
2026-05-26 06:24:57 +00:00
resolve_collateral_details(db, contract_hash).await?;
let collateral_holding = format!("collateral_{contract_hash}");
add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
add_balance_change_bytes(
&mut balance_changes,
collateral_holding.as_bytes().to_vec(),
collateral.clone(),
-collateral_amount,
);
add_balance_change_bytes(
&mut balance_changes,
address_key_bytes(db, address),
collateral.clone(),
collateral_amount,
);
2026-05-24 17:56:57 +00:00
pending_effects.set_tree("loan", decode(contract_hash)?, b"false".to_vec());
let collateral_name = binary_to_string(collateral.clone());
let nft_tree = db.open_tree("nfts")?;
if nft_tree.contains_key(collateral_name.as_bytes())? {
pending_effects.append_tree(
"nft_history",
collateral_name.as_bytes().to_vec(),
decode(hash)?,
);
}
pending_effects.set_tree(
"txid",
decode(hash)?,
format!("{block_number}:{tx_index}").into_bytes(),
);
2026-06-12 16:27:28 +00:00
index_selected_wallet_transaction(
pending_effects,
2026-06-19 18:36:01 +00:00
&[address],
2026-06-12 16:27:28 +00:00
block_number,
tx_index,
&decode(hash)?,
)?;
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
}
}
2026-05-24 17:56:57 +00:00
for (key, change) in balance_changes {
// The balance sheet stores string keys, so convert the internal byte
// keys back into wallet/asset strings before applying the delta.
let address = if key.address.len() == Wallet::SHORT_ADDRESS_BYTES_LENGTH {
2026-05-26 06:24:57 +00:00
Wallet::bytes_to_short_address(&key.address).unwrap_or_default()
} else {
binary_to_string(key.address)
};
let coin = binary_to_string(key.coin);
2026-05-24 17:56:57 +00:00
let operand = if change >= 0 {
BalanceOperand::Addition
} else {
BalanceOperand::Subtraction
};
pending_effects.add_balance(&address, change.unsigned_abs(), &coin, operand);
}
2026-06-24 17:32:25 +00:00
Ok(())
2026-05-24 17:56:57 +00:00
}
2026-05-26 06:24:57 +00:00
pub async fn clear_selected_transaction_sql(
db: &Db,
miner: String,
limit: i64,
block_number: u32,
start_index: usize,
2026-05-24 17:56:57 +00:00
) -> Result<SelectedMempoolBatch> {
// Selection and ledger math stay paired so the caller can stream this exact
// selected batch into the block file afterward.
let batch = select_transactions_for_block(limit).await?;
let mut pending_effects = PendingEffects::default();
2026-06-24 17:32:25 +00:00
apply_selected_transaction_math(
2026-05-24 17:56:57 +00:00
&batch,
db,
miner,
block_number,
start_index,
&mut pending_effects,
)
.await?;
pending_effects.apply(db).map_err(|err| anyhow!(err))?;
Ok(batch)
}
2026-05-26 06:24:57 +00:00
pub async fn stream_selected_transaction_originals(
file: &mut File,
batch: &SelectedMempoolBatch,
) -> Result<()> {
if batch.originals.is_empty() {
file.flush()?;
return Ok(());
}
2026-05-24 17:56:57 +00:00
// Block payloads store the original mempool bytes back-to-back in selected
// order rather than rebuilding each transaction from parsed fields.
let total_len: usize = batch.originals.iter().map(Vec::len).sum();
2026-05-26 06:24:57 +00:00
let mut payload = Vec::with_capacity(total_len);
for original in &batch.originals {
payload.extend_from_slice(original);
}
file.write_all(&payload)?;
file.flush()?;
Ok(())
}
2026-05-24 17:56:57 +00:00
pub async fn delete_selected_transactions(batch: &SelectedMempoolBatch) -> Result<()> {
// Each transaction kind still lives in a separate SQL table, so deletion
// groups the selected IDs by table after the block has been written.
let transfer_ids = ids_for_table(batch, "transfer");
2026-05-26 06:24:57 +00:00
let token_ids = ids_for_table(batch, "token");
let issue_token_ids = ids_for_table(batch, "issue_token");
let burn_ids = ids_for_table(batch, "burn");
let nft_ids = ids_for_table(batch, "nft");
let marketing_ids = ids_for_table(batch, "marketing");
let vanity_ids = ids_for_table(batch, "vanity_address");
let swap_ids = ids_for_table(batch, "swap");
let lender_ids = ids_for_table(batch, "loan_contract");
let borrower_ids = ids_for_table(batch, "loan_payment");
let collateral_ids = ids_for_table(batch, "collateral_claim");
2026-06-23 15:17:12 +00:00
delete_rows("transfer", &transfer_ids).await?;
delete_rows("token", &token_ids).await?;
delete_rows("issue_token", &issue_token_ids).await?;
delete_rows("burn", &burn_ids).await?;
delete_rows("nft", &nft_ids).await?;
delete_rows("marketing", &marketing_ids).await?;
delete_rows("vanity_address", &vanity_ids).await?;
delete_rows("swap", &swap_ids).await?;
delete_rows("loan_contract", &lender_ids).await?;
delete_rows("loan_payment", &borrower_ids).await?;
delete_rows("collateral_claim", &collateral_ids).await?;
2026-05-26 06:24:57 +00:00
Ok(())
}