// Copyright (C) 2024-2026 Whiterun LLC // // This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later. // A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html //! Write path: index a block or mempool batch, mutate UTXO tables, roll back. use anyhow::Result; use bitcoin_hashes::Hash; use bitcoincash::{BlockHash, TokenID, Transaction, Txid}; use log::debug; use riftenlabs_defi::moria_v1_1::{ crank_allowance_locking_bytecode, crank_hint_to_policy, decode_crank_hint, parse_moria_v11_from_tx, CrankPolicy, LoanCommitmentV11, MoriaV11ActionType, MoriaV11TokenIds, ParsedMoriaV11Action, }; use sqlx::{Row, SqlitePool}; use crate::db::blob::ToBlob; use super::{action_type_to_int, action_type_to_str, staking}; const BP_MESSAGE_SIZE: usize = 26; /// `BP_TIMESTAMP_OFFSET` — u48 LE seconds. const BP_TIMESTAMP_OFFSET: usize = 0; /// `BP_BP_OFFSET` — u16 LE threshold in basis points. const BP_BP_OFFSET: usize = 8; /// Decode threshold_bp + oracle_timestamp from a 26-byte BP oracle commitment. /// /// Layout: timestamp u48 LE @ 0, threshold u16 LE @ 8. pub fn parse_bp_commitment(commitment: &[u8]) -> Option<(u16, u64)> { if commitment.len() != BP_MESSAGE_SIZE { return None; } let mut ts_bytes = [0u8; 8]; ts_bytes[..6].copy_from_slice(&commitment[BP_TIMESTAMP_OFFSET..BP_TIMESTAMP_OFFSET + 6]); let oracle_timestamp = u64::from_le_bytes(ts_bytes); let threshold_bp = u16::from_le_bytes([commitment[BP_BP_OFFSET], commitment[BP_BP_OFFSET + 1]]); Some((threshold_bp, oracle_timestamp)) } /// Build a minimal 26-byte BP commitment for tests (only timestamp + threshold filled). #[cfg(test)] pub fn encode_bp_commitment(threshold_bp: u16, oracle_timestamp: u64) -> Vec { let mut out = vec![0u8; BP_MESSAGE_SIZE]; out[BP_TIMESTAMP_OFFSET..BP_TIMESTAMP_OFFSET + 6] .copy_from_slice(&oracle_timestamp.to_le_bytes()[..6]); out[BP_BP_OFFSET..BP_BP_OFFSET + 2].copy_from_slice(&threshold_bp.to_le_bytes()); out } fn commitment_fields(c: &LoanCommitmentV11) -> (i64, i64, i64) { ( c.principal as i64, c.annual_interest_rate_bp as i64, c.timestamp as i64, ) } #[allow(clippy::too_many_arguments)] pub(super) async fn insert_loan_utxo( pool: &SqlitePool, txid: &Txid, vout: u32, commitment: &LoanCommitmentV11, collateral_sats: u64, blockhash: &BlockHash, ) -> Result<()> { sqlx::query( "INSERT OR REPLACE INTO moria_v11_loan_utxo (txid, vout, borrower_hash, delegate_hash, principal, interest_rate, loan_timestamp, collateral_sats, blockhash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(txid.to_blob()) .bind(vout as i64) .bind(commitment.borrower_nft_hash.as_slice()) .bind(commitment.delegate_nft_hash.as_slice()) .bind(commitment.principal as i64) .bind(commitment.annual_interest_rate_bp as i64) .bind(commitment.timestamp as i64) .bind(collateral_sats as i64) .bind(blockhash.to_blob()) .execute(pool) .await .map_err(|e| anyhow::anyhow!("failed to insert v11 loan utxo: {}", e))?; Ok(()) } async fn delete_loan_utxo(pool: &SqlitePool, txid: &Txid, vout: u32) -> Result<()> { sqlx::query("DELETE FROM moria_v11_loan_utxo WHERE txid = ? AND vout = ?") .bind(txid.to_blob()) .bind(vout as i64) .execute(pool) .await .map_err(|e| anyhow::anyhow!("failed to delete v11 loan utxo: {}", e))?; Ok(()) } /// Look up a spent loan UTXO. type LoanUtxoRow = (Vec, Vec, i64, i64, i64, i64); async fn lookup_loan_utxo( pool: &SqlitePool, txid: &Txid, vout: u32, ) -> Result> { let row: Option = sqlx::query_as( "SELECT borrower_hash, delegate_hash, principal, interest_rate, loan_timestamp, collateral_sats FROM moria_v11_loan_utxo WHERE txid = ? AND vout = ?", ) .bind(txid.to_blob()) .bind(vout as i64) .fetch_optional(pool) .await?; Ok(row) } #[allow(clippy::too_many_arguments)] pub(super) async fn insert_allowance_utxo( pool: &SqlitePool, txid: &Txid, vout: u32, owner_hash: &[u8], delegate_hash: &[u8], locking_bytecode: &[u8], token_amount: i64, sats: u64, blockhash: &BlockHash, policy: Option<&CrankPolicy>, fee_sats: Option, ) -> Result<()> { sqlx::query( "INSERT OR REPLACE INTO moria_v11_allowance_utxo (txid, vout, owner_hash, delegate_hash, locking_bytecode, token_amount, sats, blockhash, policy_delta, policy_max_bp, policy_rescue_lead_bp, policy_up_leg_seconds, policy_down_leg_seconds, fee_sats) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(txid.to_blob()) .bind(vout as i64) .bind(owner_hash) .bind(delegate_hash) .bind(locking_bytecode) .bind(token_amount) .bind(sats as i64) .bind(blockhash.to_blob()) .bind(policy.map(|p| p.delta as i64)) .bind(policy.map(|p| p.max_bp as i64)) .bind(policy.map(|p| p.rescue_lead_bp as i64)) .bind(policy.map(|p| p.up_leg_seconds as i64)) .bind(policy.map(|p| p.down_leg_seconds as i64)) .bind(fee_sats) .execute(pool) .await .map_err(|e| anyhow::anyhow!("failed to insert v11 allowance utxo: {}", e))?; Ok(()) } async fn delete_allowance_utxo(pool: &SqlitePool, txid: &Txid, vout: u32) -> Result<()> { sqlx::query("DELETE FROM moria_v11_allowance_utxo WHERE txid = ? AND vout = ?") .bind(txid.to_blob()) .bind(vout as i64) .execute(pool) .await .map_err(|e| anyhow::anyhow!("failed to delete v11 allowance utxo: {}", e))?; Ok(()) } struct AllowanceUtxoRow { owner_hash: Vec, delegate_hash: Vec, policy: Option, fee_sats: Option, } fn crank_policy_from_cols( delta: Option, max_bp: Option, rescue_lead_bp: Option, up_leg_seconds: Option, down_leg_seconds: Option, ) -> Option { Some(CrankPolicy { delta: u32::try_from(delta?).ok()?, max_bp: u32::try_from(max_bp?).ok()?, rescue_lead_bp: u32::try_from(rescue_lead_bp?).ok()?, up_leg_seconds: u32::try_from(up_leg_seconds?).ok()?, down_leg_seconds: u32::try_from(down_leg_seconds?).ok()?, }) } async fn lookup_allowance_utxo( pool: &SqlitePool, txid: &Txid, vout: u32, ) -> Result> { let row = sqlx::query( "SELECT owner_hash, delegate_hash, locking_bytecode, token_amount, sats, policy_delta, policy_max_bp, policy_rescue_lead_bp, policy_up_leg_seconds, policy_down_leg_seconds, fee_sats FROM moria_v11_allowance_utxo WHERE txid = ? AND vout = ?", ) .bind(txid.to_blob()) .bind(vout as i64) .fetch_optional(pool) .await?; Ok(row.map(|r| AllowanceUtxoRow { owner_hash: r.get("owner_hash"), delegate_hash: r.get("delegate_hash"), policy: crank_policy_from_cols( r.get("policy_delta"), r.get("policy_max_bp"), r.get("policy_rescue_lead_bp"), r.get("policy_up_leg_seconds"), r.get("policy_down_leg_seconds"), ), fee_sats: r.get("fee_sats"), })) } /// Known allowance locking scripts already seen as live UTXOs (first-spend /// or borrow deposit). Used so later top-ups to the same P2SH32 are indexed. /// Policy+fee ride along so a top-up does not wipe a verified hint. struct KnownAllowanceScript { locking_bytecode: Vec, owner_hash: Vec, delegate_hash: Vec, policy: Option, fee_sats: Option, } async fn known_allowance_scripts(pool: &SqlitePool) -> Result> { let mut out: Vec = Vec::new(); let mut seen: std::collections::HashMap, usize> = std::collections::HashMap::new(); let utxo_rows = sqlx::query( "SELECT locking_bytecode, owner_hash, delegate_hash, policy_delta, policy_max_bp, policy_rescue_lead_bp, policy_up_leg_seconds, policy_down_leg_seconds, fee_sats FROM moria_v11_allowance_utxo", ) .fetch_all(pool) .await?; for row in utxo_rows { let script: Vec = row.get("locking_bytecode"); let policy = crank_policy_from_cols( row.get("policy_delta"), row.get("policy_max_bp"), row.get("policy_rescue_lead_bp"), row.get("policy_up_leg_seconds"), row.get("policy_down_leg_seconds"), ); let fee_sats: Option = row.get("fee_sats"); if let Some(&idx) = seen.get(&script) { if out[idx].policy.is_none() && policy.is_some() { out[idx].policy = policy; } if out[idx].fee_sats.is_none() && fee_sats.is_some() { out[idx].fee_sats = fee_sats; } continue; } seen.insert(script.clone(), out.len()); out.push(KnownAllowanceScript { locking_bytecode: script, owner_hash: row.get("owner_hash"), delegate_hash: row.get("delegate_hash"), policy, fee_sats, }); } Ok(out) } async fn insert_bp_threshold( pool: &SqlitePool, txid: &Txid, blockhash: &BlockHash, threshold_bp: u16, oracle_timestamp: u64, ) -> Result<()> { sqlx::query( "INSERT OR REPLACE INTO moria_v11_bp_threshold (txid, blockhash, threshold_bp, oracle_timestamp) VALUES (?, ?, ?, ?)", ) .bind(txid.to_blob()) .bind(blockhash.to_blob()) .bind(threshold_bp as i64) .bind(oracle_timestamp as i64) .execute(pool) .await .map_err(|e| anyhow::anyhow!("failed to insert v11 bp threshold: {}", e))?; Ok(()) } #[allow(clippy::too_many_arguments)] async fn insert_action( pool: &SqlitePool, txid: &Txid, blockhash: &BlockHash, mtp: i64, first_seen: Option, action: &ParsedMoriaV11Action, borrower_hash: Option<&[u8]>, delegate_hash: Option<&[u8]>, ) -> Result<()> { let old_c = action.loan_commitment.as_ref(); let new_c = action.new_loan_commitment.as_ref(); let bh = borrower_hash .or_else(|| old_c.map(|c| c.borrower_nft_hash.as_slice())) .or_else(|| new_c.map(|c| c.borrower_nft_hash.as_slice())); let dh = delegate_hash .or_else(|| old_c.map(|c| c.delegate_nft_hash.as_slice())) .or_else(|| new_c.map(|c| c.delegate_nft_hash.as_slice())); let has_old = old_c.is_some(); let (principal, interest_rate, loan_timestamp) = old_c.map(commitment_fields).unwrap_or((0, 0, 0)); let has_new = new_c.is_some(); let (new_principal, new_interest_rate, new_loan_timestamp) = new_c.map(commitment_fields).unwrap_or((0, 0, 0)); let allowance_token_amount = action .allowance_deposit .as_ref() .map(|d| d.token_amount) .or_else(|| { action .crank_allowance .as_ref() .map(|c| c.output_token_amount) }); let allowance_sats = action .allowance_deposit .as_ref() .map(|d| d.sats as i64) .or_else(|| { action .crank_allowance .as_ref() .map(|c| c.output_sats as i64) }); let fee_take_sats = action.crank_fee_sats.map(|s| s as i64); let recovered_token_amount = action.allowance_close.as_ref().map(|c| c.token_amount); let recovered_sats = action.allowance_close.as_ref().map(|c| c.sats as i64); let new_collateral_sats = if has_new { action.collateral_sats.map(|s| s as i64) } else { None }; sqlx::query( "INSERT INTO moria_v11_action (txid, blockhash, action_type, borrower_hash, delegate_hash, principal, interest_rate, loan_timestamp, collateral_sats, tokens_amount, mtp_timestamp, first_seen_timestamp, new_principal, new_interest_rate, new_loan_timestamp, new_collateral_sats, allowance_token_amount, allowance_sats, fee_take_sats, recovered_token_amount, recovered_sats, loan_output_index) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(txid.to_blob()) .bind(blockhash.to_blob()) .bind(action_type_to_int(action.action_type)) .bind(bh) .bind(dh) .bind(if has_old { Some(principal) } else { None }) .bind(if has_old { Some(interest_rate) } else { None }) .bind(if has_old { Some(loan_timestamp) } else { None }) .bind(action.collateral_sats.map(|s| s as i64)) .bind(action.tokens_amount) .bind(mtp) .bind(first_seen) .bind(if has_new { Some(new_principal) } else { None }) .bind(if has_new { Some(new_interest_rate) } else { None }) .bind(if has_new { Some(new_loan_timestamp) } else { None }) .bind(new_collateral_sats) .bind(allowance_token_amount) .bind(allowance_sats) .bind(fee_take_sats) .bind(recovered_token_amount) .bind(recovered_sats) .bind(action.loan_output_index.map(|i| i as i64)) .execute(pool) .await .map_err(|e| anyhow::anyhow!("failed to insert moria_v11_action: {}", e))?; Ok(()) } fn index_bp_oracle_outputs(tx: &Transaction, bp_oracle: &TokenID) -> Option<(u16, u64)> { for out in &tx.output { let Some(tok) = out.token.as_ref() else { continue; }; if &tok.id != bp_oracle { continue; } if let Some(parsed) = parse_bp_commitment(&tok.commitment) { return Some(parsed); } } None } fn verified_borrow_hint_policy( tx: &Transaction, token_ids: &MoriaV11TokenIds, commitment: &LoanCommitmentV11, ) -> Option<(CrankPolicy, u64)> { let seq = tx.input.get(4)?.sequence.to_consensus_u32(); let hint = decode_crank_hint(seq)?; if token_ids.loan_locking_bytecode.is_empty() { return None; } let script = tx.output.get(8)?.script_pubkey.as_bytes(); let policy = crank_hint_to_policy(&hint); let bp = token_ids.bp_oracle.to_byte_array(); let mut bp_rev = bp; bp_rev.reverse(); for cat in [&bp, &bp_rev] { let derived = crank_allowance_locking_bytecode( &token_ids.loan_locking_bytecode, &commitment.borrower_nft_hash, &commitment.delegate_nft_hash, hint.fee_sats, cat, &policy, ); if script == derived.as_slice() || script.ends_with(derived.as_slice()) { return Some((policy, hint.fee_sats)); } } None } /// Index moria v1-1 transactions from a block (or mempool batch). /// /// Returns the number of loan/allowance actions recorded (BP threshold rows /// are not included in the count). pub async fn index_moria_v11( pool: &SqlitePool, txs: &[Transaction], blockhash: &BlockHash, mtp: i64, token_ids: &MoriaV11TokenIds, ) -> Result { let mut count = 0; for tx in txs { let txid = tx.compute_txid(); if *blockhash != BlockHash::all_zeros() { delete_unconfirmed_tx(pool, &txid).await?; } // BP oracle threshold updates (independent of loan actions). if let Some((threshold_bp, oracle_ts)) = index_bp_oracle_outputs(tx, &token_ids.bp_oracle) { insert_bp_threshold(pool, &txid, blockhash, threshold_bp, oracle_ts).await?; debug!( "moria_v11 bp threshold: {} bp={} ts={}", txid, threshold_bp, oracle_ts ); } // Discover allowances from borrow optional-MUSD or first spend. // Previously-seen UTXO scripts remain recognizable for top-ups. let known_scripts = known_allowance_scripts(pool).await?; if !known_scripts.is_empty() { for (vout, out) in tx.output.iter().enumerate() { let script = out.script_pubkey.as_bytes(); let Some(known) = known_scripts .iter() .find(|k| k.locking_bytecode.as_slice() == script) else { continue; }; let Some(tok) = out.token.as_ref() else { continue; }; if tok.id != token_ids.moria || tok.has_nft() { continue; } let amount = tok.amount.to_int(); if amount <= 0 { continue; } insert_allowance_utxo( pool, &txid, vout as u32, &known.owner_hash, &known.delegate_hash, script, amount, out.value.to_sat(), blockhash, known.policy.as_ref(), known.fee_sats, ) .await?; } } let actions = parse_moria_v11_from_tx(tx, token_ids); // Track which allowance outpoints this tx's actions explicitly handle, // so the generic spend cleanup does not double-delete mid-action. let mut handled_allowance_spends: Vec<(Txid, u32)> = Vec::new(); for mut action in actions { let mut looked_up_borrower: Option> = None; let mut looked_up_delegate: Option> = None; // Fill prior commitment from the live loan UTXO set when the parser // could not recover it (bare txs lack input token payloads). if let Some((spent_txid, spent_vout)) = &action.spent_loan_outpoint { if let Some((bh, dh, principal, interest_rate, loan_ts, collateral)) = lookup_loan_utxo(pool, spent_txid, *spent_vout).await? { if action.loan_commitment.is_none() { let mut borrower = [0u8; 32]; let mut delegate = [0u8; 32]; if bh.len() == 32 { borrower.copy_from_slice(&bh); } if dh.len() == 32 { delegate.copy_from_slice(&dh); } action.loan_commitment = Some(LoanCommitmentV11 { borrower_nft_hash: borrower, delegate_nft_hash: delegate, principal: principal as u16, annual_interest_rate_bp: interest_rate as u16, timestamp: loan_ts as u64, }); } if action.collateral_sats.is_none() && matches!( action.action_type, MoriaV11ActionType::Repay | MoriaV11ActionType::Redeem | MoriaV11ActionType::Liquidate ) { action.collateral_sats = Some(collateral as u64); } looked_up_borrower = Some(bh); looked_up_delegate = Some(dh); delete_loan_utxo(pool, spent_txid, *spent_vout).await?; } } // Create / replace continuing loan UTXO. if let Some(output_idx) = action.loan_output_index { if let Some(commitment) = action .new_loan_commitment .as_ref() .or(action.loan_commitment.as_ref()) { insert_loan_utxo( pool, &txid, output_idx, commitment, action.collateral_sats.unwrap_or(0), blockhash, ) .await?; // Policy lives on the allowance, revealed at first spend. } } // Allowance deposit on borrow (parser: optional MUSD at out 8/9). if let Some(dep) = &action.allowance_deposit { if let Some(c) = action .loan_commitment .as_ref() .or(action.new_loan_commitment.as_ref()) { let verified = verified_borrow_hint_policy(tx, token_ids, c); let fee_sats = verified.as_ref().map(|(_, f)| *f as i64); insert_allowance_utxo( pool, &txid, dep.output_index, &c.borrower_nft_hash, &c.delegate_nft_hash, &dep.locking_bytecode, dep.token_amount, dep.sats, blockhash, verified.as_ref().map(|(p, _)| p), fee_sats, ) .await?; } } // Crank: spend prior allowance + insert the purse continuation // (not same-index as the METHOD_CRANK input). if let Some(info) = &action.crank_allowance { let cont_vout = tx .output .iter() .enumerate() .find(|(_, o)| o.script_pubkey.as_bytes() == info.locking_bytecode.as_slice()) .map(|(i, _)| i as u32) .unwrap_or(info.io_index); if let Some(inp) = tx.input.get(info.io_index as usize) { let prev = &inp.previous_output; if let Some(spent) = lookup_allowance_utxo(pool, &prev.txid, prev.vout).await? { delete_allowance_utxo(pool, &prev.txid, prev.vout).await?; handled_allowance_spends.push((prev.txid, prev.vout)); insert_allowance_utxo( pool, &txid, cont_vout, &spent.owner_hash, &spent.delegate_hash, &info.locking_bytecode, info.output_token_amount, info.output_sats, blockhash, spent.policy.as_ref(), spent.fee_sats, ) .await?; } else if let Some(c) = action .new_loan_commitment .as_ref() .or(action.loan_commitment.as_ref()) { // First spend of an unregistered allowance — classify now. // Policy/fee stay null until a verified borrow hint (or a // later spend of a row that already had them). insert_allowance_utxo( pool, &txid, cont_vout, &c.borrower_nft_hash, &c.delegate_nft_hash, &info.locking_bytecode, info.output_token_amount, info.output_sats, blockhash, None, None, ) .await?; } else if let (Some(owner), Some(delegate)) = (&looked_up_borrower, &looked_up_delegate) { insert_allowance_utxo( pool, &txid, cont_vout, owner, delegate, &info.locking_bytecode, info.output_token_amount, info.output_sats, blockhash, None, None, ) .await?; } } } // AllowanceClose: look up owner before deleting the spent UTXO. if let Some(close) = &action.allowance_close { if let Some(inp) = tx.input.get(close.input_index as usize) { let prev = &inp.previous_output; if let Some(spent) = lookup_allowance_utxo(pool, &prev.txid, prev.vout).await? { looked_up_borrower = Some(spent.owner_hash); looked_up_delegate = Some(spent.delegate_hash); delete_allowance_utxo(pool, &prev.txid, prev.vout).await?; handled_allowance_spends.push((prev.txid, prev.vout)); } } } debug!( "moria_v11: {} {} (borrower: {})", action_type_to_str(action.action_type), txid, action .loan_commitment .as_ref() .or(action.new_loan_commitment.as_ref()) .map(|c| hex::encode(c.borrower_nft_hash)) .or_else(|| looked_up_borrower.as_ref().map(hex::encode)) .unwrap_or_else(|| "n/a".to_string()), ); insert_action( pool, &txid, blockhash, mtp, None, &action, looked_up_borrower.as_deref(), looked_up_delegate.as_deref(), ) .await?; count += 1; } // Remove any remaining spent known-allowance UTXOs (e.g. closed without // parser match, or reorg-safe cleanup of pure spends). for inp in &tx.input { let prev = &inp.previous_output; if handled_allowance_spends .iter() .any(|(t, v)| t == &prev.txid && *v == prev.vout) { continue; } if lookup_allowance_utxo(pool, &prev.txid, prev.vout) .await? .is_some() { delete_allowance_utxo(pool, &prev.txid, prev.vout).await?; } } } Ok(count) } pub async fn delete_entries_for_block(pool: &SqlitePool, blockhash: &BlockHash) -> Result { sqlx::query("DELETE FROM moria_v11_loan_utxo WHERE blockhash = ?") .bind(blockhash.to_blob()) .execute(pool) .await .map_err(|e| anyhow::anyhow!("failed to delete v11 loan utxos for block: {}", e))?; sqlx::query("DELETE FROM moria_v11_allowance_utxo WHERE blockhash = ?") .bind(blockhash.to_blob()) .execute(pool) .await .map_err(|e| anyhow::anyhow!("failed to delete v11 allowance utxos for block: {}", e))?; sqlx::query("DELETE FROM moria_v11_bp_threshold WHERE blockhash = ?") .bind(blockhash.to_blob()) .execute(pool) .await .map_err(|e| anyhow::anyhow!("failed to delete v11 bp thresholds for block: {}", e))?; let r = sqlx::query("DELETE FROM moria_v11_action WHERE blockhash = ?") .bind(blockhash.to_blob()) .execute(pool) .await .map_err(|e| { anyhow::anyhow!( "failed to delete moria_v11_action for block {}: {}", blockhash, e ) })?; let staking_deleted = staking::delete_entries_for_block(pool, blockhash).await?; Ok(r.rows_affected() as usize + staking_deleted) } pub async fn has_entry(pool: &SqlitePool, txid: &Txid) -> Result { let row: Option<(i64,)> = sqlx::query_as("SELECT 1 FROM moria_v11_action WHERE txid = ? LIMIT 1") .bind(txid.to_blob()) .fetch_optional(pool) .await?; Ok(row.is_some()) } /// Txids currently stored under the mempool sentinel (`BlockHash::all_zeros`). pub async fn get_unconfirmed_txids(pool: &SqlitePool) -> Result> { let zero = BlockHash::all_zeros().to_blob(); let rows = sqlx::query( "SELECT txid FROM moria_v11_action WHERE blockhash = ? UNION SELECT txid FROM moria_v11_loan_utxo WHERE blockhash = ? UNION SELECT txid FROM moria_v11_allowance_utxo WHERE blockhash = ? UNION SELECT txid FROM moria_v11_bp_threshold WHERE blockhash = ?", ) .bind(&zero) .bind(&zero) .bind(&zero) .bind(&zero) .fetch_all(pool) .await?; let mut out = Vec::new(); for row in rows { let blob: Vec = row.get("txid"); if blob.len() == 32 { let mut arr = [0u8; 32]; arr.copy_from_slice(&blob); out.push(Txid::from_byte_array(arr)); } } Ok(out) } /// Drop a mempool-only row set (evicted / replaced). Confirmed rows stay. pub async fn delete_unconfirmed_tx(pool: &SqlitePool, txid: &Txid) -> Result { let zero = BlockHash::all_zeros().to_blob(); let id = txid.to_blob(); let mut n = 0usize; for table in [ "moria_v11_loan_utxo", "moria_v11_allowance_utxo", "moria_v11_bp_threshold", "moria_v11_action", ] { let sql = format!("DELETE FROM {table} WHERE txid = ? AND blockhash = ?"); let r = sqlx::query(&sql) .bind(&id) .bind(&zero) .execute(pool) .await .map_err(|e| anyhow::anyhow!("failed to delete unconfirmed {table}: {e}"))?; n += r.rows_affected() as usize; } Ok(n) } pub async fn clear_mempool(pool: &SqlitePool) -> Result { let zero = BlockHash::all_zeros().to_blob(); sqlx::query("DELETE FROM moria_v11_loan_utxo WHERE blockhash = ?") .bind(&zero) .execute(pool) .await?; sqlx::query("DELETE FROM moria_v11_allowance_utxo WHERE blockhash = ?") .bind(&zero) .execute(pool) .await?; sqlx::query("DELETE FROM moria_v11_bp_threshold WHERE blockhash = ?") .bind(&zero) .execute(pool) .await?; let r = sqlx::query("DELETE FROM moria_v11_action WHERE blockhash = ?") .bind(&zero) .execute(pool) .await .map_err(|e| anyhow::anyhow!("failed to clear v11 mempool entries: {}", e))?; Ok(r.rows_affected() as usize) }