use anyhow::Result;
use sqlx::SqlitePool;
const TAKEN_REQ_STATUSES: [&str; 4] = ["unmined", "callback", "unconfirmed", "completed"];
const TAKEN_MEMORY_STATUSES: [&str; 3] = ["accepted", "seen", "mined"];
const TAKEN_TRACKER_WORDS: [&str; 4] = ["announced", "seen", "mined", "stale"];
pub async fn broadcaster_took(pool: &SqlitePool, txid: &str) -> Result<bool> {
let req: Option<String> =
sqlx::query_scalar("SELECT status FROM proven_tx_reqs WHERE txid = ? LIMIT 1")
.bind(txid)
.fetch_optional(pool)
.await?;
if req.is_some_and(|s| TAKEN_REQ_STATUSES.contains(&s.as_str())) {
return Ok(true);
}
let completed: Option<i64> = sqlx::query_scalar(
"SELECT 1 FROM transactions WHERE txid = ? AND status = 'completed' LIMIT 1",
)
.bind(txid)
.fetch_optional(pool)
.await?;
if completed.is_some() {
return Ok(true);
}
if table_exists(pool, "broadcast_seen").await? {
let rows: Vec<String> =
sqlx::query_scalar("SELECT status FROM broadcast_seen WHERE txid = ?")
.bind(txid)
.fetch_all(pool)
.await?;
if rows
.iter()
.any(|s| TAKEN_MEMORY_STATUSES.contains(&s.as_str()))
{
return Ok(true);
}
}
if table_exists(pool, "tracker_states").await? {
let word: Option<String> =
sqlx::query_scalar("SELECT word FROM tracker_states WHERE txid = ?")
.bind(txid)
.fetch_optional(pool)
.await?;
if word.is_some_and(|w| TAKEN_TRACKER_WORDS.contains(&w.as_str())) {
return Ok(true);
}
}
Ok(false)
}
pub async fn first_taken<'a>(
pool: &SqlitePool,
txids: impl IntoIterator<Item = &'a str>,
) -> Result<Option<String>> {
for txid in txids {
if broadcaster_took(pool, txid).await? {
return Ok(Some(txid.to_string()));
}
}
Ok(None)
}
pub async fn abort_refused_for(pool: &SqlitePool, reference: &str) -> Result<Option<String>> {
let by_reference: Option<Option<String>> =
sqlx::query_scalar("SELECT txid FROM transactions WHERE reference = ? LIMIT 1")
.bind(reference)
.fetch_optional(pool)
.await?;
let txid = match by_reference {
Some(txid) => txid,
None if reference.len() == 64 && reference.chars().all(|c| c.is_ascii_hexdigit()) => {
Some(reference.to_ascii_lowercase())
}
None => None,
};
let Some(txid) = txid else {
return Ok(None);
};
if broadcaster_took(pool, &txid).await? {
Ok(Some(txid))
} else {
Ok(None)
}
}
async fn table_exists(pool: &SqlitePool, name: &str) -> Result<bool> {
let found: Option<i64> =
sqlx::query_scalar("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?")
.bind(name)
.fetch_optional(pool)
.await?;
Ok(found.is_some())
}
#[cfg(test)]
mod tests {
use super::*;
async fn pool() -> SqlitePool {
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::query("CREATE TABLE proven_tx_reqs (txid TEXT, status TEXT)")
.execute(&pool)
.await
.unwrap();
sqlx::query("CREATE TABLE transactions (txid TEXT, status TEXT)")
.execute(&pool)
.await
.unwrap();
pool
}
#[tokio::test]
async fn a_request_past_the_post_or_an_accepting_row_is_taken_and_a_refusal_alone_is_not() {
let pool = pool().await;
let (a, b, c, d) = (
"a".repeat(64),
"b".repeat(64),
"c".repeat(64),
"d".repeat(64),
);
for (txid, req, tx) in [
(&a, "unmined", "unproven"),
(&b, "unsent", "sending"),
(&c, "unsent", "unproven"),
(&d, "sending", "sending"),
] {
sqlx::query("INSERT INTO proven_tx_reqs VALUES (?, ?)")
.bind(txid)
.bind(req)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO transactions VALUES (?, ?)")
.bind(txid)
.bind(tx)
.execute(&pool)
.await
.unwrap();
}
assert!(broadcaster_took(&pool, &a).await.unwrap(), "unmined");
assert!(!broadcaster_took(&pool, &b).await.unwrap(), "no record");
sqlx::query("CREATE TABLE broadcast_seen (txid TEXT, provider TEXT, status TEXT)")
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO broadcast_seen VALUES (?, 'arcade', 'rejected'), (?, 'arcade', 'accepted')")
.bind(&b)
.bind(&c)
.execute(&pool)
.await
.unwrap();
assert!(!broadcaster_took(&pool, &b).await.unwrap(), "a refusal row");
assert!(
broadcaster_took(&pool, &c).await.unwrap(),
"an accepting row"
);
sqlx::query("CREATE TABLE tracker_states (txid TEXT, word TEXT)")
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO tracker_states VALUES (?, 'announced')")
.bind(&d)
.execute(&pool)
.await
.unwrap();
assert!(
broadcaster_took(&pool, &d).await.unwrap(),
"the tracker's word"
);
assert_eq!(
first_taken(&pool, [b.as_str(), d.as_str()]).await.unwrap(),
Some(d.clone())
);
}
}