Merge branch 'tokentoken' into 'master'
Upgrade bitcoincash to 0.32 and add TokenToken AMM pool indexing See merge request riftenlabs/riftenlabs-indexer!88
This commit is contained in:
commit
9c1fd7550f
42 changed files with 1645 additions and 810 deletions
84
Cargo.lock
generated
84
Cargo.lock
generated
|
|
@ -179,6 +179,15 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base58ck"
|
||||
version = "0.1.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec5dc7e09f7bb15f0062da7c03086d6b71a2c84e0af4fccbbc7d8c6559847816"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
|
|
@ -193,9 +202,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
|||
|
||||
[[package]]
|
||||
name = "bech32"
|
||||
version = "0.9.1"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
|
||||
checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f"
|
||||
|
||||
[[package]]
|
||||
name = "binascii"
|
||||
|
|
@ -204,22 +213,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.11.0"
|
||||
name = "bitcoin-internals"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4"
|
||||
checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoincash"
|
||||
version = "0.29.2"
|
||||
name = "bitcoin-io"
|
||||
version = "0.1.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc7b97d0bbf8b62e6cfbec23aa42e677dfced45572a65992c5e08906728d44bf"
|
||||
checksum = "11301df0b06f22dea7bb1916403fdd88a371031e495c49b8f96931b28189e175"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-units"
|
||||
version = "0.1.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57bad157b78d0d1b22c4cbb6a35a566211fc4d14866a37f2c780652b50f3b845"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.14.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f"
|
||||
dependencies = [
|
||||
"bitcoin-io",
|
||||
"hex-conservative",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoincash"
|
||||
version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58eb81f30c9d5ef8d3bc7c932ddc029999d27241e53743c6e05e70488e71a1ad"
|
||||
dependencies = [
|
||||
"base58ck",
|
||||
"bech32",
|
||||
"bitcoin-internals",
|
||||
"bitcoin-io",
|
||||
"bitcoin-units",
|
||||
"bitcoin_hashes",
|
||||
"hex-conservative",
|
||||
"hex_lit",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
]
|
||||
|
|
@ -984,6 +1025,21 @@ version = "0.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex_lit"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
|
|
@ -2154,9 +2210,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "riftenlabs-defi"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98d517c00862416d7a8c2faafc23724c5bb9c39e02a7a962cd61edb83ca284bb"
|
||||
checksum = "6ac66c2bfc69e97bd199f338c46d32eaabf5f41c9428acb726b1a35ce25a26b5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitcoin_hashes",
|
||||
|
|
@ -2491,9 +2547,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
|||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.24.3"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62"
|
||||
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"secp256k1-sys",
|
||||
|
|
@ -2502,9 +2558,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.6.1"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b"
|
||||
checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ build = "build.rs"
|
|||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.94", features = ["backtrace"] }
|
||||
bitcoincash = "0.29.2"
|
||||
bitcoin_hashes = { version = "0.11.0", default-features = false } # Same version as used by bitcoincash
|
||||
bitcoincash = "0.32.1"
|
||||
bitcoin_hashes = { version = "0.14.100", default-features = false } # Same version as used by bitcoincash
|
||||
electrum-client-netagnostic = { version = "0.21.2", features = ["use-rustls", "use-websocket"] }
|
||||
hex = "0.4.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
|
@ -22,7 +22,7 @@ serde_json = "1.0.133"
|
|||
rocket = { version = "0.5.1", features = ["json"] }
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
||||
rayon = "1.10.0"
|
||||
riftenlabs-defi = "0.3.0"
|
||||
riftenlabs-defi = "0.4.0"
|
||||
rocket_cors = "0.6.0"
|
||||
log = "0.4"
|
||||
stderrlog = "0.6.0"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use crate::{
|
|||
db::bcmr::{get_entries_missing_bcmr_download, AuthChainEntry},
|
||||
};
|
||||
use anyhow::*;
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
|
||||
use bitcoincash::Script;
|
||||
use log::{info, warn};
|
||||
use rand::thread_rng;
|
||||
|
|
@ -141,33 +141,29 @@ async fn fetch_bcmr(
|
|||
async fn process_entry(client: &reqwest::Client, db: &SqlitePool, entry: &AuthChainEntry) {
|
||||
info!(
|
||||
"bcmr: Downloading BCMR for token {}, txid {}",
|
||||
entry.token_id.to_hex(),
|
||||
entry.txid.to_hex()
|
||||
entry.token_id, entry.txid
|
||||
);
|
||||
|
||||
let Some(raw) = entry.bcmr_data.as_ref() else {
|
||||
warn!(
|
||||
"bcmr: Missing OP_RETURN BCMR for utxo {} (token {}, txid {}); skipping.",
|
||||
entry.utxo.to_hex(),
|
||||
entry.token_id.to_hex(),
|
||||
entry.txid.to_hex()
|
||||
entry.utxo, entry.token_id, entry.txid
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let bcmr = match parse_bcmr_from_opreturn(&Script::from(raw.clone())) {
|
||||
let bcmr = match parse_bcmr_from_opreturn(Script::from_bytes(raw)) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
warn!(
|
||||
"bcmr: Invalid BCMR OP_RETURN for token {}, txid {}",
|
||||
entry.token_id.to_hex(),
|
||||
entry.txid.to_hex()
|
||||
entry.token_id, entry.txid
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let (json_str, error, is_fatal) = fetch_bcmr(client, &bcmr.uris, &bcmr.hash.to_hex()).await;
|
||||
let (json_str, error, is_fatal) = fetch_bcmr(client, &bcmr.uris, &hex::encode(bcmr.hash)).await;
|
||||
|
||||
let (json_str, actual_hash) = match json_str {
|
||||
Some(j) => j,
|
||||
|
|
@ -211,7 +207,7 @@ async fn process_entry(client: &reqwest::Client, db: &SqlitePool, entry: &AuthCh
|
|||
&json,
|
||||
&entry.token_id,
|
||||
Some(actual_hash),
|
||||
Some(bcmr.hash.to_hex()),
|
||||
Some(hex::encode(bcmr.hash)),
|
||||
SOURCE_ON_CHAIN,
|
||||
) {
|
||||
Ok(b) => b,
|
||||
|
|
|
|||
101
src/bcmr/mod.rs
101
src/bcmr/mod.rs
|
|
@ -6,9 +6,7 @@
|
|||
use std::convert::TryInto;
|
||||
|
||||
use anyhow::Result;
|
||||
#[cfg(test)]
|
||||
use bitcoin_hashes::hex::FromHex;
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
|
||||
#[cfg(test)]
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::{BlockHash, Script, TokenID, Transaction, Txid};
|
||||
|
|
@ -108,7 +106,7 @@ async fn find_parent_auth_entries(
|
|||
}
|
||||
|
||||
pub fn parse_bcmr_from_opreturn(bcmr_op_return: &Script) -> Option<BCMR> {
|
||||
let iter = bcmr_op_return[6..].iter();
|
||||
let iter = bcmr_op_return.as_bytes()[6..].iter();
|
||||
let (iter, hash) = read_push_from_script(iter).ok()?;
|
||||
|
||||
let hash: [u8; 32] = hash.and_then(|h| h.try_into().ok())?;
|
||||
|
|
@ -157,13 +155,13 @@ pub async fn index_bcmr(
|
|||
// 1) Genesis path: start of auth chain (height 0)
|
||||
if let Some(token_id) = is_genesis_tx(tx) {
|
||||
let bcmr = parse_bcmr(tx);
|
||||
let txid = tx.txid();
|
||||
let txid = tx.compute_txid();
|
||||
let utxo = compute_outpoint_hash(&txid, 0);
|
||||
|
||||
debug!(
|
||||
"Found token genesis for {} in {}; has bcmr: {}",
|
||||
token_id.to_hex(),
|
||||
txid.to_hex(),
|
||||
token_id,
|
||||
txid,
|
||||
bcmr.is_some()
|
||||
);
|
||||
|
||||
|
|
@ -188,18 +186,18 @@ pub async fn index_bcmr(
|
|||
|
||||
// 2) Update path: if any input spends a known auth utxo, extend that chain
|
||||
let bcmr = parse_bcmr(tx);
|
||||
let txid = tx.txid();
|
||||
let txid = tx.compute_txid();
|
||||
let new_utxo = compute_outpoint_hash(&txid, 0);
|
||||
|
||||
for parent in parents {
|
||||
let new_height = parent.height + 1;
|
||||
debug!(
|
||||
"Found authheader update {} in {}; has bcmr: {}; new height: {}, utxo: {}",
|
||||
parent.token_id.to_hex(),
|
||||
txid.to_hex(),
|
||||
parent.token_id,
|
||||
txid,
|
||||
bcmr.is_some(),
|
||||
new_height,
|
||||
new_utxo.to_hex()
|
||||
new_utxo
|
||||
);
|
||||
|
||||
insert_authheader(
|
||||
|
|
@ -222,7 +220,10 @@ pub async fn index_bcmr(
|
|||
#[tokio::test]
|
||||
async fn multi_parent_auth_update_creates_entries_for_all_tokens() {
|
||||
use crate::db::bcmr::prepare_tables;
|
||||
use bitcoincash::{OutPoint, PackedLockTime, Sequence, TxIn, TxOut};
|
||||
use bitcoincash::{
|
||||
locktime::absolute::LockTime, transaction::Version, Amount, OutPoint, ScriptBuf, Sequence,
|
||||
TxIn, TxOut,
|
||||
};
|
||||
|
||||
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||
prepare_tables(&pool).await;
|
||||
|
|
@ -230,18 +231,20 @@ async fn multi_parent_auth_update_creates_entries_for_all_tokens() {
|
|||
let bh = BlockHash::all_zeros();
|
||||
|
||||
// Two fake token IDs
|
||||
let token_a =
|
||||
TokenID::from_hex("0101010101010101010101010101010101010101010101010101010101010101")
|
||||
.unwrap();
|
||||
let token_b =
|
||||
TokenID::from_hex("0202020202020202020202020202020202020202020202020202020202020202")
|
||||
.unwrap();
|
||||
let token_a = "0101010101010101010101010101010101010101010101010101010101010101"
|
||||
.parse::<TokenID>()
|
||||
.unwrap();
|
||||
let token_b = "0202020202020202020202020202020202020202020202020202020202020202"
|
||||
.parse::<TokenID>()
|
||||
.unwrap();
|
||||
|
||||
// Two fake previous txids (current heads for A and B)
|
||||
let prev_txid_a =
|
||||
Txid::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
|
||||
let prev_txid_b =
|
||||
Txid::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
|
||||
let prev_txid_a = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
.parse::<Txid>()
|
||||
.unwrap();
|
||||
let prev_txid_b = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
.parse::<Txid>()
|
||||
.unwrap();
|
||||
|
||||
// Their auth-head UTXOs (vout 0)
|
||||
let utxo_a = compute_outpoint_hash(&prev_txid_a, 0);
|
||||
|
|
@ -261,7 +264,7 @@ async fn multi_parent_auth_update_creates_entries_for_all_tokens() {
|
|||
txid: prev_txid_a,
|
||||
vout: 0,
|
||||
},
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0xFFFF_FFFF),
|
||||
..Default::default()
|
||||
};
|
||||
|
|
@ -271,25 +274,25 @@ async fn multi_parent_auth_update_creates_entries_for_all_tokens() {
|
|||
txid: prev_txid_b,
|
||||
vout: 0,
|
||||
},
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0xFFFF_FFFF),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let output0 = TxOut {
|
||||
value: 0,
|
||||
script_pubkey: Script::new(),
|
||||
..Default::default()
|
||||
value: Amount::ZERO,
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
token: None,
|
||||
};
|
||||
|
||||
let update_tx = Transaction {
|
||||
version: 2,
|
||||
lock_time: PackedLockTime(0),
|
||||
version: Version::TWO,
|
||||
lock_time: LockTime::ZERO,
|
||||
input: vec![input_a, input_b],
|
||||
output: vec![output0],
|
||||
};
|
||||
|
||||
let update_txid = update_tx.txid();
|
||||
let update_txid = update_tx.compute_txid();
|
||||
|
||||
// Index this one tx as if it's in a block
|
||||
index_bcmr(&pool, &bh, vec![update_tx]).await.unwrap();
|
||||
|
|
@ -334,9 +337,9 @@ async fn multi_parent_auth_update_creates_entries_for_all_tokens() {
|
|||
|
||||
seen.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
assert_eq!(seen[0].0, token_a.to_hex());
|
||||
assert_eq!(seen[0].0, token_a.to_string());
|
||||
assert_eq!(seen[0].1, 1);
|
||||
assert_eq!(seen[1].0, token_b.to_hex());
|
||||
assert_eq!(seen[1].0, token_b.to_string());
|
||||
assert_eq!(seen[1].1, 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -346,17 +349,21 @@ async fn find_parent_auth_entries_reads_bcmr_data_from_text_column() {
|
|||
// Regression test: bcmr_data is stored as hex-encoded TEXT, not BLOB.
|
||||
// This test ensures find_parent_auth_entries correctly decodes it.
|
||||
use crate::db::bcmr::prepare_tables;
|
||||
use bitcoincash::{OutPoint, PackedLockTime, Sequence, TxIn, TxOut};
|
||||
use bitcoincash::{
|
||||
locktime::absolute::LockTime, transaction::Version, Amount, OutPoint, ScriptBuf, Sequence,
|
||||
TxIn, TxOut,
|
||||
};
|
||||
|
||||
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||
prepare_tables(&pool).await;
|
||||
|
||||
let bh = BlockHash::all_zeros();
|
||||
let token_id =
|
||||
TokenID::from_hex("0101010101010101010101010101010101010101010101010101010101010101")
|
||||
.unwrap();
|
||||
let prev_txid =
|
||||
Txid::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
|
||||
let token_id = "0101010101010101010101010101010101010101010101010101010101010101"
|
||||
.parse::<TokenID>()
|
||||
.unwrap();
|
||||
let prev_txid = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
.parse::<Txid>()
|
||||
.unwrap();
|
||||
let utxo = compute_outpoint_hash(&prev_txid, 0);
|
||||
|
||||
// Sample BCMR OP_RETURN data (the actual bytes that would be in the script)
|
||||
|
|
@ -384,21 +391,21 @@ async fn find_parent_auth_entries_reads_bcmr_data_from_text_column() {
|
|||
|
||||
// Build a tx that spends the auth head
|
||||
let spending_tx = Transaction {
|
||||
version: 2,
|
||||
lock_time: PackedLockTime(0),
|
||||
version: Version::TWO,
|
||||
lock_time: LockTime::ZERO,
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: prev_txid,
|
||||
vout: 0,
|
||||
},
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0xFFFF_FFFF),
|
||||
..Default::default()
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: 0,
|
||||
script_pubkey: Script::new(),
|
||||
..Default::default()
|
||||
value: Amount::ZERO,
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
token: None,
|
||||
}],
|
||||
};
|
||||
|
||||
|
|
@ -507,7 +514,7 @@ mod tests {
|
|||
let expected_txid = *expected_hash;
|
||||
index_bcmr(&pool, &block_hash, sorted_txs).await.unwrap();
|
||||
|
||||
let txid_blob = Txid::from_hex(expected_txid).unwrap().to_blob();
|
||||
let txid_blob = expected_txid.parse::<Txid>().unwrap().to_blob();
|
||||
let row: (i64,) =
|
||||
sqlx::query_as("SELECT COUNT(*) FROM auth_chain_entry WHERE txid = ?")
|
||||
.bind(&txid_blob)
|
||||
|
|
@ -576,7 +583,7 @@ mod tests {
|
|||
let expected_txid = *expected_hash;
|
||||
index_bcmr(&pool, &block_hash, sorted_txs).await.unwrap();
|
||||
|
||||
let txid_blob = Txid::from_hex(expected_txid).unwrap().to_blob();
|
||||
let txid_blob = expected_txid.parse::<Txid>().unwrap().to_blob();
|
||||
let row: (i64,) =
|
||||
sqlx::query_as("SELECT COUNT(*) FROM auth_chain_entry WHERE txid = ?")
|
||||
.bind(&txid_blob)
|
||||
|
|
@ -661,7 +668,7 @@ mod tests {
|
|||
let expected_txid = *expected_hash;
|
||||
index_bcmr(&pool, &block_hash, sorted_txs).await.unwrap();
|
||||
|
||||
let txid_blob = Txid::from_hex(expected_txid).unwrap().to_blob();
|
||||
let txid_blob = expected_txid.parse::<Txid>().unwrap().to_blob();
|
||||
let row: (i64,) =
|
||||
sqlx::query_as("SELECT COUNT(*) FROM auth_chain_entry WHERE txid = ?")
|
||||
.bind(&txid_blob)
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@
|
|||
|
||||
use anyhow::bail;
|
||||
use anyhow::{Context, Result};
|
||||
use bitcoin_hashes::hex::FromHex;
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
|
||||
use bitcoincash::TokenID;
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -160,11 +159,11 @@ fn parse_token_identity_history(
|
|||
.context("category missing from 'token'")?
|
||||
.as_str()
|
||||
.context("'category' field in 'token' not a string")?;
|
||||
if category.to_lowercase() != token_id.to_hex().to_lowercase() {
|
||||
if category.to_lowercase() != token_id.to_string().to_lowercase() {
|
||||
bail!(
|
||||
"Mismatch on 'token'->'category' entry and expected category ({} != {}",
|
||||
category,
|
||||
token_id.to_hex()
|
||||
token_id.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -225,7 +224,7 @@ fn parse_token_identity_history(
|
|||
};
|
||||
|
||||
Ok(ParsedBCMR::new(
|
||||
&token_id.to_hex(),
|
||||
&token_id.to_string(),
|
||||
symbol,
|
||||
decimals,
|
||||
name,
|
||||
|
|
@ -253,8 +252,9 @@ pub fn parse_all_bcmr_identities(
|
|||
|
||||
let mut tokens: Vec<ParsedBCMR> = Vec::default();
|
||||
for (token_id, history) in identities {
|
||||
let token_id =
|
||||
TokenID::from_hex(token_id).context("Invalid token ID in identities object")?;
|
||||
let token_id = token_id
|
||||
.parse::<TokenID>()
|
||||
.context("Invalid token ID in identities object")?;
|
||||
|
||||
let one = match parse_token_identity_history(
|
||||
history,
|
||||
|
|
@ -266,11 +266,7 @@ pub fn parse_all_bcmr_identities(
|
|||
) {
|
||||
Ok(bcmr) => bcmr,
|
||||
Err(e) if ignore_errors => {
|
||||
debug!(
|
||||
"parse bcmr: ignoring token {} due to error {}",
|
||||
token_id.to_hex(),
|
||||
e
|
||||
);
|
||||
debug!("parse bcmr: ignoring token {} due to error {}", token_id, e);
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
|
|
@ -294,7 +290,7 @@ pub fn parse_bcmr_json(
|
|||
.context("BCMR missing 'identities'")?;
|
||||
|
||||
let token_history = identities
|
||||
.get(token_id.to_hex())
|
||||
.get(token_id.to_string())
|
||||
.context("'identities' does not contain token ID")?;
|
||||
|
||||
let latest_revision_opt = bcmr.get("latestRevision").and_then(|v| v.as_str());
|
||||
|
|
@ -313,7 +309,7 @@ pub fn parse_bcmr_json(
|
|||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use bitcoin_hashes::{hex::FromHex, Hash};
|
||||
use bitcoin_hashes::Hash;
|
||||
|
||||
#[test]
|
||||
fn test_bcmr_parse() {
|
||||
|
|
@ -355,11 +351,12 @@ mod tests {
|
|||
}
|
||||
"#;
|
||||
|
||||
let dummy_hash = Some(TokenID::all_zeros().to_hex());
|
||||
let dummy_hash = Some(TokenID::all_zeros().to_string());
|
||||
|
||||
let parsed = parse_bcmr_json(
|
||||
&serde_json::from_str(bcmr).unwrap(),
|
||||
&TokenID::from_hex("83e12eea20b19a9a0906bb0521ff18520db69a4a8136293bafbfca0acb2c2313")
|
||||
&"83e12eea20b19a9a0906bb0521ff18520db69a4a8136293bafbfca0acb2c2313"
|
||||
.parse::<TokenID>()
|
||||
.unwrap(),
|
||||
dummy_hash.clone(),
|
||||
dummy_hash,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
use std::time;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use bitcoin_hashes::{hex::ToHex, sha256, Hash};
|
||||
use bitcoin_hashes::{sha256, Hash};
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn get_url(
|
||||
|
|
@ -28,5 +28,5 @@ pub async fn get_url(
|
|||
|
||||
let content_str = String::from_utf8(bytes.to_vec())?;
|
||||
let hash = sha256::Hash::hash(content_str.as_bytes());
|
||||
Ok((content_str, hash.to_hex()))
|
||||
Ok((content_str, hash.to_string()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ pub fn encode(
|
|||
Network::Scalenet => TESTNET_PREFIX,
|
||||
Network::Regtest => REGNET_PREFIX,
|
||||
Network::Chipnet => TESTNET_PREFIX,
|
||||
_ => TESTNET_PREFIX,
|
||||
};
|
||||
|
||||
// Convert payload to 5 bit array
|
||||
|
|
|
|||
28
src/chain.rs
28
src/chain.rs
|
|
@ -6,8 +6,8 @@
|
|||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use bitcoin_hashes::{hex::ToHex, Hash};
|
||||
use bitcoincash::{consensus::deserialize, BlockHash, BlockHeader};
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::{blockdata::block::Header as BlockHeader, consensus::deserialize, BlockHash};
|
||||
use electrum_client_netagnostic::{Batch, Client, ElectrumApi, Param};
|
||||
use log::{info, trace};
|
||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
||||
|
|
@ -63,6 +63,14 @@ impl BlockUndoer for StoreBlockUndoer {
|
|||
&blockheader.block_hash(),
|
||||
)
|
||||
.await?;
|
||||
if db::cauldron::tokentoken::delete_entries_for_block(
|
||||
&self.db.cauldron_w,
|
||||
&blockheader.block_hash(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
was_indexed = true;
|
||||
}
|
||||
if db::bcmr::delete_entries_for_block(&self.db.bcmr_w, &blockheader.block_hash())
|
||||
.await?
|
||||
{
|
||||
|
|
@ -85,7 +93,7 @@ impl BlockUndoer for StoreBlockUndoer {
|
|||
config_set(
|
||||
&self.db.cauldron_w,
|
||||
KEY_LAST_INDEXED,
|
||||
&blockheader.prev_blockhash.to_hex(),
|
||||
&blockheader.prev_blockhash.to_string(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
@ -186,7 +194,7 @@ impl Chain {
|
|||
}
|
||||
header_chain.push(header_map.remove(prev).context(format!(
|
||||
"missing header {} at height {}",
|
||||
prev.to_hex(),
|
||||
prev,
|
||||
header_chain.last().unwrap().height - 1
|
||||
))?);
|
||||
}
|
||||
|
|
@ -359,7 +367,7 @@ pub fn download_all_headers(electrum: &Client) -> Result<Vec<NewHeader>> {
|
|||
pub fn download_block_header(electrum: &Client, blockhash: &BlockHash) -> Result<NewHeader> {
|
||||
let resp = electrum.raw_call(
|
||||
"blockchain.block.header_verbose",
|
||||
[Param::String(blockhash.to_hex())],
|
||||
[Param::String(blockhash.to_string())],
|
||||
);
|
||||
let header: Value = serde_json::from_str(&resp?.to_string())?;
|
||||
|
||||
|
|
@ -434,14 +442,14 @@ pub fn get_new_headers(
|
|||
mod tests {
|
||||
use super::*;
|
||||
use bitcoincash::consensus::deserialize;
|
||||
use bitcoincash::hashes::hex::{FromHex, ToHex};
|
||||
use bitcoincash::hashes::hex::FromHex;
|
||||
|
||||
#[test]
|
||||
fn test_genesis() {
|
||||
let regtest = Chain::new_regtest();
|
||||
assert_eq!(regtest.height(), 0);
|
||||
assert_eq!(
|
||||
regtest.tip_hash().to_hex(),
|
||||
regtest.tip_hash().to_string(),
|
||||
"0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206"
|
||||
);
|
||||
}
|
||||
|
|
@ -524,7 +532,7 @@ mod tests {
|
|||
}
|
||||
assert_eq!(regtest.height(), 0);
|
||||
assert_eq!(
|
||||
regtest.tip_hash().to_hex(),
|
||||
regtest.tip_hash().to_string(),
|
||||
"0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206"
|
||||
);
|
||||
|
||||
|
|
@ -533,7 +541,7 @@ mod tests {
|
|||
.unwrap();
|
||||
assert_eq!(regtest.height(), 0);
|
||||
assert_eq!(
|
||||
regtest.tip_hash().to_hex(),
|
||||
regtest.tip_hash().to_string(),
|
||||
"0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206"
|
||||
);
|
||||
|
||||
|
|
@ -552,7 +560,7 @@ mod tests {
|
|||
.unwrap();
|
||||
assert_eq!(regtest.height(), height);
|
||||
assert_eq!(
|
||||
regtest.tip_hash().to_hex(),
|
||||
regtest.tip_hash().to_string(),
|
||||
"0e16637fe0700a7c52e9a6eaa58bd6ac7202652103be8f778680c66f51ad2e9b"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
use crate::bcmr::parsedbcmr::{FileMeta, ParsedBCMR, Token, Uris, SOURCE_ON_CHAIN};
|
||||
use crate::db::blob::{display_hex_to_blob, FromBlob, ToBlob};
|
||||
use anyhow::*;
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
|
||||
use bitcoincash::{BlockHash, TokenID, Txid};
|
||||
use log::info;
|
||||
use riftenlabs_defi::chainutil::OutPointHash;
|
||||
|
|
@ -160,7 +160,7 @@ pub async fn insert_authheader(
|
|||
.bind(txid.to_blob())
|
||||
.bind(token_id.to_blob())
|
||||
.bind(height as i64)
|
||||
.bind(bcmr_data.map(|bcmr| bcmr.to_hex()))
|
||||
.bind(bcmr_data.map(|bcmr| hex::encode(&bcmr)))
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
|
@ -252,7 +252,7 @@ pub async fn get_entries_missing_bcmr_download(pool: &SqlitePool) -> Result<Vec<
|
|||
matches
|
||||
.iter()
|
||||
.take(3)
|
||||
.map(|e| (e.token_id.to_hex(), e.height, e.utxo.to_hex()))
|
||||
.map(|e| (e.token_id.to_string(), e.height, e.utxo.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
|
@ -270,7 +270,7 @@ pub async fn insert_bcmr_data(
|
|||
expected_hash, actual_hash
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
info!("Trying to insert {}", utxo.to_hex());
|
||||
info!("Trying to insert {}", utxo);
|
||||
let empty_string = "".to_owned();
|
||||
|
||||
sqlx::query(sql)
|
||||
|
|
@ -477,27 +477,25 @@ pub async fn filter_tokens_with_bcmr(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bitcoin_hashes::hex::FromHex;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_bcmr_failure_multiple_tokens_same_utxo_txid() {
|
||||
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||
prepare_tables(&pool).await;
|
||||
|
||||
let token_a =
|
||||
TokenID::from_hex("0101010101010101010101010101010101010101010101010101010101010101")
|
||||
.unwrap();
|
||||
let token_b =
|
||||
TokenID::from_hex("0202020202020202020202020202020202020202020202020202020202020202")
|
||||
.unwrap();
|
||||
let token_a = "0101010101010101010101010101010101010101010101010101010101010101"
|
||||
.parse::<TokenID>()
|
||||
.unwrap();
|
||||
let token_b = "0202020202020202020202020202020202020202020202020202020202020202"
|
||||
.parse::<TokenID>()
|
||||
.unwrap();
|
||||
|
||||
let utxo = OutPointHash::from_hex(
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
)
|
||||
.unwrap();
|
||||
let txid =
|
||||
Txid::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
|
||||
.unwrap();
|
||||
let utxo = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
.parse::<OutPointHash>()
|
||||
.unwrap();
|
||||
let txid = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
.parse::<Txid>()
|
||||
.unwrap();
|
||||
|
||||
update_bcmr_failure(&pool, &utxo, &txid, &token_a, "error A", false)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -14,17 +14,18 @@
|
|||
//! reverse order compared to how they're stored internally.
|
||||
//!
|
||||
//! This means:
|
||||
//! - `Txid::from_hex(hex)` reverses the input bytes
|
||||
//! - `hex.parse::<Txid>()` reverses the input bytes
|
||||
//! - `Txid::to_hex()` reverses the output bytes
|
||||
//! - `Txid::into_inner()` / `Txid::from_inner()` work with internal (non-reversed) bytes
|
||||
//! - `Txid::into_inner()` / `Txid::from_byte_array()` work with internal (non-reversed) bytes
|
||||
//!
|
||||
//! **NEVER use `hex::decode()` directly on user-provided hash hex strings!**
|
||||
//! Instead, use `display_hex_to_blob::<TokenID>(hex)` or `TokenID::from_hex(hex)?.to_blob()`.
|
||||
//! Instead, use `display_hex_to_blob::<TokenID>(hex)` or `hex.parse::<TokenID>()?.to_blob()`.
|
||||
//!
|
||||
//! **PubkeyHash is the exception** - it does NOT reverse bytes.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use bitcoin_hashes::hex::{FromHex, ToHex};
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::{BlockHash, PubkeyHash, TokenID, Txid};
|
||||
use riftenlabs_defi::chainutil::OutPointHash;
|
||||
|
|
@ -44,38 +45,38 @@ pub trait FromBlob: Sized {
|
|||
// Implement ToBlob for 32-byte hash types
|
||||
impl ToBlob for Txid {
|
||||
fn to_blob(&self) -> Vec<u8> {
|
||||
self.into_inner().to_vec()
|
||||
self.as_byte_array().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToBlob for BlockHash {
|
||||
fn to_blob(&self) -> Vec<u8> {
|
||||
self.into_inner().to_vec()
|
||||
self.as_byte_array().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToBlob for TokenID {
|
||||
fn to_blob(&self) -> Vec<u8> {
|
||||
self.into_inner().to_vec()
|
||||
self.as_byte_array().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToBlob for OutPointHash {
|
||||
fn to_blob(&self) -> Vec<u8> {
|
||||
self.into_inner().to_vec()
|
||||
self.as_byte_array().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToBlob for PoolID {
|
||||
fn to_blob(&self) -> Vec<u8> {
|
||||
self.into_inner().to_vec()
|
||||
self.as_byte_array().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
// Implement ToBlob for 20-byte PubkeyHash
|
||||
impl ToBlob for PubkeyHash {
|
||||
fn to_blob(&self) -> Vec<u8> {
|
||||
self.into_inner().to_vec()
|
||||
self.as_byte_array().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +84,7 @@ impl ToBlob for PubkeyHash {
|
|||
impl FromBlob for Txid {
|
||||
fn from_blob(bytes: &[u8]) -> Result<Self> {
|
||||
let arr: [u8; 32] = bytes.try_into().context("Txid blob must be 32 bytes")?;
|
||||
Ok(Txid::from_inner(arr))
|
||||
Ok(Txid::from_byte_array(arr))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,14 +93,14 @@ impl FromBlob for BlockHash {
|
|||
let arr: [u8; 32] = bytes
|
||||
.try_into()
|
||||
.context("BlockHash blob must be 32 bytes")?;
|
||||
Ok(BlockHash::from_inner(arr))
|
||||
Ok(BlockHash::from_byte_array(arr))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromBlob for TokenID {
|
||||
fn from_blob(bytes: &[u8]) -> Result<Self> {
|
||||
let arr: [u8; 32] = bytes.try_into().context("TokenID blob must be 32 bytes")?;
|
||||
Ok(TokenID::from_inner(arr))
|
||||
Ok(TokenID::from_byte_array(arr))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,14 +109,14 @@ impl FromBlob for OutPointHash {
|
|||
let arr: [u8; 32] = bytes
|
||||
.try_into()
|
||||
.context("OutPointHash blob must be 32 bytes")?;
|
||||
Ok(OutPointHash::from_inner(arr))
|
||||
Ok(OutPointHash::from_byte_array(arr))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromBlob for PoolID {
|
||||
fn from_blob(bytes: &[u8]) -> Result<Self> {
|
||||
let arr: [u8; 32] = bytes.try_into().context("PoolID blob must be 32 bytes")?;
|
||||
Ok(PoolID::from_inner(arr))
|
||||
Ok(PoolID::from_byte_array(arr))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +126,7 @@ impl FromBlob for PubkeyHash {
|
|||
let arr: [u8; 20] = bytes
|
||||
.try_into()
|
||||
.context("PubkeyHash blob must be 20 bytes")?;
|
||||
Ok(PubkeyHash::from_inner(arr))
|
||||
Ok(PubkeyHash::from_byte_array(arr))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,9 +144,9 @@ impl FromBlob for PubkeyHash {
|
|||
/// ```
|
||||
pub fn display_hex_to_blob<T>(hex: &str) -> Result<Vec<u8>>
|
||||
where
|
||||
T: FromHex + ToBlob,
|
||||
T: FromStr + ToBlob,
|
||||
{
|
||||
let hash = T::from_hex(hex).map_err(|e| anyhow::anyhow!("invalid hash hex: {}", e))?;
|
||||
let hash = T::from_str(hex).map_err(|_| anyhow::anyhow!("invalid hash hex"))?;
|
||||
Ok(hash.to_blob())
|
||||
}
|
||||
|
||||
|
|
@ -162,32 +163,31 @@ where
|
|||
/// ```
|
||||
pub fn blob_to_display_hex<T>(blob: &[u8]) -> Result<String>
|
||||
where
|
||||
T: FromBlob + ToHex,
|
||||
T: FromBlob + std::fmt::Display,
|
||||
{
|
||||
let hash = T::from_blob(blob)?;
|
||||
Ok(hash.to_hex())
|
||||
Ok(hash.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bitcoin_hashes::hex::{FromHex, ToHex};
|
||||
|
||||
#[test]
|
||||
fn test_txid_roundtrip() {
|
||||
let hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
let txid = Txid::from_hex(hex).unwrap();
|
||||
let txid = hex.parse::<Txid>().unwrap();
|
||||
let blob = txid.to_blob();
|
||||
assert_eq!(blob.len(), 32);
|
||||
let recovered = Txid::from_blob(&blob).unwrap();
|
||||
assert_eq!(txid, recovered);
|
||||
assert_eq!(recovered.to_hex(), hex);
|
||||
assert_eq!(recovered.to_string(), hex);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blockhash_roundtrip() {
|
||||
let hex = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210";
|
||||
let hash = BlockHash::from_hex(hex).unwrap();
|
||||
let hash = hex.parse::<BlockHash>().unwrap();
|
||||
let blob = hash.to_blob();
|
||||
assert_eq!(blob.len(), 32);
|
||||
let recovered = BlockHash::from_blob(&blob).unwrap();
|
||||
|
|
@ -197,7 +197,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_token_id_roundtrip() {
|
||||
let hex = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
|
||||
let token = TokenID::from_hex(hex).unwrap();
|
||||
let token = hex.parse::<TokenID>().unwrap();
|
||||
let blob = token.to_blob();
|
||||
assert_eq!(blob.len(), 32);
|
||||
let recovered = TokenID::from_blob(&blob).unwrap();
|
||||
|
|
@ -207,7 +207,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_outpointhash_roundtrip() {
|
||||
let hex = "1111111111111111111111111111111111111111111111111111111111111111";
|
||||
let hash = OutPointHash::from_hex(hex).unwrap();
|
||||
let hash = hex.parse::<OutPointHash>().unwrap();
|
||||
let blob = hash.to_blob();
|
||||
assert_eq!(blob.len(), 32);
|
||||
let recovered = OutPointHash::from_blob(&blob).unwrap();
|
||||
|
|
@ -217,7 +217,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_pubkeyhash_roundtrip() {
|
||||
let hex = "0123456789abcdef01230123456789abcdef0123";
|
||||
let pkh = PubkeyHash::from_hex(hex).unwrap();
|
||||
let pkh = hex.parse::<PubkeyHash>().unwrap();
|
||||
let blob = pkh.to_blob();
|
||||
assert_eq!(blob.len(), 20);
|
||||
let recovered = PubkeyHash::from_blob(&blob).unwrap();
|
||||
|
|
@ -248,7 +248,7 @@ mod tests {
|
|||
let raw_bytes = hex::decode(display_hex).unwrap();
|
||||
|
||||
// CORRECT: parse as TokenID then get blob (handles reversal)
|
||||
let token = TokenID::from_hex(display_hex).unwrap();
|
||||
let token = display_hex.parse::<TokenID>().unwrap();
|
||||
let blob_bytes = token.to_blob();
|
||||
|
||||
// These MUST be different because Bitcoin hashes reverse bytes for display
|
||||
|
|
@ -274,7 +274,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_sql_hex_vs_to_hex() {
|
||||
let display_hex = "0102030405060708091011121314151617181920212223242526272829303132";
|
||||
let token = TokenID::from_hex(display_hex).unwrap();
|
||||
let token = display_hex.parse::<TokenID>().unwrap();
|
||||
let blob = token.to_blob();
|
||||
|
||||
// Simulate what SQL hex() does: just uppercase hex of raw bytes (no reversal)
|
||||
|
|
@ -289,7 +289,7 @@ mod tests {
|
|||
// Correct way: parse blob back to TokenID and use to_hex()
|
||||
let recovered = TokenID::from_blob(&blob).unwrap();
|
||||
assert_eq!(
|
||||
recovered.to_hex(),
|
||||
recovered.to_string(),
|
||||
display_hex,
|
||||
"to_hex() should match original display format"
|
||||
);
|
||||
|
|
@ -302,7 +302,7 @@ mod tests {
|
|||
|
||||
// Helper should produce same result as manual from_hex + to_blob
|
||||
let helper_result = display_hex_to_blob::<TokenID>(display_hex).unwrap();
|
||||
let manual_result = TokenID::from_hex(display_hex).unwrap().to_blob();
|
||||
let manual_result = display_hex.parse::<TokenID>().unwrap().to_blob();
|
||||
|
||||
assert_eq!(helper_result, manual_result);
|
||||
|
||||
|
|
@ -315,7 +315,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_blob_to_display_hex_helper() {
|
||||
let display_hex = "0102030405060708091011121314151617181920212223242526272829303132";
|
||||
let token = TokenID::from_hex(display_hex).unwrap();
|
||||
let token = display_hex.parse::<TokenID>().unwrap();
|
||||
let blob = token.to_blob();
|
||||
|
||||
// Helper should produce correct display format
|
||||
|
|
@ -334,7 +334,7 @@ mod tests {
|
|||
|
||||
// For PubkeyHash, raw hex::decode SHOULD equal to_blob
|
||||
let raw_bytes = hex::decode(hex_str).unwrap();
|
||||
let pkh = PubkeyHash::from_hex(hex_str).unwrap();
|
||||
let pkh = hex_str.parse::<PubkeyHash>().unwrap();
|
||||
let blob_bytes = pkh.to_blob();
|
||||
|
||||
assert_eq!(raw_bytes, blob_bytes, "PubkeyHash should NOT reverse bytes");
|
||||
|
|
@ -347,23 +347,23 @@ mod tests {
|
|||
let raw_bytes = hex::decode(display_hex).unwrap();
|
||||
|
||||
// Txid reverses
|
||||
let txid_blob = Txid::from_hex(display_hex).unwrap().to_blob();
|
||||
let txid_blob = display_hex.parse::<Txid>().unwrap().to_blob();
|
||||
assert_ne!(raw_bytes, txid_blob, "Txid should reverse");
|
||||
|
||||
// BlockHash reverses
|
||||
let blockhash_blob = BlockHash::from_hex(display_hex).unwrap().to_blob();
|
||||
let blockhash_blob = display_hex.parse::<BlockHash>().unwrap().to_blob();
|
||||
assert_ne!(raw_bytes, blockhash_blob, "BlockHash should reverse");
|
||||
|
||||
// TokenID reverses
|
||||
let tokenid_blob = TokenID::from_hex(display_hex).unwrap().to_blob();
|
||||
let tokenid_blob = display_hex.parse::<TokenID>().unwrap().to_blob();
|
||||
assert_ne!(raw_bytes, tokenid_blob, "TokenID should reverse");
|
||||
|
||||
// OutPointHash reverses
|
||||
let outpoint_blob = OutPointHash::from_hex(display_hex).unwrap().to_blob();
|
||||
let outpoint_blob = display_hex.parse::<OutPointHash>().unwrap().to_blob();
|
||||
assert_ne!(raw_bytes, outpoint_blob, "OutPointHash should reverse");
|
||||
|
||||
// PoolID reverses
|
||||
let poolid_blob = PoolID::from_hex(display_hex).unwrap().to_blob();
|
||||
let poolid_blob = display_hex.parse::<PoolID>().unwrap().to_blob();
|
||||
assert_ne!(raw_bytes, poolid_blob, "PoolID should reverse");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@
|
|||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
use bitcoincash::consensus::serialize;
|
||||
use bitcoincash::{consensus::deserialize, BlockHash, BlockHeader};
|
||||
use bitcoincash::{blockdata::block::Header as BlockHeader, consensus::deserialize, BlockHash};
|
||||
use sqlx::{SqliteConnection, SqlitePool};
|
||||
|
||||
use crate::chain::NewHeader;
|
||||
|
|
@ -30,7 +29,7 @@ pub async fn load_all_headers(pool: &SqlitePool) -> Result<Vec<(BlockHeader, u64
|
|||
|
||||
pub async fn db_get_header(pool: &SqlitePool, blockhash: &BlockHash) -> Result<BlockHeader> {
|
||||
let row: Option<(Vec<u8>,)> = sqlx::query_as("SELECT header FROM headers WHERE key = ?")
|
||||
.bind(blockhash.to_hex())
|
||||
.bind(blockhash.to_string())
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
|
|
@ -47,7 +46,7 @@ pub async fn db_get_header(pool: &SqlitePool, blockhash: &BlockHash) -> Result<B
|
|||
pub async fn store_headers(conn: &mut SqliteConnection, headers: &[NewHeader]) -> Result<()> {
|
||||
for h in headers {
|
||||
sqlx::query("INSERT OR IGNORE INTO headers (key, height, header) VALUES (?, ?, ?)")
|
||||
.bind(h.hash.to_hex())
|
||||
.bind(h.hash.to_string())
|
||||
.bind(h.height as i64)
|
||||
.bind(serialize(&h.header))
|
||||
.execute(&mut *conn)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ pub mod ohlcv;
|
|||
pub mod pool;
|
||||
pub mod poolvisitor;
|
||||
pub mod tokenlist;
|
||||
pub mod tokentoken;
|
||||
pub mod tx;
|
||||
pub mod user;
|
||||
pub mod utxo_funding;
|
||||
|
|
@ -45,6 +46,7 @@ pub async fn prepare_tables(pool: &SqlitePool) {
|
|||
.unwrap();
|
||||
|
||||
pool::create_table(pool).await;
|
||||
tokentoken::create_table(pool).await;
|
||||
|
||||
sqlx::query(
|
||||
"CREATE INDEX idx_utxo_funding_join ON utxo_funding(new_utxo_hash, sats, token_id);",
|
||||
|
|
|
|||
|
|
@ -204,6 +204,52 @@ JOIN vol ON ohlc.token_id = vol.token_id AND ohlc.bucket_ts = vol.bucket_ts
|
|||
Ok(inserted)
|
||||
}
|
||||
|
||||
pub struct OhlcvRow {
|
||||
pub bucket_ts: i64,
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
pub volume_sats: i64,
|
||||
pub volume_tokens: i64,
|
||||
pub tx_count: i64,
|
||||
}
|
||||
|
||||
/// Return the materialised 1-hour candles for a single token in `[start, end)`.
|
||||
/// Only buckets that had at least one trade are returned (gaps must be filled by the caller).
|
||||
pub async fn get_active_candles(
|
||||
pool: &SqlitePool,
|
||||
token_blob: &[u8],
|
||||
start: i64,
|
||||
end: i64,
|
||||
) -> Result<Vec<OhlcvRow>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT bucket_ts, open, high, low, close, volume_sats, volume_tokens, tx_count
|
||||
FROM ohlcv_1h
|
||||
WHERE token_id = ? AND bucket_ts >= ? AND bucket_ts < ?
|
||||
ORDER BY bucket_ts ASC",
|
||||
)
|
||||
.bind(token_blob)
|
||||
.bind(start)
|
||||
.bind(end)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| OhlcvRow {
|
||||
bucket_ts: r.get(0),
|
||||
open: r.get(1),
|
||||
high: r.get(2),
|
||||
low: r.get(3),
|
||||
close: r.get(4),
|
||||
volume_sats: r.get(5),
|
||||
volume_tokens: r.get(6),
|
||||
tx_count: r.get(7),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -401,49 +447,3 @@ mod tests {
|
|||
assert_eq!(n, 0, "mempool trades must not be materialised");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OhlcvRow {
|
||||
pub bucket_ts: i64,
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
pub volume_sats: i64,
|
||||
pub volume_tokens: i64,
|
||||
pub tx_count: i64,
|
||||
}
|
||||
|
||||
/// Return the materialised 1-hour candles for a single token in `[start, end)`.
|
||||
/// Only buckets that had at least one trade are returned (gaps must be filled by the caller).
|
||||
pub async fn get_active_candles(
|
||||
pool: &SqlitePool,
|
||||
token_blob: &[u8],
|
||||
start: i64,
|
||||
end: i64,
|
||||
) -> Result<Vec<OhlcvRow>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT bucket_ts, open, high, low, close, volume_sats, volume_tokens, tx_count
|
||||
FROM ohlcv_1h
|
||||
WHERE token_id = ? AND bucket_ts >= ? AND bucket_ts < ?
|
||||
ORDER BY bucket_ts ASC",
|
||||
)
|
||||
.bind(token_blob)
|
||||
.bind(start)
|
||||
.bind(end)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| OhlcvRow {
|
||||
bucket_ts: r.get(0),
|
||||
open: r.get(1),
|
||||
high: r.get(2),
|
||||
low: r.get(3),
|
||||
close: r.get(4),
|
||||
volume_sats: r.get(5),
|
||||
volume_tokens: r.get(6),
|
||||
tx_count: r.get(7),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use std::{
|
|||
use crate::db::blob::{blob_to_display_hex, display_hex_to_blob, FromBlob, ToBlob};
|
||||
use crate::def::PoolID;
|
||||
use anyhow::{Context, Result};
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
|
||||
use bitcoincash::TokenID;
|
||||
use log::{debug, info, warn};
|
||||
use malachite::Integer;
|
||||
|
|
@ -240,19 +240,12 @@ pub async fn update_pool_history(
|
|||
};
|
||||
|
||||
if current.is_withdrawn {
|
||||
info!(
|
||||
"LP {} withdrawn in {}",
|
||||
pool_utxo.to_hex(),
|
||||
current.spent_utxo_hash.to_hex()
|
||||
);
|
||||
info!("LP {} withdrawn in {}", pool_utxo, current.spent_utxo_hash);
|
||||
flag_as_withdrawn(&mut *conn, &pool_utxo, ¤t).await?;
|
||||
} else if is_new {
|
||||
info!(
|
||||
"Cauldron LP created in tx {}",
|
||||
current
|
||||
.new_utxo_txid
|
||||
.expect("expected txid in new LP")
|
||||
.to_hex()
|
||||
current.new_utxo_txid.expect("expected txid in new LP")
|
||||
);
|
||||
insert_new_pool(&mut *conn, ¤t).await?;
|
||||
insert_pool_history_entry(
|
||||
|
|
@ -266,7 +259,7 @@ pub async fn update_pool_history(
|
|||
)
|
||||
.await?;
|
||||
} else {
|
||||
debug!("New entry for pool {}", pool_utxo.to_hex());
|
||||
debug!("New entry for pool {}", pool_utxo);
|
||||
let prev_entry = get_pool_history_entry(&mut *conn, current.spent_utxo_hash).await?;
|
||||
let sats_delta = if let Some(current_sats) = current.sats {
|
||||
current_sats as i64 - prev_entry.sats as i64
|
||||
|
|
@ -840,7 +833,7 @@ mod tests {
|
|||
use crate::db::cauldron::prepare_tables;
|
||||
use crate::db::cauldron::tx::{insert_block_tx, insert_mempool_tx};
|
||||
use crate::db::cauldron::utxo_funding::insert_utxo_funding;
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::{BlockHash, PubkeyHash, Txid};
|
||||
use riftenlabs_defi::cauldron::ParsedContract;
|
||||
|
|
@ -879,16 +872,16 @@ mod tests {
|
|||
|
||||
// Use the hex representation to seed the buffer if into_inner() is missing
|
||||
// This ensures a unique, deterministic seed for each owner in the test
|
||||
let pkh_hex = owner_pkh.to_hex();
|
||||
let pkh_hex = owner_pkh.to_string();
|
||||
let pkh_bytes = hex::decode(pkh_hex).unwrap();
|
||||
seed[0..20].copy_from_slice(&pkh_bytes);
|
||||
|
||||
let pool_utxo_0 = OutPointHash::from_inner(seed);
|
||||
let pool_utxo_0 = OutPointHash::from_byte_array(seed);
|
||||
let mut seed_tx = seed;
|
||||
seed_tx[31] = 0xFF; // slightly different for txid
|
||||
let txid0 = Txid::from_inner(seed_tx);
|
||||
let txid0 = Txid::from_byte_array(seed_tx);
|
||||
|
||||
let txid1 = Txid::from_inner(seed); // different enough for test
|
||||
let txid1 = Txid::from_byte_array(seed); // different enough for test
|
||||
|
||||
let contract0 = ParsedContract {
|
||||
pkh: owner_pkh,
|
||||
|
|
@ -926,7 +919,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// Use a different UTXO for the second entry in the history to satisfy DB constraints
|
||||
let pool_utxo_1 = OutPointHash::from_inner(seed_tx);
|
||||
let pool_utxo_1 = OutPointHash::from_byte_array(seed_tx);
|
||||
let contract1 = ParsedContract {
|
||||
pkh: owner_pkh,
|
||||
is_withdrawn: false,
|
||||
|
|
@ -987,14 +980,14 @@ mod tests {
|
|||
let db_pool = test_pool().await;
|
||||
setup_test_db(&db_pool).await;
|
||||
|
||||
let token = TokenID::from_inner([0x01; 32]);
|
||||
let token = TokenID::from_byte_array([0x01; 32]);
|
||||
let owner = PubkeyHash::all_zeros();
|
||||
let t0 = 1700000000i64;
|
||||
let t1 = 1700001000i64;
|
||||
|
||||
seed_test_pool(&db_pool, token, owner, t0, t1, 1000, 500).await;
|
||||
|
||||
let token_hex = token.to_hex();
|
||||
let token_hex = token.to_string();
|
||||
let result =
|
||||
get_pool_period_snapshot(&db_pool, Some(&token_hex), None, t0 - 100, t1 + 100).await;
|
||||
|
||||
|
|
@ -1010,14 +1003,14 @@ mod tests {
|
|||
let db_pool = test_pool().await;
|
||||
setup_test_db(&db_pool).await;
|
||||
|
||||
let token = TokenID::from_inner([0x02; 32]);
|
||||
let owner = PubkeyHash::from_inner([0x11; 20]);
|
||||
let token = TokenID::from_byte_array([0x02; 32]);
|
||||
let owner = PubkeyHash::from_byte_array([0x11; 20]);
|
||||
let t0 = 1700000000i64;
|
||||
let t1 = 1700001000i64;
|
||||
|
||||
seed_test_pool(&db_pool, token, owner, t0, t1, 2000, 1000).await;
|
||||
|
||||
let pkh_hex = owner.to_hex();
|
||||
let pkh_hex = owner.to_string();
|
||||
let result =
|
||||
get_pool_period_snapshot(&db_pool, None, Some(&pkh_hex), t0 - 100, t1 + 100).await;
|
||||
|
||||
|
|
@ -1033,12 +1026,12 @@ mod tests {
|
|||
let db_pool = test_pool().await;
|
||||
setup_test_db(&db_pool).await;
|
||||
|
||||
let token = TokenID::from_inner([0x02; 32]);
|
||||
let token = TokenID::from_byte_array([0x02; 32]);
|
||||
let t0 = 1700000000i64;
|
||||
let t1 = 1700001000i64;
|
||||
|
||||
let owner_1 = PubkeyHash::from_inner([0x11; 20]);
|
||||
let owner_2 = PubkeyHash::from_inner([0x22; 20]);
|
||||
let owner_1 = PubkeyHash::from_byte_array([0x11; 20]);
|
||||
let owner_2 = PubkeyHash::from_byte_array([0x22; 20]);
|
||||
|
||||
let pool_id_1 = seed_test_pool(&db_pool, token, owner_1, t0, t1, 2000, 1000).await;
|
||||
let pool_id_2 = seed_test_pool(&db_pool, token, owner_2, t0, t1, 3000, 1500).await;
|
||||
|
|
@ -1046,7 +1039,7 @@ mod tests {
|
|||
// Filter by a single pool ID
|
||||
let result = get_pool_period_snapshot_by_pool_ids(
|
||||
&db_pool,
|
||||
&[pool_id_1.to_hex()],
|
||||
&[pool_id_1.to_string()],
|
||||
t0 - 100,
|
||||
t1 + 100,
|
||||
)
|
||||
|
|
@ -1057,7 +1050,7 @@ mod tests {
|
|||
// Filter by both pool IDs
|
||||
let result = get_pool_period_snapshot_by_pool_ids(
|
||||
&db_pool,
|
||||
&[pool_id_1.to_hex(), pool_id_2.to_hex()],
|
||||
&[pool_id_1.to_string(), pool_id_2.to_string()],
|
||||
t0 - 100,
|
||||
t1 + 100,
|
||||
)
|
||||
|
|
@ -1071,7 +1064,7 @@ mod tests {
|
|||
let db_pool = test_pool().await;
|
||||
setup_test_db(&db_pool).await;
|
||||
|
||||
let token = TokenID::from_inner([0x03; 32]);
|
||||
let token = TokenID::from_byte_array([0x03; 32]);
|
||||
let owner = PubkeyHash::all_zeros();
|
||||
let t0 = 1700000000i64;
|
||||
let t1 = 1700001000i64;
|
||||
|
|
@ -1099,7 +1092,7 @@ mod tests {
|
|||
|
||||
assert_eq!(
|
||||
start_pool.pool_id,
|
||||
expected_pool_id.to_hex(),
|
||||
expected_pool_id.to_string(),
|
||||
"pool_id should match expected"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ use crate::rpc::tvl::TvlByTokenVisitor;
|
|||
use crate::signal::shutdown_requested;
|
||||
use crate::timeutil::time_now;
|
||||
|
||||
use bitcoin_hashes::hex::FromHex;
|
||||
use bitcoincash::TokenID;
|
||||
use log::{error, info, warn};
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -487,7 +486,7 @@ pub async fn update_changes_score_volume_and_ranking(db: &DB) -> anyhow::Result<
|
|||
};
|
||||
|
||||
// We'll need TokenID for historical probes
|
||||
let tid = match TokenID::from_hex(&token_id) {
|
||||
let tid = match token_id.parse::<TokenID>() {
|
||||
Ok(t) => t,
|
||||
Err(_) => {
|
||||
// still update the "now" fields and basics
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ mod tests {
|
|||
};
|
||||
use crate::utiltest::mock_db_pool;
|
||||
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::{BlockHash, PubkeyHash, TokenID, Txid};
|
||||
use riftenlabs_defi::{cauldron::ParsedContract, chainutil::OutPointHash};
|
||||
|
|
@ -56,9 +55,9 @@ mod tests {
|
|||
insert_delphi_entry(
|
||||
&pool,
|
||||
&DelphiEntry {
|
||||
txid: Txid::hash(b"test_oracle_tx").to_hex(),
|
||||
blockhash: BlockHash::hash(b"test_oracle_block").to_hex(),
|
||||
token_id: BlockHash::hash(b"test_oracle_token").to_hex(),
|
||||
txid: Txid::hash(b"test_oracle_tx").to_string(),
|
||||
blockhash: BlockHash::hash(b"test_oracle_block").to_string(),
|
||||
token_id: BlockHash::hash(b"test_oracle_token").to_string(),
|
||||
oracle_timestamp: 0,
|
||||
oracle_price: 50000,
|
||||
oracle_sequence: 1,
|
||||
|
|
@ -82,17 +81,17 @@ mod tests {
|
|||
// Derive unique hashes per-token by embedding the token's own bytes.
|
||||
// Single-byte seeds like 0xA0+seed collide when different tokens yield the same
|
||||
// wrapping result, so use the full 32-byte token identity instead.
|
||||
let token_bytes = token.into_inner();
|
||||
let token_bytes = token.to_byte_array();
|
||||
let make_hash = |prefix: u8| {
|
||||
let mut h = token_bytes;
|
||||
h[0] = h[0].wrapping_add(prefix);
|
||||
h
|
||||
};
|
||||
|
||||
let pool_utxo_0 = OutPointHash::from_inner(make_hash(0xA0));
|
||||
let pool_utxo_1 = OutPointHash::from_inner(make_hash(0xB0));
|
||||
let txid0 = Txid::from_inner(make_hash(0xF0));
|
||||
let txid1 = Txid::from_inner(make_hash(0xF1));
|
||||
let pool_utxo_0 = OutPointHash::from_byte_array(make_hash(0xA0));
|
||||
let pool_utxo_1 = OutPointHash::from_byte_array(make_hash(0xB0));
|
||||
let txid0 = Txid::from_byte_array(make_hash(0xF0));
|
||||
let txid1 = Txid::from_byte_array(make_hash(0xF1));
|
||||
|
||||
let owner = PubkeyHash::all_zeros();
|
||||
|
||||
|
|
@ -110,18 +109,18 @@ mod tests {
|
|||
sats: Some(sats0),
|
||||
token_amount: Some(toks0),
|
||||
};
|
||||
insert_new_pool(&mut *conn, &c0).await.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![c0.clone()], &txid0)
|
||||
insert_new_pool(&mut conn, &c0).await.unwrap();
|
||||
insert_utxo_funding(&mut conn, &vec![c0.clone()], &txid0)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid0, &BlockHash::all_zeros(), t0)
|
||||
insert_block_tx(&mut conn, &txid0, &BlockHash::all_zeros(), t0)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_mempool_tx(&mut *conn, &txid0, t0 as u64)
|
||||
insert_mempool_tx(&mut conn, &txid0, t0 as u64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool_utxo_0,
|
||||
&c0,
|
||||
Some(t0 as u64),
|
||||
|
|
@ -144,20 +143,20 @@ mod tests {
|
|||
sats: Some(sats1),
|
||||
token_amount: Some(toks1),
|
||||
};
|
||||
insert_utxo_funding(&mut *conn, &vec![c1.clone()], &txid1)
|
||||
insert_utxo_funding(&mut conn, &vec![c1.clone()], &txid1)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_new_pool(&mut *conn, &c1).await.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid1, &BlockHash::all_zeros(), t1)
|
||||
insert_new_pool(&mut conn, &c1).await.unwrap();
|
||||
insert_block_tx(&mut conn, &txid1, &BlockHash::all_zeros(), t1)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_mempool_tx(&mut *conn, &txid1, t1 as u64)
|
||||
insert_mempool_tx(&mut conn, &txid1, t1 as u64)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Trading delta on the old pool (this is what your volume query reads)
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool_utxo_0,
|
||||
&c1,
|
||||
Some(t1 as u64),
|
||||
|
|
@ -170,7 +169,7 @@ mod tests {
|
|||
|
||||
// Zero-delta anchor on the new active pool so TVL is visible
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool_utxo_1,
|
||||
&c1,
|
||||
Some(t1 as u64),
|
||||
|
|
@ -214,9 +213,9 @@ mod tests {
|
|||
let pool = &mock.cauldron_w;
|
||||
|
||||
// on-chain BCMR for token A
|
||||
let token_a = TokenID::from_inner([0x01; 32]);
|
||||
let utxo_a = OutPointHash::from_inner([0xa1; 32]);
|
||||
let txid_a = Txid::from_inner([0x11; 32]);
|
||||
let token_a = TokenID::from_byte_array([0x01; 32]);
|
||||
let utxo_a = OutPointHash::from_byte_array([0xa1; 32]);
|
||||
let txid_a = Txid::from_byte_array([0x11; 32]);
|
||||
let row_a = BCMRRow {
|
||||
name: "OnChainName".to_string(),
|
||||
description: "desc".to_string(),
|
||||
|
|
@ -251,7 +250,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// well-known only for token B
|
||||
let token_b = TokenID::from_inner([0x02; 32]);
|
||||
let token_b = TokenID::from_byte_array([0x02; 32]);
|
||||
sqlx::query(
|
||||
"INSERT INTO bcmr_well_known (token_id, name, symbol, decimals, description, icon, web, source)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
|
|
@ -269,7 +268,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// crc20 only for token C
|
||||
let token_c = TokenID::from_inner([0x03; 32]);
|
||||
let token_c = TokenID::from_byte_array([0x03; 32]);
|
||||
sqlx::query("INSERT INTO crc20 (token_id, name, symbol, decimals) VALUES (?, ?, ?, ?)")
|
||||
.bind(token_c.to_blob())
|
||||
.bind("CRCName")
|
||||
|
|
@ -280,24 +279,24 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// A → on-chain wins
|
||||
assert_eq!(resolve_decimals(pool, pool, &token_a.to_hex()).await, 8);
|
||||
let (dn_a, sym_a) = resolve_display_labels(pool, pool, &token_a.to_hex())
|
||||
assert_eq!(resolve_decimals(pool, pool, &token_a.to_string()).await, 8);
|
||||
let (dn_a, sym_a) = resolve_display_labels(pool, pool, &token_a.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dn_a, "OnChainName");
|
||||
assert_eq!(sym_a, "OC");
|
||||
|
||||
// B → well-known wins
|
||||
assert_eq!(resolve_decimals(pool, pool, &token_b.to_hex()).await, 2);
|
||||
let (dn_b, sym_b) = resolve_display_labels(pool, pool, &token_b.to_hex())
|
||||
assert_eq!(resolve_decimals(pool, pool, &token_b.to_string()).await, 2);
|
||||
let (dn_b, sym_b) = resolve_display_labels(pool, pool, &token_b.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dn_b, "WKName");
|
||||
assert_eq!(sym_b, "WKS");
|
||||
|
||||
// C → crc20 fallback
|
||||
assert_eq!(resolve_decimals(pool, pool, &token_c.to_hex()).await, 6);
|
||||
let (dn_c, sym_c) = resolve_display_labels(pool, pool, &token_c.to_hex())
|
||||
assert_eq!(resolve_decimals(pool, pool, &token_c.to_string()).await, 6);
|
||||
let (dn_c, sym_c) = resolve_display_labels(pool, pool, &token_c.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dn_c, "CRCName");
|
||||
|
|
@ -371,9 +370,9 @@ mod tests {
|
|||
let mock = mock_db_pool(|pool| async move { setup_basic_schemas(pool).await }).await;
|
||||
let pool = &mock.cauldron_w;
|
||||
|
||||
let t1 = TokenID::from_inner([0x10; 32]).to_hex();
|
||||
let t2 = TokenID::from_inner([0x20; 32]).to_hex();
|
||||
let t3_zero_tvl = TokenID::from_inner([0x30; 32]).to_hex();
|
||||
let t1 = TokenID::from_byte_array([0x10; 32]).to_string();
|
||||
let t2 = TokenID::from_byte_array([0x20; 32]).to_string();
|
||||
let t3_zero_tvl = TokenID::from_byte_array([0x30; 32]).to_string();
|
||||
|
||||
// seed 3 rows (one with zero TVL that should be filtered out by reader)
|
||||
seed_cached_row(
|
||||
|
|
@ -437,9 +436,9 @@ mod tests {
|
|||
let mock = mock_db_pool(|pool| async move { setup_basic_schemas(pool).await }).await;
|
||||
let pool = &mock.cauldron_w;
|
||||
|
||||
let t1 = TokenID::from_inner([0x41; 32]).to_hex();
|
||||
let t2 = TokenID::from_inner([0x42; 32]).to_hex();
|
||||
let t3 = TokenID::from_inner([0x43; 32]).to_hex();
|
||||
let t1 = TokenID::from_byte_array([0x41; 32]).to_string();
|
||||
let t2 = TokenID::from_byte_array([0x42; 32]).to_string();
|
||||
let t3 = TokenID::from_byte_array([0x43; 32]).to_string();
|
||||
|
||||
seed_cached_row(
|
||||
pool, &t1, 1, 1, 10, 10, "A", "A", 5.0, 4.0, 3.0, 50, 40, 0.5, 0.4, 0.3, 500, 400, 200,
|
||||
|
|
@ -479,7 +478,7 @@ mod tests {
|
|||
let mock = mock_db_pool(|pool| async move { setup_basic_schemas(pool).await }).await;
|
||||
let pool = &mock.cauldron_r;
|
||||
|
||||
let token = TokenID::from_inner([0x77; 32]).to_hex();
|
||||
let token = TokenID::from_byte_array([0x77; 32]).to_string();
|
||||
let v = apy_30d_bp_for_token(pool, &token, time_now())
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -592,7 +591,7 @@ mod tests {
|
|||
let pool = &mock.cauldron_w;
|
||||
|
||||
// Minimal fake: create a row in cached_token_metrics by hand (simulating the fast path)
|
||||
let tok_a = TokenID::from_inner([0x11; 32]);
|
||||
let tok_a = TokenID::from_byte_array([0x11; 32]);
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO cached_token_metrics(token_id, trade_volume, tvl_sats, tvl_tokens, score,
|
||||
|
|
@ -639,7 +638,7 @@ mod tests {
|
|||
let pool = &mock.cauldron_w;
|
||||
|
||||
// Create a cache row with tvl so it's included
|
||||
let tok_b = TokenID::from_inner([0x22; 32]);
|
||||
let tok_b = TokenID::from_byte_array([0x22; 32]);
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO cached_token_metrics(token_id, trade_volume, tvl_sats, tvl_tokens, score,
|
||||
|
|
@ -677,7 +676,7 @@ mod tests {
|
|||
// Seed a YOUNG token with *volume* so we do the full path, but with no 24h anchor.
|
||||
// Make both events within the last hour.
|
||||
let now = time_now();
|
||||
let token = TokenID::from_inner([0xCC; 32]);
|
||||
let token = TokenID::from_byte_array([0xCC; 32]);
|
||||
let t0 = now - 3600; // 1h ago
|
||||
let t1 = now - 600; // 10m ago
|
||||
seed_minimal_token_history(pool, token, t0, t1, 1_000, 500, 2_000, 1_000).await;
|
||||
|
|
@ -724,7 +723,7 @@ mod tests {
|
|||
let pool = &mock.cauldron_w;
|
||||
|
||||
// Seed a cached row with some tvl so it exists initially
|
||||
let tok_remove = TokenID::from_inner([0x33; 32]);
|
||||
let tok_remove = TokenID::from_byte_array([0x33; 32]);
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO cached_token_metrics
|
||||
|
|
@ -772,10 +771,10 @@ mod tests {
|
|||
let pool = &mock.cauldron_w;
|
||||
|
||||
// Two tokens in cache with initial TVL and scores so they get ranks 1 & 2
|
||||
let tok_keep_id = TokenID::from_inner([0xAA; 32]);
|
||||
let tok_drop_id = TokenID::from_inner([0xAB; 32]);
|
||||
let tok_keep = tok_keep_id.to_hex();
|
||||
let _tok_drop = tok_drop_id.to_hex();
|
||||
let tok_keep_id = TokenID::from_byte_array([0xAA; 32]);
|
||||
let tok_drop_id = TokenID::from_byte_array([0xAB; 32]);
|
||||
let tok_keep = tok_keep_id.to_string();
|
||||
let _tok_drop = tok_drop_id.to_string();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
|
|
@ -830,7 +829,7 @@ mod tests {
|
|||
// sats/tokens arbitrary but non-zero
|
||||
super::tests::seed_minimal_token_history(
|
||||
pool,
|
||||
TokenID::from_inner([0xAA; 32]),
|
||||
TokenID::from_byte_array([0xAA; 32]),
|
||||
t0,
|
||||
t1,
|
||||
1_000,
|
||||
|
|
@ -883,16 +882,16 @@ mod tests {
|
|||
let pool = &mock.cauldron_w;
|
||||
|
||||
// Token and times
|
||||
let token = TokenID::from_inner([0x55; 32]);
|
||||
let token = TokenID::from_byte_array([0x55; 32]);
|
||||
let t0: i64 = time_now() - 10_000; // oldest
|
||||
let t1: i64 = time_now() - 5_000; // newer
|
||||
|
||||
// Unique ids
|
||||
let pool0 = OutPointHash::from_inner([0x10; 32]);
|
||||
let pool1 = OutPointHash::from_inner([0x11; 32]);
|
||||
let txid0 = Txid::from_inner([0x80; 32]); // creation tx for pool0
|
||||
let txid1 = Txid::from_inner([0x81; 32]); // creation tx for pool1
|
||||
let txid_w = Txid::from_inner([0x82; 32]); // withdraw tx for pool0
|
||||
let pool0 = OutPointHash::from_byte_array([0x10; 32]);
|
||||
let pool1 = OutPointHash::from_byte_array([0x11; 32]);
|
||||
let txid0 = Txid::from_byte_array([0x80; 32]); // creation tx for pool0
|
||||
let txid1 = Txid::from_byte_array([0x81; 32]); // creation tx for pool1
|
||||
let txid_w = Txid::from_byte_array([0x82; 32]); // withdraw tx for pool0
|
||||
let owner = PubkeyHash::all_zeros();
|
||||
|
||||
let mut conn = pool.acquire().await.unwrap();
|
||||
|
|
@ -909,18 +908,18 @@ mod tests {
|
|||
sats: Some(1_000),
|
||||
token_amount: Some(100),
|
||||
};
|
||||
insert_new_pool(&mut *conn, &c0).await.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![c0.clone()], &txid0)
|
||||
insert_new_pool(&mut conn, &c0).await.unwrap();
|
||||
insert_utxo_funding(&mut conn, &vec![c0.clone()], &txid0)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid0, &BlockHash::all_zeros(), t0)
|
||||
insert_block_tx(&mut conn, &txid0, &BlockHash::all_zeros(), t0)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_mempool_tx(&mut *conn, &txid0, t0 as u64)
|
||||
insert_mempool_tx(&mut conn, &txid0, t0 as u64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool0,
|
||||
&c0,
|
||||
Some(t0 as u64),
|
||||
|
|
@ -943,10 +942,10 @@ mod tests {
|
|||
sats: None,
|
||||
token_amount: None,
|
||||
};
|
||||
insert_utxo_spending(&mut *conn, &vec![withdraw.clone()], &txid_w, true)
|
||||
insert_utxo_spending(&mut conn, &vec![withdraw.clone()], &txid_w, true)
|
||||
.await
|
||||
.unwrap();
|
||||
flag_as_withdrawn(&mut *conn, &pool0, &withdraw)
|
||||
flag_as_withdrawn(&mut conn, &pool0, &withdraw)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -962,18 +961,18 @@ mod tests {
|
|||
sats: Some(2_000),
|
||||
token_amount: Some(200),
|
||||
};
|
||||
insert_new_pool(&mut *conn, &c1).await.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![c1.clone()], &txid1)
|
||||
insert_new_pool(&mut conn, &c1).await.unwrap();
|
||||
insert_utxo_funding(&mut conn, &vec![c1.clone()], &txid1)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid1, &BlockHash::all_zeros(), t1)
|
||||
insert_block_tx(&mut conn, &txid1, &BlockHash::all_zeros(), t1)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_mempool_tx(&mut *conn, &txid1, t1 as u64)
|
||||
insert_mempool_tx(&mut conn, &txid1, t1 as u64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool1,
|
||||
&c1,
|
||||
Some(t1 as u64),
|
||||
|
|
@ -988,12 +987,12 @@ mod tests {
|
|||
drop(conn);
|
||||
|
||||
// Oldest creation must be from pool0 @ t0, even though pool0 is withdrawn
|
||||
let row = db_first_pool_creation_row(pool, &token.to_hex())
|
||||
let row = db_first_pool_creation_row(pool, &token.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let (first_utxo, first_txid, ts, _height) = row.expect("expected a first pool row");
|
||||
assert_eq!(first_utxo, pool0.to_hex());
|
||||
assert_eq!(first_txid, txid0.to_hex());
|
||||
assert_eq!(first_utxo, pool0.to_string());
|
||||
assert_eq!(first_txid, txid0.to_string());
|
||||
assert_eq!(ts, t0);
|
||||
|
||||
// Also verify the batched cache backfill writes t0 into cached_token_metrics.first_pool_ts
|
||||
|
|
@ -1028,8 +1027,8 @@ mod tests {
|
|||
let pool = &mock.cauldron_w;
|
||||
|
||||
// Two tokens, different first timestamps
|
||||
let token_a = TokenID::from_inner([0xA1; 32]);
|
||||
let token_b = TokenID::from_inner([0xB1; 32]);
|
||||
let token_a = TokenID::from_byte_array([0xA1; 32]);
|
||||
let token_b = TokenID::from_byte_array([0xB1; 32]);
|
||||
let t0a: i64 = time_now() - 50_000;
|
||||
let t0b: i64 = time_now() - 40_000;
|
||||
|
||||
|
|
@ -1101,8 +1100,8 @@ mod tests {
|
|||
let mock = mock_db_pool(|pool| async move { setup_basic_schemas(pool).await }).await;
|
||||
let pool = &mock.cauldron_w;
|
||||
|
||||
let token = TokenID::from_inner([0xFE; 32]);
|
||||
let token_hex = token.to_hex();
|
||||
let token = TokenID::from_byte_array([0xFE; 32]);
|
||||
let token_hex = token.to_string();
|
||||
|
||||
// No pools for this token.
|
||||
let row = db_first_pool_creation_row(pool, &token_hex).await.unwrap();
|
||||
|
|
@ -1138,10 +1137,10 @@ mod tests {
|
|||
let bcmr_pool = &mock.bcmr_w;
|
||||
|
||||
// Seed BCMR with absurd decimals (should trigger overflow pre-patch)
|
||||
let token = TokenID::from_inner([0xDE; 32]);
|
||||
let token_hex = token.to_hex();
|
||||
let utxo = OutPointHash::from_inner([0xEE; 32]);
|
||||
let txid = Txid::from_inner([0xEF; 32]);
|
||||
let token = TokenID::from_byte_array([0xDE; 32]);
|
||||
let token_hex = token.to_string();
|
||||
let utxo = OutPointHash::from_byte_array([0xEE; 32]);
|
||||
let txid = Txid::from_byte_array([0xEF; 32]);
|
||||
|
||||
let row = BCMRRow {
|
||||
name: "OverflowCoin".into(),
|
||||
|
|
@ -1222,9 +1221,9 @@ mod tests {
|
|||
let pool = &mock.cauldron_w;
|
||||
let bcmr_pool = &mock.bcmr_w;
|
||||
|
||||
let token = TokenID::from_inner([0xEE; 32]);
|
||||
let utxo = OutPointHash::from_inner([0xCD; 32]);
|
||||
let txid = Txid::from_inner([0xAB; 32]);
|
||||
let token = TokenID::from_byte_array([0xEE; 32]);
|
||||
let utxo = OutPointHash::from_byte_array([0xCD; 32]);
|
||||
let txid = Txid::from_byte_array([0xAB; 32]);
|
||||
|
||||
// decimals = 28 OK, but we'll make price huge by tiny tokens
|
||||
insert_bcmr_data(
|
||||
|
|
@ -1390,7 +1389,7 @@ mod tests {
|
|||
let p = price_from_tvl(std::u64::MAX, 1).unwrap(); // huge base
|
||||
// Use your clamped pow10_dec; should not panic
|
||||
let factor = crate::db::cauldron::tokenlist::token_utils::pow10_dec(28);
|
||||
let scaled = p.checked_mul(factor).unwrap_or_else(|| Decimal::ZERO);
|
||||
let scaled = p.checked_mul(factor).unwrap_or(Decimal::ZERO);
|
||||
// Convert to f64 bounded; must be finite or clamped per your helper
|
||||
let f = crate::db::cauldron::tokenlist::token_utils::dec_to_f64_bounded(scaled);
|
||||
assert!(f.is_finite() || f.abs() == f64::MAX);
|
||||
|
|
|
|||
666
src/db/cauldron/tokentoken.rs
Normal file
666
src/db/cauldron/tokentoken.rs
Normal file
|
|
@ -0,0 +1,666 @@
|
|||
// 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
|
||||
|
||||
//! Indexing of native token-A <-> token-B (`TokenToken`) AMM pools.
|
||||
//!
|
||||
//! Mirrors [`crate::db::cauldron::pool`] but tracks both token reserves of a
|
||||
//! pool (token A in the main UTXO, token B in the linked storage UTXO). Tables
|
||||
//! live in `cauldron.db` so ingestion shares the block write-transaction and
|
||||
//! `KEY_LAST_INDEXED` checkpoint.
|
||||
//!
|
||||
//! Like cauldron pools, states are indexed both from confirmed blocks (with
|
||||
//! `mtp_timestamp`) and from the mempool (with `first_seen_timestamp`); the
|
||||
//! history upsert reconciles the two when a mempool tx confirms.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use bitcoincash::{BlockHash, TokenID};
|
||||
use log::info;
|
||||
use riftenlabs_defi::{chainutil::OutPointHash, tokentoken::ParsedTokenToken};
|
||||
use sqlx::{Row, SqliteConnection, SqlitePool};
|
||||
|
||||
use crate::db::blob::{blob_to_display_hex, FromBlob, ToBlob};
|
||||
|
||||
/// Idempotent: also run as an always-on migration for existing databases
|
||||
/// (the tables were added after `DB_VERSION` 5 without a version bump).
|
||||
pub async fn create_table(pool: &SqlitePool) {
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS tokentoken_pool (
|
||||
creation_utxo BLOB PRIMARY KEY,
|
||||
nft_owner BLOB NOT NULL,
|
||||
token_a_id BLOB NOT NULL,
|
||||
token_b_id BLOB NOT NULL,
|
||||
fee_rate INT NOT NULL,
|
||||
min_fee INT NOT NULL,
|
||||
withdrawn_in_txid BLOB
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("failed to create table tokentoken_pool");
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS tokentoken_pool_history_entry (
|
||||
utxo BLOB PRIMARY KEY,
|
||||
pool BLOB NOT NULL REFERENCES tokentoken_pool(creation_utxo) ON DELETE CASCADE,
|
||||
storage_utxo BLOB NOT NULL,
|
||||
token_a_id BLOB NOT NULL,
|
||||
token_b_id BLOB NOT NULL,
|
||||
txid BLOB NOT NULL,
|
||||
tx_pos INT NOT NULL,
|
||||
storage_tx_pos INT NOT NULL,
|
||||
mtp_timestamp BIGINT,
|
||||
first_seen_timestamp BIGINT,
|
||||
effective_timestamp BIGINT GENERATED ALWAYS AS (COALESCE(first_seen_timestamp, mtp_timestamp)),
|
||||
sequence BIGINT NOT NULL,
|
||||
reserve_a BIGINT NOT NULL,
|
||||
reserve_b BIGINT NOT NULL,
|
||||
main_sats BIGINT NOT NULL,
|
||||
storage_sats BIGINT NOT NULL,
|
||||
reserve_a_delta BIGINT NOT NULL,
|
||||
reserve_b_delta BIGINT NOT NULL
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("failed to create table tokentoken_pool_history_entry");
|
||||
|
||||
sqlx::query(
|
||||
"CREATE INDEX IF NOT EXISTS idx_tt_history_pool_sequence ON tokentoken_pool_history_entry(pool, sequence)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query(
|
||||
"CREATE INDEX IF NOT EXISTS idx_tt_pool_pair ON tokentoken_pool(token_a_id, token_b_id)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query(
|
||||
"CREATE INDEX IF NOT EXISTS idx_tt_history_txid ON tokentoken_pool_history_entry(txid)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Next sequence number in the `tokentoken_pool_history_entry` table.
|
||||
static NEXT_SEQUENCE: AtomicI64 = AtomicI64::new(-10);
|
||||
|
||||
pub async fn initialize_seq(pool: &SqlitePool) {
|
||||
let row: (i64,) =
|
||||
sqlx::query_as("SELECT IFNULL(MAX(sequence), 0) + 1 FROM tokentoken_pool_history_entry")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
NEXT_SEQUENCE.store(row.0, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn dummy_init_seq() {
|
||||
if NEXT_SEQUENCE.load(Ordering::SeqCst) < 0 {
|
||||
NEXT_SEQUENCE.store(42, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_pool_by_main_utxo(
|
||||
conn: &mut SqliteConnection,
|
||||
utxo_hash: &OutPointHash,
|
||||
) -> Result<Option<OutPointHash>> {
|
||||
let row: Option<(Vec<u8>,)> =
|
||||
sqlx::query_as("SELECT pool FROM tokentoken_pool_history_entry WHERE utxo = ?")
|
||||
.bind(utxo_hash.to_blob())
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?;
|
||||
match row {
|
||||
Some((blob,)) => Ok(Some(OutPointHash::from_blob(&blob)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Latest (reserve_a, reserve_b) for the pool state recorded at `utxo_hash`.
|
||||
async fn get_reserves_at(
|
||||
conn: &mut SqliteConnection,
|
||||
utxo_hash: &OutPointHash,
|
||||
) -> Result<(i64, i64)> {
|
||||
let row = sqlx::query(
|
||||
"SELECT reserve_a, reserve_b FROM tokentoken_pool_history_entry WHERE utxo = ?",
|
||||
)
|
||||
.bind(utxo_hash.to_blob())
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?;
|
||||
match row {
|
||||
Some(r) => Ok((r.get(0), r.get(1))),
|
||||
None => Ok((0, 0)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert_new_pool(conn: &mut SqliteConnection, pool: &ParsedTokenToken) -> Result<()> {
|
||||
sqlx::query(
|
||||
"INSERT OR REPLACE INTO tokentoken_pool
|
||||
(creation_utxo, nft_owner, token_a_id, token_b_id, fee_rate, min_fee, withdrawn_in_txid)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NULL)",
|
||||
)
|
||||
.bind(
|
||||
pool.new_main_utxo_hash
|
||||
.expect("new pool main utxo missing")
|
||||
.to_blob(),
|
||||
)
|
||||
.bind(pool.nft_owner.to_vec())
|
||||
.bind(pool.token_a_id.expect("token a id missing").to_blob())
|
||||
.bind(pool.token_b_id.expect("token b id missing").to_blob())
|
||||
.bind(pool.fee_rate as i64)
|
||||
.bind(pool.min_fee as i64)
|
||||
.execute(&mut *conn)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("failed to insert tokentoken pool: {e:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn flag_as_withdrawn(
|
||||
conn: &mut SqliteConnection,
|
||||
creation_utxo: &OutPointHash,
|
||||
pool: &ParsedTokenToken,
|
||||
) -> Result<()> {
|
||||
let txid = pool
|
||||
.new_main_utxo_txid
|
||||
.expect("withdraw must carry the withdrawing txid");
|
||||
sqlx::query("UPDATE tokentoken_pool SET withdrawn_in_txid = ? WHERE creation_utxo = ?")
|
||||
.bind(txid.to_blob())
|
||||
.bind(creation_utxo.to_blob())
|
||||
.execute(&mut *conn)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("failed to flag tokentoken pool withdrawn: {e:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn insert_history_entry(
|
||||
conn: &mut SqliteConnection,
|
||||
creation_utxo: &OutPointHash,
|
||||
pool: &ParsedTokenToken,
|
||||
mtp_timestamp: Option<u64>,
|
||||
first_seen_timestamp: Option<u64>,
|
||||
reserve_a_delta: i64,
|
||||
reserve_b_delta: i64,
|
||||
) -> Result<()> {
|
||||
let next_seq = NEXT_SEQUENCE.fetch_add(1, Ordering::SeqCst);
|
||||
assert!(next_seq >= 0, "tokentoken sequence not initialized");
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO tokentoken_pool_history_entry
|
||||
(utxo, pool, storage_utxo, token_a_id, token_b_id, txid, tx_pos, storage_tx_pos,
|
||||
mtp_timestamp, first_seen_timestamp, sequence, reserve_a, reserve_b,
|
||||
main_sats, storage_sats, reserve_a_delta, reserve_b_delta)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(utxo) DO UPDATE SET
|
||||
pool = excluded.pool,
|
||||
storage_utxo = excluded.storage_utxo,
|
||||
txid = excluded.txid,
|
||||
tx_pos = excluded.tx_pos,
|
||||
storage_tx_pos = excluded.storage_tx_pos,
|
||||
mtp_timestamp = COALESCE(excluded.mtp_timestamp, tokentoken_pool_history_entry.mtp_timestamp),
|
||||
first_seen_timestamp = COALESCE(excluded.first_seen_timestamp, tokentoken_pool_history_entry.first_seen_timestamp),
|
||||
sequence = excluded.sequence",
|
||||
)
|
||||
.bind(pool.new_main_utxo_hash.expect("new main utxo hash missing").to_blob())
|
||||
.bind(creation_utxo.to_blob())
|
||||
.bind(pool.new_storage_utxo_hash.expect("new storage utxo hash missing").to_blob())
|
||||
.bind(pool.token_a_id.expect("token a id missing").to_blob())
|
||||
.bind(pool.token_b_id.expect("token b id missing").to_blob())
|
||||
.bind(pool.new_main_utxo_txid.expect("main txid missing").to_blob())
|
||||
.bind(pool.new_main_utxo_n.expect("main vout missing") as i64)
|
||||
.bind(pool.new_storage_utxo_n.expect("storage vout missing") as i64)
|
||||
.bind(mtp_timestamp.map(|t| t as i64))
|
||||
.bind(first_seen_timestamp.map(|t| t as i64))
|
||||
.bind(next_seq)
|
||||
.bind(pool.token_a_amount.expect("reserve a missing"))
|
||||
.bind(pool.token_b_amount.expect("reserve b missing"))
|
||||
.bind(pool.main_sats.expect("main sats missing") as i64)
|
||||
.bind(pool.storage_sats.expect("storage sats missing") as i64)
|
||||
.bind(reserve_a_delta)
|
||||
.bind(reserve_b_delta)
|
||||
.execute(&mut *conn)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("failed to insert tokentoken history entry: {e:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a batch of parsed TokenToken states to the database.
|
||||
///
|
||||
/// Mirrors [`crate::db::cauldron::pool::update_pool_history`]: a swap is linked
|
||||
/// to its pool by the spent main UTXO; a creation (no known parent) opens a new
|
||||
/// pool; a withdrawal flags the pool closed. The queue defers states whose
|
||||
/// parent is created later in the same block.
|
||||
pub async fn update_tokentoken_pool_history(
|
||||
conn: &mut SqliteConnection,
|
||||
pools: Vec<ParsedTokenToken>,
|
||||
mtp_timestamp: Option<u64>,
|
||||
first_seen_timestamp: Option<u64>,
|
||||
) -> Result<()> {
|
||||
if pools.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut queue = VecDeque::from(pools);
|
||||
|
||||
while let Some(current) = queue.pop_front() {
|
||||
let (creation_utxo, is_new) =
|
||||
match get_pool_by_main_utxo(&mut *conn, ¤t.spent_main_utxo_hash).await? {
|
||||
Some(c) => (c, false),
|
||||
None => {
|
||||
let has_parent = queue
|
||||
.iter()
|
||||
.any(|p| Some(current.spent_main_utxo_hash) == p.new_main_utxo_hash);
|
||||
if has_parent {
|
||||
queue.push_back(current);
|
||||
continue;
|
||||
}
|
||||
match current.new_main_utxo_hash {
|
||||
Some(utxo) if !current.is_withdrawn => (utxo, true),
|
||||
// A withdrawal of a pool we never indexed, or a malformed
|
||||
// state — nothing to update.
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if current.is_withdrawn {
|
||||
info!("TokenToken pool {} withdrawn", creation_utxo);
|
||||
flag_as_withdrawn(&mut *conn, &creation_utxo, ¤t).await?;
|
||||
} else if is_new {
|
||||
info!(
|
||||
"TokenToken pool created in tx {}",
|
||||
current.new_main_utxo_txid.expect("new pool txid")
|
||||
);
|
||||
insert_new_pool(&mut *conn, ¤t).await?;
|
||||
insert_history_entry(
|
||||
&mut *conn,
|
||||
&creation_utxo,
|
||||
¤t,
|
||||
mtp_timestamp,
|
||||
first_seen_timestamp,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
let (prev_a, prev_b) =
|
||||
get_reserves_at(&mut *conn, ¤t.spent_main_utxo_hash).await?;
|
||||
let reserve_a_delta = current.token_a_amount.unwrap_or(0) - prev_a;
|
||||
let reserve_b_delta = current.token_b_amount.unwrap_or(0) - prev_b;
|
||||
insert_history_entry(
|
||||
&mut *conn,
|
||||
&creation_utxo,
|
||||
¤t,
|
||||
mtp_timestamp,
|
||||
first_seen_timestamp,
|
||||
reserve_a_delta,
|
||||
reserve_b_delta,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove TokenToken state recorded in the given block (chain reorg).
|
||||
///
|
||||
/// Order-independent w.r.t. how reorged blocks are replayed: deletes the
|
||||
/// block's history rows, drops pools left with no history (their creation was
|
||||
/// undone), and reverts withdrawals made in the block.
|
||||
pub async fn delete_entries_for_block(pool: &SqlitePool, blockhash: &BlockHash) -> Result<bool> {
|
||||
let r1 = sqlx::query(
|
||||
"DELETE FROM tokentoken_pool_history_entry
|
||||
WHERE txid IN (SELECT txid FROM tx WHERE blockhash = ?)",
|
||||
)
|
||||
.bind(blockhash.to_blob())
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"DELETE FROM tokentoken_pool
|
||||
WHERE creation_utxo NOT IN (SELECT DISTINCT pool FROM tokentoken_pool_history_entry)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
let r3 = sqlx::query(
|
||||
"UPDATE tokentoken_pool SET withdrawn_in_txid = NULL
|
||||
WHERE withdrawn_in_txid IN (SELECT txid FROM tx WHERE blockhash = ?)",
|
||||
)
|
||||
.bind(blockhash.to_blob())
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(r1.rows_affected() + r3.rows_affected() != 0)
|
||||
}
|
||||
|
||||
fn serialize_u64_as_string<S>(value: &u64, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&value.to_string())
|
||||
}
|
||||
|
||||
/// A single active TokenToken pool at its latest state, for the RPC layer.
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct ActiveTokenTokenPool {
|
||||
pub pool_id: String,
|
||||
pub nft_owner: String,
|
||||
pub token_a_id: String,
|
||||
pub token_b_id: String,
|
||||
/// Decimal string (base units) — avoids f64 precision loss for large token amounts.
|
||||
#[serde(serialize_with = "serialize_u64_as_string")]
|
||||
pub reserve_a: u64,
|
||||
#[serde(serialize_with = "serialize_u64_as_string")]
|
||||
pub reserve_b: u64,
|
||||
pub fee_rate: u32,
|
||||
pub min_fee: u64,
|
||||
/// Satoshis locked in the main UTXO (preserved across swaps).
|
||||
pub main_sats: u64,
|
||||
/// Satoshis locked in the storage UTXO (preserved across swaps).
|
||||
pub storage_sats: u64,
|
||||
/// Transaction that created the pool's current state.
|
||||
pub txid: String,
|
||||
/// Output index of the main UTXO (token A).
|
||||
pub main_vout: u32,
|
||||
/// Output index of the storage UTXO (token B); also `otherTokenOutpointIndex`.
|
||||
pub storage_vout: u32,
|
||||
}
|
||||
|
||||
/// Active pools containing both tokens, regardless of which is A (main) or B
|
||||
/// (storage). Returns each pool's latest state.
|
||||
pub async fn db_active_pools_for_pair(
|
||||
pool: &SqlitePool,
|
||||
token_a: &TokenID,
|
||||
token_b: &TokenID,
|
||||
) -> Result<Vec<ActiveTokenTokenPool>> {
|
||||
let a = token_a.to_blob();
|
||||
let b = token_b.to_blob();
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT p.creation_utxo, p.nft_owner, p.token_a_id, p.token_b_id, p.fee_rate, p.min_fee,
|
||||
phe.reserve_a, phe.reserve_b, phe.main_sats, phe.storage_sats,
|
||||
phe.txid, phe.tx_pos, phe.storage_tx_pos
|
||||
FROM tokentoken_pool p
|
||||
JOIN tokentoken_pool_history_entry phe ON phe.pool = p.creation_utxo
|
||||
AND phe.sequence = (
|
||||
SELECT MAX(sequence) FROM tokentoken_pool_history_entry WHERE pool = p.creation_utxo
|
||||
)
|
||||
WHERE p.withdrawn_in_txid IS NULL
|
||||
AND ((p.token_a_id = ?1 AND p.token_b_id = ?2)
|
||||
OR (p.token_a_id = ?2 AND p.token_b_id = ?1))",
|
||||
)
|
||||
.bind(&a)
|
||||
.bind(&b)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let mut out = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let pool_blob: Vec<u8> = row.get(0);
|
||||
let nft_owner_blob: Vec<u8> = row.get(1);
|
||||
let token_a_blob: Vec<u8> = row.get(2);
|
||||
let token_b_blob: Vec<u8> = row.get(3);
|
||||
let fee_rate: i64 = row.get(4);
|
||||
let min_fee: i64 = row.get(5);
|
||||
let reserve_a: i64 = row.get(6);
|
||||
let reserve_b: i64 = row.get(7);
|
||||
let main_sats: i64 = row.get(8);
|
||||
let storage_sats: i64 = row.get(9);
|
||||
let txid_blob: Vec<u8> = row.get(10);
|
||||
let tx_pos: i64 = row.get(11);
|
||||
let storage_tx_pos: i64 = row.get(12);
|
||||
|
||||
out.push(ActiveTokenTokenPool {
|
||||
pool_id: blob_to_display_hex::<OutPointHash>(&pool_blob)?,
|
||||
nft_owner: hex::encode(&nft_owner_blob),
|
||||
token_a_id: blob_to_display_hex::<TokenID>(&token_a_blob)?,
|
||||
token_b_id: blob_to_display_hex::<TokenID>(&token_b_blob)?,
|
||||
reserve_a: reserve_a as u64,
|
||||
reserve_b: reserve_b as u64,
|
||||
fee_rate: fee_rate as u32,
|
||||
min_fee: min_fee as u64,
|
||||
main_sats: main_sats as u64,
|
||||
storage_sats: storage_sats as u64,
|
||||
txid: blob_to_display_hex::<bitcoincash::Txid>(&txid_blob)?,
|
||||
main_vout: tx_pos as u32,
|
||||
storage_vout: storage_tx_pos as u32,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Distinct token ids that appear in at least one active TokenToken pool.
|
||||
pub async fn db_pair_tokens(pool: &SqlitePool) -> Result<Vec<String>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT token_a_id AS tid FROM tokentoken_pool WHERE withdrawn_in_txid IS NULL
|
||||
UNION
|
||||
SELECT token_b_id AS tid FROM tokentoken_pool WHERE withdrawn_in_txid IS NULL",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let mut out = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let blob: Vec<u8> = row.get(0);
|
||||
out.push(blob_to_display_hex::<TokenID>(&blob)?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::cauldron::prepare_tables;
|
||||
use crate::db::cauldron::tx::insert_block_tx;
|
||||
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::{BlockHash, Txid};
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||
use std::sync::atomic::{AtomicU64, Ordering as AOrdering};
|
||||
|
||||
static TT_TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
async fn test_db() -> SqlitePool {
|
||||
let id = TT_TEST_COUNTER.fetch_add(1, AOrdering::SeqCst);
|
||||
let uri = format!("file:tt_test_{}?mode=memory&cache=shared", id);
|
||||
let opts = SqliteConnectOptions::new()
|
||||
.filename(&uri)
|
||||
.foreign_keys(false);
|
||||
let pool = SqlitePoolOptions::new().connect_with(opts).await.unwrap();
|
||||
prepare_tables(&pool).await;
|
||||
dummy_init_seq();
|
||||
pool
|
||||
}
|
||||
|
||||
/// The always-run startup migration calls `create_table` on databases
|
||||
/// that already have the tables; it must be a no-op then.
|
||||
#[tokio::test]
|
||||
async fn create_table_is_idempotent() {
|
||||
let pool = test_db().await;
|
||||
create_table(&pool).await;
|
||||
}
|
||||
|
||||
/// A state first seen in the mempool (first_seen only) and later confirmed
|
||||
/// in a block (mtp only) must keep the mempool first_seen timestamp.
|
||||
#[tokio::test]
|
||||
async fn mempool_then_block_keeps_first_seen() {
|
||||
let pool = test_db().await;
|
||||
let block = BlockHash::all_zeros();
|
||||
|
||||
let c = creation(1, 10, 1_000_000, 2_000_000);
|
||||
seed_tx(&pool, &c, &block).await;
|
||||
|
||||
let mut conn = pool.acquire().await.unwrap();
|
||||
// Mempool: first_seen only.
|
||||
update_tokentoken_pool_history(&mut conn, vec![c.clone()], None, Some(1_700_000_100))
|
||||
.await
|
||||
.unwrap();
|
||||
// Block confirmation of the same state: mtp only.
|
||||
update_tokentoken_pool_history(&mut conn, vec![c], Some(1_700_000_500), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let row = sqlx::query(
|
||||
"SELECT mtp_timestamp, first_seen_timestamp, effective_timestamp
|
||||
FROM tokentoken_pool_history_entry",
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let mtp: Option<i64> = row.get(0);
|
||||
let first_seen: Option<i64> = row.get(1);
|
||||
let effective: Option<i64> = row.get(2);
|
||||
assert_eq!(mtp, Some(1_700_000_500));
|
||||
assert_eq!(first_seen, Some(1_700_000_100));
|
||||
assert_eq!(effective, Some(1_700_000_100), "first_seen wins");
|
||||
}
|
||||
|
||||
fn oph(b: u8) -> OutPointHash {
|
||||
OutPointHash::from_byte_array([b; 32])
|
||||
}
|
||||
fn tid(b: u8) -> TokenID {
|
||||
TokenID::from_byte_array([b; 32])
|
||||
}
|
||||
fn txid(b: u8) -> Txid {
|
||||
Txid::from_byte_array([b; 32])
|
||||
}
|
||||
|
||||
fn creation(main: u8, txid_b: u8, ra: i64, rb: i64) -> ParsedTokenToken {
|
||||
ParsedTokenToken {
|
||||
nft_owner: [0x11; 32],
|
||||
fee_rate: 300,
|
||||
min_fee: 1,
|
||||
other_token_outpoint_index: 1,
|
||||
is_withdrawn: false,
|
||||
spent_main_utxo_hash: OutPointHash::all_zeros(),
|
||||
spent_storage_utxo_hash: Some(OutPointHash::all_zeros()),
|
||||
new_main_utxo_hash: Some(oph(main)),
|
||||
new_main_utxo_txid: Some(txid(txid_b)),
|
||||
new_main_utxo_n: Some(0),
|
||||
new_storage_utxo_hash: Some(oph(main.wrapping_add(100))),
|
||||
new_storage_utxo_txid: Some(txid(txid_b)),
|
||||
new_storage_utxo_n: Some(1),
|
||||
token_a_id: Some(tid(0xAA)),
|
||||
token_b_id: Some(tid(0xBB)),
|
||||
token_a_amount: Some(ra),
|
||||
token_b_amount: Some(rb),
|
||||
main_sats: Some(800),
|
||||
storage_sats: Some(800),
|
||||
}
|
||||
}
|
||||
|
||||
/// A swap spending `prev_main`, producing `new_main` in tx `txid_b`.
|
||||
fn swap(prev_main: u8, new_main: u8, txid_b: u8, ra: i64, rb: i64) -> ParsedTokenToken {
|
||||
let mut p = creation(new_main, txid_b, ra, rb);
|
||||
p.spent_main_utxo_hash = oph(prev_main);
|
||||
p
|
||||
}
|
||||
|
||||
async fn seed_tx(pool: &SqlitePool, t: &ParsedTokenToken, blockhash: &BlockHash) {
|
||||
let mut conn = pool.acquire().await.unwrap();
|
||||
insert_block_tx(
|
||||
&mut conn,
|
||||
&t.new_main_utxo_txid.unwrap(),
|
||||
blockhash,
|
||||
1_700_000_000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creation_then_swap_tracks_reserves_and_pair_query() {
|
||||
let pool = test_db().await;
|
||||
let block = BlockHash::all_zeros();
|
||||
|
||||
let c = creation(1, 10, 1_000_000, 2_000_000);
|
||||
seed_tx(&pool, &c, &block).await;
|
||||
let s = swap(1, 2, 11, 1_010_000, 1_980_000);
|
||||
seed_tx(&pool, &s, &block).await;
|
||||
|
||||
let mut conn = pool.acquire().await.unwrap();
|
||||
update_tokentoken_pool_history(&mut conn, vec![c, s], Some(1_700_000_000), None)
|
||||
.await
|
||||
.unwrap();
|
||||
drop(conn);
|
||||
|
||||
// Order-insensitive pair lookup returns the latest state.
|
||||
let pools = db_active_pools_for_pair(&pool, &tid(0xBB), &tid(0xAA))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(pools.len(), 1);
|
||||
assert_eq!(pools[0].reserve_a, 1_010_000);
|
||||
assert_eq!(pools[0].reserve_b, 1_980_000);
|
||||
assert_eq!(pools[0].fee_rate, 300);
|
||||
assert_eq!(pools[0].main_vout, 0);
|
||||
assert_eq!(pools[0].storage_vout, 1);
|
||||
|
||||
let tokens = db_pair_tokens(&pool).await.unwrap();
|
||||
assert!(tokens.contains(&tid(0xAA).to_string()) && tokens.contains(&tid(0xBB).to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn withdraw_hides_pool_and_reorg_restores_it() {
|
||||
let pool = test_db().await;
|
||||
let block_a = BlockHash::from_byte_array([0xA1; 32]);
|
||||
let block_b = BlockHash::from_byte_array([0xB2; 32]);
|
||||
|
||||
let c = creation(1, 10, 1_000_000, 2_000_000);
|
||||
seed_tx(&pool, &c, &block_a).await;
|
||||
|
||||
// Withdrawal in a later block.
|
||||
let mut w = creation(0, 20, 0, 0);
|
||||
w.is_withdrawn = true;
|
||||
w.spent_main_utxo_hash = oph(1);
|
||||
w.new_main_utxo_hash = None;
|
||||
w.new_storage_utxo_hash = None;
|
||||
// carry the withdrawing txid (as the parser does)
|
||||
w.new_main_utxo_txid = Some(txid(20));
|
||||
seed_tx(&pool, &w, &block_b).await;
|
||||
|
||||
let mut conn = pool.acquire().await.unwrap();
|
||||
update_tokentoken_pool_history(&mut conn, vec![c], Some(1), None)
|
||||
.await
|
||||
.unwrap();
|
||||
update_tokentoken_pool_history(&mut conn, vec![w], Some(2), None)
|
||||
.await
|
||||
.unwrap();
|
||||
drop(conn);
|
||||
|
||||
let a = tid(0xAA);
|
||||
let b = tid(0xBB);
|
||||
assert!(
|
||||
db_active_pools_for_pair(&pool, &a, &b)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty(),
|
||||
"withdrawn pool should not be active"
|
||||
);
|
||||
|
||||
// Reorg the withdrawal block: pool becomes active again.
|
||||
delete_entries_for_block(&pool, &block_b).await.unwrap();
|
||||
assert_eq!(
|
||||
db_active_pools_for_pair(&pool, &a, &b).await.unwrap().len(),
|
||||
1,
|
||||
"reorg of the withdrawal should restore the pool"
|
||||
);
|
||||
|
||||
// Reorg the creation block: pool disappears entirely.
|
||||
delete_entries_for_block(&pool, &block_a).await.unwrap();
|
||||
assert!(db_active_pools_for_pair(&pool, &a, &b)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
assert!(db_pair_tokens(&pool).await.unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
|
|
@ -46,13 +46,13 @@ pub async fn insert_user_action(
|
|||
.iter()
|
||||
.filter_map(|output| {
|
||||
if output.script_pubkey.is_p2pkh() {
|
||||
Some(output.script_pubkey[3..23].to_vec())
|
||||
Some(output.script_pubkey.as_bytes()[3..23].to_vec())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|pubkey| {
|
||||
PubkeyHash::from_inner(
|
||||
PubkeyHash::from_byte_array(
|
||||
pubkey
|
||||
.try_into()
|
||||
.expect("failed to convert into pubkeyhash"),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
use anyhow::Result;
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
|
||||
use bitcoincash::TokenID;
|
||||
use sqlx::{Row, SqlitePool};
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ pub async fn insert_crc20_candidate(pool: &SqlitePool, token_id: &TokenID) -> Re
|
|||
.map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"failed to insert crc20 candidate: token_id = {}. Original error: {:?}",
|
||||
token_id.to_hex(),
|
||||
token_id.to_string(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
|
@ -190,19 +190,18 @@ pub async fn bump_failed_attempts(pool: &SqlitePool, token_hex: &str) -> Result<
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bitcoin_hashes::hex::FromHex;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_not_indexed_tokens_excludes_bcmr_marked_tokens() {
|
||||
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||
prepare_tables(&pool).await;
|
||||
|
||||
let token_a =
|
||||
TokenID::from_hex("0101010101010101010101010101010101010101010101010101010101010101")
|
||||
.unwrap();
|
||||
let token_b =
|
||||
TokenID::from_hex("0202020202020202020202020202020202020202020202020202020202020202")
|
||||
.unwrap();
|
||||
let token_a = "0101010101010101010101010101010101010101010101010101010101010101"
|
||||
.parse::<TokenID>()
|
||||
.unwrap();
|
||||
let token_b = "0202020202020202020202020202020202020202020202020202020202020202"
|
||||
.parse::<TokenID>()
|
||||
.unwrap();
|
||||
|
||||
insert_crc20_candidate(&pool, &token_a).await.unwrap();
|
||||
insert_crc20_candidate(&pool, &token_b).await.unwrap();
|
||||
|
|
@ -214,7 +213,7 @@ mod tests {
|
|||
assert_eq!(result.len(), 1, "should only return 1 token");
|
||||
assert_eq!(
|
||||
result[0].1.to_lowercase(),
|
||||
token_a.to_hex().to_lowercase(),
|
||||
token_a.to_string().to_lowercase(),
|
||||
"should return token_a"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use super::DB;
|
|||
use crate::db::bcmr::prepare_tables as bcmr_prepare_tables;
|
||||
use crate::db::cauldron::config::check_db_version;
|
||||
use crate::db::cauldron::prepare_tables as cauldron_prepare_tables;
|
||||
use crate::db::cauldron::tokentoken::create_table as tokentoken_prepare_tables;
|
||||
use crate::db::crc20::prepare_tables as crc20_prepare_tables;
|
||||
use crate::db::moria::prepare_tables as moria_prepare_tables;
|
||||
use crate::db::oracle::prepare_tables as oracle_prepare_tables;
|
||||
|
|
@ -103,6 +104,8 @@ pub async fn initialize_databases(network: &str, read_slots: ReadSlots) -> Resul
|
|||
} else {
|
||||
check_db_version(&cauldron_db_read).await?;
|
||||
}
|
||||
// Always-run migration: safe on both new and existing cauldron.db
|
||||
tokentoken_prepare_tables(&cauldron_db_write).await;
|
||||
|
||||
// Initialize BCMR database
|
||||
let (db_exists, bcmr_db_write, bcmr_db_read) =
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
use anyhow::Result;
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::{BlockHash, Transaction, Txid};
|
||||
use log::debug;
|
||||
|
|
@ -275,7 +275,7 @@ pub async fn index_moria(
|
|||
None => continue,
|
||||
};
|
||||
|
||||
let txid = tx.txid();
|
||||
let txid = tx.compute_txid();
|
||||
|
||||
// If the parser returned Borrow but the spent outpoint is a known loan UTXO,
|
||||
// reclassify as Refinance
|
||||
|
|
@ -343,7 +343,7 @@ pub async fn index_moria(
|
|||
debug!(
|
||||
"moria: {} {} (borrower: {})",
|
||||
action_type_to_str(action.action_type),
|
||||
txid.to_hex(),
|
||||
txid,
|
||||
action
|
||||
.loan_commitment
|
||||
.as_ref()
|
||||
|
|
@ -541,11 +541,11 @@ mod tests {
|
|||
}
|
||||
|
||||
fn test_blockhash() -> BlockHash {
|
||||
BlockHash::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xAA; 32]).unwrap())
|
||||
BlockHash::from_raw_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xAA; 32]).unwrap())
|
||||
}
|
||||
|
||||
fn test_txid(n: u8) -> Txid {
|
||||
Txid::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[n; 32]).unwrap())
|
||||
Txid::from_raw_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[n; 32]).unwrap())
|
||||
}
|
||||
|
||||
fn borrower_a() -> [u8; 32] {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
pub mod v2;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use bitcoin_hashes::{hex::ToHex, Hash};
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::{BlockHash, TokenID, Transaction, Txid};
|
||||
use log::debug;
|
||||
use riftenlabs_defi::delphi::parse_delphi_update;
|
||||
|
|
@ -113,8 +113,8 @@ pub async fn index_oracle(
|
|||
// collide.
|
||||
if let Some(update) = parse_delphi_update(tx) {
|
||||
let entry = DelphiEntry {
|
||||
txid: tx.txid().to_hex(),
|
||||
blockhash: blockhash.to_hex(),
|
||||
txid: tx.compute_txid().to_string(),
|
||||
blockhash: blockhash.to_string(),
|
||||
oracle_timestamp: update.timestamp as i64,
|
||||
oracle_price: update.price as i64,
|
||||
oracle_sequence: update.sequence as i64,
|
||||
|
|
@ -136,8 +136,8 @@ pub async fn index_oracle(
|
|||
continue;
|
||||
}
|
||||
let entry = DelphiEntry {
|
||||
txid: tx.txid().to_hex(),
|
||||
blockhash: blockhash.to_hex(),
|
||||
txid: tx.compute_txid().to_string(),
|
||||
blockhash: blockhash.to_string(),
|
||||
oracle_timestamp: update.timestamp as i64,
|
||||
oracle_price: update.price as i64,
|
||||
oracle_sequence: update.sequence as i64,
|
||||
|
|
@ -305,48 +305,47 @@ mod tests {
|
|||
}
|
||||
|
||||
async fn insert_test_entries(pool: &SqlitePool, token_id: &TokenID, base_ts: i64) {
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::Txid;
|
||||
|
||||
let blockhash = BlockHash::hash(b"block");
|
||||
let txid1 = Txid::hash(b"tx1").to_hex();
|
||||
let txid2 = Txid::hash(b"tx2").to_hex();
|
||||
let txid3 = Txid::hash(b"tx3").to_hex();
|
||||
let txid4 = Txid::hash(b"tx4").to_hex();
|
||||
let txid1 = Txid::hash(b"tx1").to_string();
|
||||
let txid2 = Txid::hash(b"tx2").to_string();
|
||||
let txid3 = Txid::hash(b"tx3").to_string();
|
||||
let txid4 = Txid::hash(b"tx4").to_string();
|
||||
|
||||
let entries = vec![
|
||||
DelphiEntry {
|
||||
txid: txid1,
|
||||
blockhash: blockhash.to_hex(),
|
||||
blockhash: blockhash.to_string(),
|
||||
oracle_timestamp: base_ts - 200,
|
||||
oracle_price: 1000,
|
||||
oracle_sequence: 1,
|
||||
token_id: token_id.to_hex(),
|
||||
token_id: token_id.to_string(),
|
||||
},
|
||||
DelphiEntry {
|
||||
txid: txid2,
|
||||
blockhash: blockhash.to_hex(),
|
||||
blockhash: blockhash.to_string(),
|
||||
oracle_timestamp: base_ts - 150,
|
||||
oracle_price: 1100,
|
||||
oracle_sequence: 2,
|
||||
token_id: token_id.to_hex(),
|
||||
token_id: token_id.to_string(),
|
||||
},
|
||||
DelphiEntry {
|
||||
txid: txid3,
|
||||
blockhash: blockhash.to_hex(),
|
||||
blockhash: blockhash.to_string(),
|
||||
oracle_timestamp: base_ts - 100,
|
||||
oracle_price: 1200,
|
||||
oracle_sequence: 3,
|
||||
token_id: token_id.to_hex(),
|
||||
token_id: token_id.to_string(),
|
||||
},
|
||||
DelphiEntry {
|
||||
txid: txid4,
|
||||
blockhash: blockhash.to_hex(),
|
||||
blockhash: blockhash.to_string(),
|
||||
oracle_timestamp: base_ts - 50,
|
||||
oracle_price: 1300,
|
||||
oracle_sequence: 4,
|
||||
token_id: token_id.to_hex(),
|
||||
token_id: token_id.to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -357,7 +356,6 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_get_range_with_step_downsamples_correctly() {
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::Txid;
|
||||
|
||||
|
|
@ -367,9 +365,9 @@ mod tests {
|
|||
|
||||
insert_test_entries(&pool, &token_id, now).await;
|
||||
|
||||
let txid1 = Txid::hash(b"tx1").to_hex();
|
||||
let txid3 = Txid::hash(b"tx3").to_hex();
|
||||
let txid4 = Txid::hash(b"tx4").to_hex();
|
||||
let txid1 = Txid::hash(b"tx1").to_string();
|
||||
let txid3 = Txid::hash(b"tx3").to_string();
|
||||
let txid4 = Txid::hash(b"tx4").to_string();
|
||||
|
||||
let result = get_range_with_step(&pool, &Some(token_id), now - 250, now, 100)
|
||||
.await
|
||||
|
|
@ -401,7 +399,6 @@ mod tests {
|
|||
/// or making them mutually exclusive).
|
||||
#[tokio::test]
|
||||
async fn index_oracle_still_indexes_v1_updates() {
|
||||
use bitcoin_hashes::hex::FromHex;
|
||||
use bitcoincash::consensus::deserialize;
|
||||
|
||||
// Same hex fixture as `riftenlabs_defi::delphi::tests::test_parse_cauldron_trade`.
|
||||
|
|
@ -418,9 +415,9 @@ mod tests {
|
|||
.expect("v1 indexing succeeds");
|
||||
|
||||
// Should now have exactly one entry, populated from the v1 path.
|
||||
let v1_token =
|
||||
TokenID::from_hex("d0d46f5cbd82188acede0d3e49c75700c19cb8331a30101f0bb6a260066ac972")
|
||||
.unwrap();
|
||||
let v1_token = "d0d46f5cbd82188acede0d3e49c75700c19cb8331a30101f0bb6a260066ac972"
|
||||
.parse::<TokenID>()
|
||||
.unwrap();
|
||||
let entry = get_closest(&pool, &Some(v1_token), 1_747_745_823 + 1)
|
||||
.await
|
||||
.unwrap()
|
||||
|
|
@ -428,7 +425,7 @@ mod tests {
|
|||
assert_eq!(entry.oracle_timestamp, 1_747_745_823);
|
||||
assert_eq!(entry.oracle_price, 38_424); // 384.24 USD
|
||||
assert_eq!(entry.oracle_sequence, 1_144_563);
|
||||
assert_eq!(entry.token_id, v1_token.to_hex());
|
||||
assert_eq!(entry.token_id, v1_token.to_string());
|
||||
}
|
||||
|
||||
/// V2 end-to-end pipeline test.
|
||||
|
|
@ -462,7 +459,7 @@ mod tests {
|
|||
assert_eq!(entry.oracle_timestamp, 1_777_889_180);
|
||||
assert_eq!(entry.oracle_price, 43_972); // $439.72
|
||||
assert_eq!(entry.oracle_sequence, 1_646_804);
|
||||
assert_eq!(entry.token_id, v2_token.to_hex());
|
||||
assert_eq!(entry.token_id, v2_token.to_string());
|
||||
}
|
||||
|
||||
/// Reserve / disabled-feed filter at the indexer's policy boundary.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use bitcoin_hashes::hex::FromHex;
|
||||
use bitcoincash::TokenID;
|
||||
|
||||
/// Token category for the deployed v2 BCH/USD Delphi contract on mainnet.
|
||||
|
|
@ -47,7 +46,9 @@ pub const V1_BCHUSD_TOKEN_HEX: &str =
|
|||
pub fn v2_bchusd_token_id() -> &'static TokenID {
|
||||
static TOKEN: OnceLock<TokenID> = OnceLock::new();
|
||||
TOKEN.get_or_init(|| {
|
||||
TokenID::from_hex(V2_BCHUSD_TOKEN_HEX).expect("v2 BCH/USD token hex is valid")
|
||||
V2_BCHUSD_TOKEN_HEX
|
||||
.parse::<TokenID>()
|
||||
.expect("v2 BCH/USD token hex is valid")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +60,9 @@ pub fn v2_bchusd_token_id() -> &'static TokenID {
|
|||
pub fn v1_bchusd_token_id() -> &'static TokenID {
|
||||
static TOKEN: OnceLock<TokenID> = OnceLock::new();
|
||||
TOKEN.get_or_init(|| {
|
||||
TokenID::from_hex(V1_BCHUSD_TOKEN_HEX).expect("v1 BCH/USD token hex is valid")
|
||||
V1_BCHUSD_TOKEN_HEX
|
||||
.parse::<TokenID>()
|
||||
.expect("v1 BCH/USD token hex is valid")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
use anyhow::Result;
|
||||
use bitcoin_hashes::hex::FromHex;
|
||||
use bitcoincash::TokenID;
|
||||
use sqlx::{Row, SqlitePool};
|
||||
|
||||
|
|
@ -18,7 +17,7 @@ use crate::db::cauldron::tokenlist::db_utils::CachedSort;
|
|||
use crate::db::cauldron::tokenlist::list_cached::TokenListItemCached;
|
||||
|
||||
async fn search_bcmr(bcmr_pool: &SqlitePool, search_query: &str) -> Result<Vec<TokenBasicInfo>> {
|
||||
let is_full_hex_token_id = TokenID::from_hex(search_query).is_ok();
|
||||
let is_full_hex_token_id = search_query.parse::<TokenID>().is_ok();
|
||||
|
||||
let sql = if is_full_hex_token_id {
|
||||
"SELECT token_id, name, symbol
|
||||
|
|
@ -77,7 +76,7 @@ async fn search_bcmr(bcmr_pool: &SqlitePool, search_query: &str) -> Result<Vec<T
|
|||
}
|
||||
|
||||
async fn search_crc20(crc20_pool: &SqlitePool, search_query: &str) -> Result<Vec<TokenBasicInfo>> {
|
||||
let is_full_hex_token_id = TokenID::from_hex(search_query).is_ok();
|
||||
let is_full_hex_token_id = search_query.parse::<TokenID>().is_ok();
|
||||
|
||||
let sql = if is_full_hex_token_id {
|
||||
"SELECT token_id, name, symbol FROM crc20 WHERE token_id = ?1;"
|
||||
|
|
@ -211,7 +210,7 @@ pub async fn db_search_tokens_cached(
|
|||
let q = q.trim();
|
||||
if q.is_empty() {
|
||||
Filter::None
|
||||
} else if TokenID::from_hex(q).is_ok() {
|
||||
} else if q.parse::<TokenID>().is_ok() {
|
||||
where_sql.push_str("WHERE token_id = ?");
|
||||
Filter::Id(display_hex_to_blob::<TokenID>(&q.to_lowercase())?)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ mod tests {
|
|||
use crate::db::search::{db_search_tokens_cached, search_tokens_by_volume};
|
||||
use crate::timeutil::time_now;
|
||||
use crate::utiltest::mock_db_pool;
|
||||
use bitcoin_hashes::hex::{FromHex, ToHex};
|
||||
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::{BlockHash, PubkeyHash, TokenID, Txid};
|
||||
use riftenlabs_defi::cauldron::ParsedContract;
|
||||
|
|
@ -112,9 +112,9 @@ mod tests {
|
|||
.await;
|
||||
|
||||
// three tokens
|
||||
let t_alpha = TokenID::from_inner([0xA1; 32]).to_hex();
|
||||
let t_beta = TokenID::from_inner([0xB2; 32]).to_hex();
|
||||
let t_gamma = TokenID::from_inner([0xC3; 32]).to_hex();
|
||||
let t_alpha = TokenID::from_byte_array([0xA1; 32]).to_string();
|
||||
let t_beta = TokenID::from_byte_array([0xB2; 32]).to_string();
|
||||
let t_gamma = TokenID::from_byte_array([0xC3; 32]).to_string();
|
||||
|
||||
// tvl > 0 so they're visible; make volumes/prices different so sorts are deterministic
|
||||
seed_cached_row_simple(
|
||||
|
|
@ -218,8 +218,8 @@ mod tests {
|
|||
})
|
||||
.await;
|
||||
|
||||
let t1 = TokenID::from_inner([0x11; 32]).to_hex(); // smaller volume
|
||||
let t2 = TokenID::from_inner([0x22; 32]).to_hex(); // larger volume
|
||||
let t1 = TokenID::from_byte_array([0x11; 32]).to_string(); // smaller volume
|
||||
let t2 = TokenID::from_byte_array([0x22; 32]).to_string(); // larger volume
|
||||
|
||||
seed_cached_row_simple(
|
||||
&mock.cauldron_w,
|
||||
|
|
@ -299,9 +299,9 @@ mod tests {
|
|||
})
|
||||
.await;
|
||||
|
||||
let t_low = TokenID::from_inner([0x33; 32]).to_hex();
|
||||
let t_mid = TokenID::from_inner([0x44; 32]).to_hex();
|
||||
let t_high = TokenID::from_inner([0x55; 32]).to_hex();
|
||||
let t_low = TokenID::from_byte_array([0x33; 32]).to_string();
|
||||
let t_mid = TokenID::from_byte_array([0x44; 32]).to_string();
|
||||
let t_high = TokenID::from_byte_array([0x55; 32]).to_string();
|
||||
|
||||
// make USD price ascending: low < mid < high
|
||||
seed_cached_row_simple(
|
||||
|
|
@ -564,16 +564,15 @@ mod tests {
|
|||
dummy_init_seq();
|
||||
let current_timestamp = time_now();
|
||||
let thirty_days_ago = current_timestamp - 30 * 24 * 60 * 60;
|
||||
let owner_pkh = PubkeyHash::from_inner([0xca; 20]);
|
||||
let owner_pkh = PubkeyHash::from_byte_array([0xca; 20]);
|
||||
let block_zero = BlockHash::all_zeros();
|
||||
|
||||
let mut conn = pool.acquire().await?;
|
||||
|
||||
// ================== TOKEN 1 (BCMR Token with Volume) ==================
|
||||
let utxo1 = OutPointHash::from_hex(
|
||||
"a3f1d42e2a5c9f2b5f1b9d7f7c2b19e7a3b1d2c3f4a5e6f2d1c3e4f5a1b2c3d4",
|
||||
)?;
|
||||
let token_id1: TokenID = TokenID::from_inner([0xda; 32]);
|
||||
let utxo1 = "a3f1d42e2a5c9f2b5f1b9d7f7c2b19e7a3b1d2c3f4a5e6f2d1c3e4f5a1b2c3d4"
|
||||
.parse::<OutPointHash>()?;
|
||||
let token_id1: TokenID = TokenID::from_byte_array([0xda; 32]);
|
||||
|
||||
let token1 = Token {
|
||||
category: "asset_category".to_string(),
|
||||
|
|
@ -599,9 +598,9 @@ mod tests {
|
|||
|
||||
insert_bcmr_data(pool, &token_id1, &utxo1, &parsed_bcmr1).await?;
|
||||
|
||||
let txid1 = Txid::from_inner([0xf0; 32]);
|
||||
let txid1 = Txid::from_byte_array([0xf0; 32]);
|
||||
let cauldron1 = dummy_cauldron(&txid1, &utxo1, &token_id1, 1000, 500, &owner_pkh);
|
||||
insert_new_pool(&mut *conn, &cauldron1).await?;
|
||||
insert_new_pool(&mut conn, &cauldron1).await?;
|
||||
|
||||
insert_authheader(
|
||||
pool,
|
||||
|
|
@ -614,13 +613,13 @@ mod tests {
|
|||
)
|
||||
.await?;
|
||||
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron1.clone()], &txid1).await?;
|
||||
insert_block_tx(&mut *conn, &txid1, &block_zero, thirty_days_ago).await?;
|
||||
insert_mempool_tx(&mut *conn, &txid1, thirty_days_ago as u64).await?;
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron1.clone()], &txid1).await?;
|
||||
insert_block_tx(&mut conn, &txid1, &block_zero, thirty_days_ago).await?;
|
||||
insert_mempool_tx(&mut conn, &txid1, thirty_days_ago as u64).await?;
|
||||
|
||||
// Simulate volume on token1
|
||||
let txid1_spend = Txid::from_inner([0xf1; 32]);
|
||||
let utxo1_spend = OutPointHash::from_inner([0xe1; 32]);
|
||||
let txid1_spend = Txid::from_byte_array([0xf1; 32]);
|
||||
let utxo1_spend = OutPointHash::from_byte_array([0xe1; 32]);
|
||||
let cauldron1_spent = ParsedContract {
|
||||
pkh: owner_pkh,
|
||||
is_withdrawn: false,
|
||||
|
|
@ -633,12 +632,12 @@ mod tests {
|
|||
token_amount: Some(1000),
|
||||
};
|
||||
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron1_spent.clone()], &txid1_spend).await?;
|
||||
insert_block_tx(&mut *conn, &txid1_spend, &block_zero, current_timestamp).await?;
|
||||
insert_mempool_tx(&mut *conn, &txid1_spend, current_timestamp as u64).await?;
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron1_spent.clone()], &txid1_spend).await?;
|
||||
insert_block_tx(&mut conn, &txid1_spend, &block_zero, current_timestamp).await?;
|
||||
insert_mempool_tx(&mut conn, &txid1_spend, current_timestamp as u64).await?;
|
||||
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo1,
|
||||
&cauldron1,
|
||||
Some(thirty_days_ago as u64),
|
||||
|
|
@ -648,7 +647,7 @@ mod tests {
|
|||
)
|
||||
.await?;
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo1,
|
||||
&cauldron1_spent,
|
||||
Some(current_timestamp as u64),
|
||||
|
|
@ -659,10 +658,9 @@ mod tests {
|
|||
.await?;
|
||||
|
||||
// ================== TOKEN 2 (BCMR Token) ==================
|
||||
let utxo2 = OutPointHash::from_hex(
|
||||
"c3d2e1f4b6a7c8d1e2f5d7c3b1a9e4f2b1d3f4e6c2b9f3a8d1e4f5b6c3d2e7a4",
|
||||
)?;
|
||||
let token_id2 = TokenID::from_inner([0xdb; 32]);
|
||||
let utxo2 = "c3d2e1f4b6a7c8d1e2f5d7c3b1a9e4f2b1d3f4e6c2b9f3a8d1e4f5b6c3d2e7a4"
|
||||
.parse::<OutPointHash>()?;
|
||||
let token_id2 = TokenID::from_byte_array([0xdb; 32]);
|
||||
|
||||
let token2 = Token {
|
||||
category: "asset_category_2".to_string(),
|
||||
|
|
@ -688,9 +686,9 @@ mod tests {
|
|||
|
||||
insert_bcmr_data(pool, &token_id2, &utxo2, &parsed_bcmr2).await?;
|
||||
|
||||
let txid2_init = Txid::from_inner([0xf2; 32]);
|
||||
let txid2_init = Txid::from_byte_array([0xf2; 32]);
|
||||
let cauldron2 = dummy_cauldron(&txid2_init, &utxo2, &token_id2, 1500, 700, &owner_pkh);
|
||||
insert_new_pool(&mut *conn, &cauldron2).await?;
|
||||
insert_new_pool(&mut conn, &cauldron2).await?;
|
||||
|
||||
insert_authheader(
|
||||
pool,
|
||||
|
|
@ -703,12 +701,12 @@ mod tests {
|
|||
)
|
||||
.await?;
|
||||
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron2.clone()], &txid2_init).await?;
|
||||
insert_block_tx(&mut *conn, &txid2_init, &block_zero, thirty_days_ago).await?;
|
||||
insert_mempool_tx(&mut *conn, &txid2_init, thirty_days_ago as u64).await?;
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron2.clone()], &txid2_init).await?;
|
||||
insert_block_tx(&mut conn, &txid2_init, &block_zero, thirty_days_ago).await?;
|
||||
insert_mempool_tx(&mut conn, &txid2_init, thirty_days_ago as u64).await?;
|
||||
|
||||
let txid2_spend = Txid::from_inner([0xf3; 32]);
|
||||
let utxo2_spend = OutPointHash::from_inner([0xe2; 32]);
|
||||
let txid2_spend = Txid::from_byte_array([0xf3; 32]);
|
||||
let utxo2_spend = OutPointHash::from_byte_array([0xe2; 32]);
|
||||
let cauldron2_spent = ParsedContract {
|
||||
pkh: owner_pkh,
|
||||
is_withdrawn: false,
|
||||
|
|
@ -721,12 +719,12 @@ mod tests {
|
|||
token_amount: Some(1500),
|
||||
};
|
||||
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron2_spent.clone()], &txid2_spend).await?;
|
||||
insert_block_tx(&mut *conn, &txid2_spend, &block_zero, current_timestamp).await?;
|
||||
insert_mempool_tx(&mut *conn, &txid2_spend, current_timestamp as u64).await?;
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron2_spent.clone()], &txid2_spend).await?;
|
||||
insert_block_tx(&mut conn, &txid2_spend, &block_zero, current_timestamp).await?;
|
||||
insert_mempool_tx(&mut conn, &txid2_spend, current_timestamp as u64).await?;
|
||||
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo2,
|
||||
&cauldron2,
|
||||
Some(thirty_days_ago as u64),
|
||||
|
|
@ -736,7 +734,7 @@ mod tests {
|
|||
)
|
||||
.await?;
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo2,
|
||||
&cauldron2_spent,
|
||||
Some(current_timestamp as u64),
|
||||
|
|
@ -747,10 +745,9 @@ mod tests {
|
|||
.await?;
|
||||
|
||||
// ================== TOKEN 3 (BCMR Token with No Volume) ==================
|
||||
let utxo3 = OutPointHash::from_hex(
|
||||
"d5e1f2a3b4c3d2f5b6a8e7d3c2f4b9a6d2e3f1c4b5a9e3d1b2c5f7a3d4b8e2c3",
|
||||
)?;
|
||||
let token_id3 = TokenID::from_inner([0xdd; 32]);
|
||||
let utxo3 = "d5e1f2a3b4c3d2f5b6a8e7d3c2f4b9a6d2e3f1c4b5a9e3d1b2c5f7a3d4b8e2c3"
|
||||
.parse::<OutPointHash>()?;
|
||||
let token_id3 = TokenID::from_byte_array([0xdd; 32]);
|
||||
|
||||
let token3 = Token {
|
||||
category: "asset_category_3".to_string(),
|
||||
|
|
@ -776,9 +773,9 @@ mod tests {
|
|||
|
||||
insert_bcmr_data(pool, &token_id3, &utxo3, &parsed_bcmr3).await?;
|
||||
|
||||
let txid3_init = Txid::from_inner([0xf6; 32]);
|
||||
let txid3_init = Txid::from_byte_array([0xf6; 32]);
|
||||
let cauldron3_init = dummy_cauldron(&txid3_init, &utxo3, &token_id3, 1000, 500, &owner_pkh);
|
||||
insert_new_pool(&mut *conn, &cauldron3_init).await?;
|
||||
insert_new_pool(&mut conn, &cauldron3_init).await?;
|
||||
|
||||
insert_authheader(
|
||||
pool,
|
||||
|
|
@ -791,12 +788,12 @@ mod tests {
|
|||
)
|
||||
.await?;
|
||||
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron3_init.clone()], &txid3_init).await?;
|
||||
insert_block_tx(&mut *conn, &txid3_init, &block_zero, thirty_days_ago).await?;
|
||||
insert_mempool_tx(&mut *conn, &txid3_init, thirty_days_ago as u64).await?;
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron3_init.clone()], &txid3_init).await?;
|
||||
insert_block_tx(&mut conn, &txid3_init, &block_zero, thirty_days_ago).await?;
|
||||
insert_mempool_tx(&mut conn, &txid3_init, thirty_days_ago as u64).await?;
|
||||
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo3,
|
||||
&cauldron3_init,
|
||||
Some(thirty_days_ago as u64),
|
||||
|
|
@ -807,10 +804,9 @@ mod tests {
|
|||
.await?;
|
||||
|
||||
// ================== TOKEN 4 (CRC20 Token with Volume) ==================
|
||||
let utxo4_initial = OutPointHash::from_hex(
|
||||
"f1d4e2a3b5c4d2f7b6a8e7d3c2f4b9a6d3e1f1c4b7a8e2d3b6c7f9a5d4b1e6c3",
|
||||
)?;
|
||||
let token_id4 = TokenID::from_inner([0xdc; 32]);
|
||||
let utxo4_initial = "f1d4e2a3b5c4d2f7b6a8e7d3c2f4b9a6d3e1f1c4b7a8e2d3b6c7f9a5d4b1e6c3"
|
||||
.parse::<OutPointHash>()?;
|
||||
let token_id4 = TokenID::from_byte_array([0xdc; 32]);
|
||||
|
||||
sqlx::query("INSERT INTO crc20 (token_id, name, symbol, decimals) VALUES (?, ?, ?, ?)")
|
||||
.bind(token_id4.to_blob())
|
||||
|
|
@ -820,7 +816,7 @@ mod tests {
|
|||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
let txid4_init = Txid::from_inner([0xf4; 32]);
|
||||
let txid4_init = Txid::from_byte_array([0xf4; 32]);
|
||||
let cauldron4_init = dummy_cauldron(
|
||||
&txid4_init,
|
||||
&utxo4_initial,
|
||||
|
|
@ -830,13 +826,13 @@ mod tests {
|
|||
&owner_pkh,
|
||||
);
|
||||
|
||||
insert_new_pool(&mut *conn, &cauldron4_init).await?;
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron4_init.clone()], &txid4_init).await?;
|
||||
insert_block_tx(&mut *conn, &txid4_init, &block_zero, thirty_days_ago).await?;
|
||||
insert_mempool_tx(&mut *conn, &txid4_init, thirty_days_ago as u64).await?;
|
||||
insert_new_pool(&mut conn, &cauldron4_init).await?;
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron4_init.clone()], &txid4_init).await?;
|
||||
insert_block_tx(&mut conn, &txid4_init, &block_zero, thirty_days_ago).await?;
|
||||
insert_mempool_tx(&mut conn, &txid4_init, thirty_days_ago as u64).await?;
|
||||
|
||||
let txid4_spend = Txid::from_inner([0xf5; 32]);
|
||||
let utxo4_spent = OutPointHash::from_inner([0xe4; 32]);
|
||||
let txid4_spend = Txid::from_byte_array([0xf5; 32]);
|
||||
let utxo4_spent = OutPointHash::from_byte_array([0xe4; 32]);
|
||||
let cauldron4_spent = ParsedContract {
|
||||
pkh: owner_pkh,
|
||||
is_withdrawn: false,
|
||||
|
|
@ -849,12 +845,12 @@ mod tests {
|
|||
token_amount: Some(1500),
|
||||
};
|
||||
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron4_spent.clone()], &txid4_spend).await?;
|
||||
insert_block_tx(&mut *conn, &txid4_spend, &block_zero, current_timestamp).await?;
|
||||
insert_mempool_tx(&mut *conn, &txid4_spend, current_timestamp as u64).await?;
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron4_spent.clone()], &txid4_spend).await?;
|
||||
insert_block_tx(&mut conn, &txid4_spend, &block_zero, current_timestamp).await?;
|
||||
insert_mempool_tx(&mut conn, &txid4_spend, current_timestamp as u64).await?;
|
||||
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo4_initial,
|
||||
&cauldron4_init,
|
||||
Some(thirty_days_ago as u64),
|
||||
|
|
@ -864,7 +860,7 @@ mod tests {
|
|||
)
|
||||
.await?;
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo4_initial,
|
||||
&cauldron4_spent,
|
||||
Some(current_timestamp as u64),
|
||||
|
|
@ -887,10 +883,10 @@ mod tests {
|
|||
dummy_init_seq();
|
||||
let block_zero = BlockHash::all_zeros();
|
||||
|
||||
let token_id = TokenID::from_inner([0xAB; 32]);
|
||||
let token_id_hex = token_id.to_hex();
|
||||
let utxo = OutPointHash::from_inner([0xCD; 32]);
|
||||
let txid = Txid::from_inner([0xEF; 32]);
|
||||
let token_id = TokenID::from_byte_array([0xAB; 32]);
|
||||
let token_id_hex = token_id.to_string();
|
||||
let utxo = OutPointHash::from_byte_array([0xCD; 32]);
|
||||
let txid = Txid::from_byte_array([0xEF; 32]);
|
||||
|
||||
// Insert into auth_chain_entry WITHOUT bcmr_data
|
||||
insert_authheader(
|
||||
|
|
@ -984,10 +980,10 @@ mod tests {
|
|||
})
|
||||
.await;
|
||||
|
||||
let tok_named = TokenID::from_inner([0x62; 32]);
|
||||
let tok_null = TokenID::from_inner([0x63; 32]);
|
||||
let t_named = tok_named.to_hex();
|
||||
let t_null = tok_null.to_hex();
|
||||
let tok_named = TokenID::from_byte_array([0x62; 32]);
|
||||
let tok_null = TokenID::from_byte_array([0x63; 32]);
|
||||
let t_named = tok_named.to_string();
|
||||
let t_null = tok_null.to_string();
|
||||
|
||||
// Named row
|
||||
sqlx::query(
|
||||
|
|
@ -1036,7 +1032,7 @@ mod tests {
|
|||
// ==================== ENDIANNESS / BYTE ORDER TESTS ====================
|
||||
|
||||
/// Regression test: Proves that using hex::decode() for queries does NOT work
|
||||
/// when the blob was stored using TokenID::from_hex().to_blob().
|
||||
/// when the blob was stored using .parse::<TokenID>().to_blob().
|
||||
#[tokio::test]
|
||||
async fn test_token_lookup_fails_with_raw_hex_decode() {
|
||||
use crate::db::blob::display_hex_to_blob;
|
||||
|
|
@ -1048,7 +1044,7 @@ mod tests {
|
|||
|
||||
// Real-world-like token ID (asymmetric, not palindromic)
|
||||
let display_hex = "83e12eea20b19a9a0906bb0521ff18520db69a4a8136293bafbfca0acb2c2313";
|
||||
let token = TokenID::from_hex(display_hex).unwrap();
|
||||
let token = display_hex.parse::<TokenID>().unwrap();
|
||||
|
||||
// Store using correct method: to_blob()
|
||||
sqlx::query(
|
||||
|
|
@ -1100,7 +1096,7 @@ mod tests {
|
|||
|
||||
// Asymmetric token ID
|
||||
let display_hex = "83e12eea20b19a9a0906bb0521ff18520db69a4a8136293bafbfca0acb2c2313";
|
||||
let token = TokenID::from_hex(display_hex).unwrap();
|
||||
let token = display_hex.parse::<TokenID>().unwrap();
|
||||
|
||||
// Insert into crc20
|
||||
sqlx::query(
|
||||
|
|
@ -1135,7 +1131,7 @@ mod tests {
|
|||
let wrong_blob = hex_to_blob(display_hex);
|
||||
|
||||
// Correct conversion
|
||||
let correct_blob = TokenID::from_hex(display_hex).unwrap().to_blob();
|
||||
let correct_blob = display_hex.parse::<TokenID>().unwrap().to_blob();
|
||||
|
||||
// They MUST differ
|
||||
assert_ne!(
|
||||
|
|
|
|||
10
src/def.rs
10
src/def.rs
|
|
@ -6,9 +6,7 @@
|
|||
use bitcoin_hashes::hash_newtype;
|
||||
use bitcoin_hashes::sha256d;
|
||||
|
||||
hash_newtype!(
|
||||
PoolID,
|
||||
sha256d::Hash,
|
||||
32,
|
||||
doc = "An identifier for a Cauldron pool (hash of its creation outpoint)"
|
||||
);
|
||||
hash_newtype! {
|
||||
/// An identifier for a Cauldron pool (hash of its creation outpoint)
|
||||
pub struct PoolID(sha256d::Hash);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,15 @@
|
|||
use std::{collections::HashSet, str::FromStr};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
use bitcoincash::{consensus::deserialize, BlockHeader, Transaction, Txid};
|
||||
use bitcoincash::{
|
||||
blockdata::block::Header as BlockHeader, consensus::deserialize, Transaction, Txid,
|
||||
};
|
||||
use electrum_client_netagnostic::{Client, ElectrumApi, Param};
|
||||
use log::info;
|
||||
use riftenlabs_defi::{
|
||||
cauldron::V2_CONTRACT_TEMPLATE,
|
||||
delphi::{v2::DELPHI_V2_REDEEM_SCRIPT_BODY, DELPHI_REDEEM_SCRIPT},
|
||||
tokentoken::{CONJURE_HINT_PREFIX, TOKENTOKEN_CONTRACT_CODE},
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
|
|
@ -36,7 +38,7 @@ pub fn electrum_get_tip(client: &Client) -> Result<(BlockHeader, u64)> {
|
|||
Ok((deserialize(&hex::decode(header)?)?, height as u64))
|
||||
}
|
||||
|
||||
/// Fetch cauldron mempool transactions
|
||||
/// Fetch defi (cauldron + tokentoken) and oracle mempool transactions
|
||||
pub fn electrum_fetch_mempool(client: &Client) -> Result<(HashSet<Txid>, HashSet<Txid>)> {
|
||||
let cauldron_filter = json!({
|
||||
"scriptsig": hex::encode(&V2_CONTRACT_TEMPLATE[(V2_CONTRACT_TEMPLATE.len() - 43)..]), // cauldron spends
|
||||
|
|
@ -44,6 +46,20 @@ pub fn electrum_fetch_mempool(client: &Client) -> Result<(HashSet<Txid>, HashSet
|
|||
"operation": "union"
|
||||
});
|
||||
|
||||
// The contract code is the constant tail of every tokentoken redeem
|
||||
// script, pushed in full in spending scriptsigs; creations carry an
|
||||
// `OP_RETURN "CONJURE"` hint output.
|
||||
let conjure_hint_prefix: Vec<u8> = [0x6a /* op_return */, 0x07 /* push */]
|
||||
.iter()
|
||||
.chain(CONJURE_HINT_PREFIX)
|
||||
.copied()
|
||||
.collect();
|
||||
let tokentoken_filter = json!({
|
||||
"scriptsig": hex::encode(TOKENTOKEN_CONTRACT_CODE), // pool spends
|
||||
"scriptpubkey": hex::encode(&conjure_hint_prefix), // new pools (potentially)
|
||||
"operation": "union"
|
||||
});
|
||||
|
||||
// v1 oracle filter: matches the cashc 0.10.5 redeem-script template.
|
||||
let oracle_v1_filter = json!({
|
||||
"scriptsig": hex::encode(DELPHI_REDEEM_SCRIPT),
|
||||
|
|
@ -82,18 +98,24 @@ pub fn electrum_fetch_mempool(client: &Client) -> Result<(HashSet<Txid>, HashSet
|
|||
.collect())
|
||||
};
|
||||
|
||||
let cauldron_txs = fetch_txs(cauldron_filter)?;
|
||||
// Cauldron and tokentoken txs share one set: update_mempool diffs it
|
||||
// against the stored mempool (tx table) to decide adds and deletes.
|
||||
let mut defi_txs = fetch_txs(cauldron_filter)?;
|
||||
defi_txs.extend(fetch_txs(tokentoken_filter)?);
|
||||
let mut oracle_txs = fetch_txs(oracle_v1_filter)?;
|
||||
oracle_txs.extend(fetch_txs(oracle_v2_filter)?);
|
||||
|
||||
Ok((cauldron_txs, oracle_txs))
|
||||
Ok((defi_txs, oracle_txs))
|
||||
}
|
||||
|
||||
/// Fetch blockchain tip from electrum server
|
||||
pub fn electrum_get_tx(client: &Client, txid: &Txid) -> Result<Transaction> {
|
||||
let tx: Value = serde_json::from_str(
|
||||
&client
|
||||
.raw_call("blockchain.transaction.get", [Param::String(txid.to_hex())])?
|
||||
.raw_call(
|
||||
"blockchain.transaction.get",
|
||||
[Param::String(txid.to_string())],
|
||||
)?
|
||||
.to_string(),
|
||||
)?;
|
||||
|
||||
|
|
|
|||
154
src/index.rs
154
src/index.rs
|
|
@ -9,17 +9,16 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
use bitcoin_hashes::{
|
||||
hex::{FromHex, ToHex},
|
||||
Hash,
|
||||
};
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::{
|
||||
consensus::deserialize, Block, BlockHash, BlockHeader, Network, TokenID, Transaction, Txid,
|
||||
blockdata::block::Header as BlockHeader, consensus::deserialize, Block, BlockHash, Network,
|
||||
TokenID, Transaction, Txid,
|
||||
};
|
||||
use electrum_client_netagnostic::{Client, ElectrumApi, Param};
|
||||
use log::{debug, info, warn};
|
||||
use riftenlabs_defi::cauldron::{parse_cauldrons_from_tx, ParsedContract};
|
||||
use riftenlabs_defi::moria::MoriaTokenIds;
|
||||
use riftenlabs_defi::tokentoken::parse_tokentoken_from_tx;
|
||||
|
||||
use crate::{
|
||||
bcmr::index_bcmr,
|
||||
|
|
@ -50,24 +49,20 @@ use anyhow::{bail, Context, Result};
|
|||
fn moria_token_ids(network: Option<Network>) -> MoriaTokenIds {
|
||||
match network {
|
||||
Some(Network::Chipnet) => MoriaTokenIds {
|
||||
moria: TokenID::from_hex(
|
||||
"29566f4884539dbcedfcb55cc7f5e66b5ae14975b70c0b6eb12de7e9707775ea",
|
||||
)
|
||||
.expect("valid chipnet moria token_id"),
|
||||
bp_oracle: TokenID::from_hex(
|
||||
"f3d6b85bfb0eaaf417ccabc8c8032464c5ec410e40b3b13f0c369a541bfb2a6a",
|
||||
)
|
||||
.expect("valid chipnet bp_oracle token_id"),
|
||||
moria: "29566f4884539dbcedfcb55cc7f5e66b5ae14975b70c0b6eb12de7e9707775ea"
|
||||
.parse::<TokenID>()
|
||||
.expect("valid chipnet moria token_id"),
|
||||
bp_oracle: "f3d6b85bfb0eaaf417ccabc8c8032464c5ec410e40b3b13f0c369a541bfb2a6a"
|
||||
.parse::<TokenID>()
|
||||
.expect("valid chipnet bp_oracle token_id"),
|
||||
},
|
||||
_ => MoriaTokenIds {
|
||||
moria: TokenID::from_hex(
|
||||
"b38a33f750f84c5c169a6f23cb873e6e79605021585d4f3408789689ed87f366",
|
||||
)
|
||||
.expect("valid mainnet moria token_id"),
|
||||
bp_oracle: TokenID::from_hex(
|
||||
"01711e39e7bf3b8ca0d9a6fc6ea32e340caa1d64dc7d1dc51fae20fd66755558",
|
||||
)
|
||||
.expect("valid mainnet bp_oracle token_id"),
|
||||
moria: "b38a33f750f84c5c169a6f23cb873e6e79605021585d4f3408789689ed87f366"
|
||||
.parse::<TokenID>()
|
||||
.expect("valid mainnet moria token_id"),
|
||||
bp_oracle: "01711e39e7bf3b8ca0d9a6fc6ea32e340caa1d64dc7d1dc51fae20fd66755558"
|
||||
.parse::<TokenID>()
|
||||
.expect("valid mainnet bp_oracle token_id"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -77,13 +72,13 @@ pub async fn update_mempool(db: &DB, electrum: Arc<Mutex<Client>>) -> Result<()>
|
|||
db::cauldron::mempool::load_mempool(&db.cauldron_w).await?;
|
||||
|
||||
let electrum_clone = electrum.clone();
|
||||
let (cauldron_txs, oracle_txs) = tokio::task::spawn_blocking(move || {
|
||||
let (defi_txs, oracle_txs) = tokio::task::spawn_blocking(move || {
|
||||
electrum_fetch_mempool(&electrum_clone.lock().unwrap())
|
||||
})
|
||||
.await??;
|
||||
|
||||
let txs_to_delete: Vec<Txid> = our_mempool_txs.difference(&cauldron_txs).cloned().collect();
|
||||
let txs_to_add: Vec<&Txid> = cauldron_txs.difference(&our_mempool_txs).collect();
|
||||
let txs_to_delete: Vec<Txid> = our_mempool_txs.difference(&defi_txs).cloned().collect();
|
||||
let txs_to_add: Vec<&Txid> = defi_txs.difference(&our_mempool_txs).collect();
|
||||
|
||||
let electrum_for_fetch = electrum.clone();
|
||||
let txids_to_fetch: Vec<Txid> = txs_to_add.iter().map(|t| **t).collect();
|
||||
|
|
@ -111,28 +106,34 @@ pub async fn update_mempool(db: &DB, electrum: Arc<Mutex<Client>>) -> Result<()>
|
|||
db::cauldron::mempool::delete_mempool_txs(&mut db_tx, txs_to_delete.iter()).await?;
|
||||
|
||||
let mut all_cauldrons = vec![];
|
||||
let mut all_tokentokens = vec![];
|
||||
let current_timestamp = time_now() as u64;
|
||||
|
||||
for btx in txs_to_add {
|
||||
let cauldrons: Vec<ParsedContract> = parse_cauldrons_from_tx(&btx);
|
||||
let tokentokens = parse_tokentoken_from_tx(&btx);
|
||||
|
||||
if cauldrons.is_empty() {
|
||||
info!("ignoring non-cauldron tx {}", btx.txid());
|
||||
if cauldrons.is_empty() && tokentokens.is_empty() {
|
||||
info!("ignoring non-defi tx {}", btx.compute_txid());
|
||||
continue;
|
||||
}
|
||||
let txid = btx.txid();
|
||||
let txid = btx.compute_txid();
|
||||
db::cauldron::tx::insert_mempool_tx(&mut db_tx, &txid, current_timestamp).await?;
|
||||
info!(
|
||||
"mempool add {} with {} cauldrons",
|
||||
txid.to_hex(),
|
||||
cauldrons.len()
|
||||
"mempool add {} with {} cauldrons, {} tokentoken states",
|
||||
txid,
|
||||
cauldrons.len(),
|
||||
tokentokens.len()
|
||||
);
|
||||
|
||||
insert_utxo_funding(&mut db_tx, &cauldrons, &txid).await?;
|
||||
insert_utxo_spending(&mut db_tx, &cauldrons, &txid, false).await?;
|
||||
insert_user_action(&mut db_tx, &cauldrons, &btx, true).await?;
|
||||
if !cauldrons.is_empty() {
|
||||
insert_utxo_funding(&mut db_tx, &cauldrons, &txid).await?;
|
||||
insert_utxo_spending(&mut db_tx, &cauldrons, &txid, false).await?;
|
||||
insert_user_action(&mut db_tx, &cauldrons, &btx, true).await?;
|
||||
|
||||
all_cauldrons.extend(cauldrons);
|
||||
all_cauldrons.extend(cauldrons);
|
||||
}
|
||||
all_tokentokens.extend(tokentokens);
|
||||
}
|
||||
|
||||
db::cauldron::pool::update_pool_history(
|
||||
|
|
@ -142,6 +143,13 @@ pub async fn update_mempool(db: &DB, electrum: Arc<Mutex<Client>>) -> Result<()>
|
|||
Some(current_timestamp),
|
||||
)
|
||||
.await?;
|
||||
db::cauldron::tokentoken::update_tokentoken_pool_history(
|
||||
&mut db_tx,
|
||||
all_tokentokens,
|
||||
None,
|
||||
Some(current_timestamp),
|
||||
)
|
||||
.await?;
|
||||
db_tx.commit().await?;
|
||||
}
|
||||
|
||||
|
|
@ -198,11 +206,14 @@ pub async fn index_blocks(
|
|||
|
||||
// Validate network before proceeding
|
||||
let start_block_hash = match network {
|
||||
Some(Network::Chipnet) => BlockHash::from_hex(CHIPNET_START_BLOCK)
|
||||
Some(Network::Chipnet) => CHIPNET_START_BLOCK
|
||||
.parse::<BlockHash>()
|
||||
.map_err(|e| anyhow::anyhow!("Invalid CHIPNET_START_BLOCK: {}", e))?,
|
||||
Some(Network::Bitcoin) => BlockHash::from_hex(CASHTOKEN_ACTIVATION_HEIGHT)
|
||||
Some(Network::Bitcoin) => CASHTOKEN_ACTIVATION_HEIGHT
|
||||
.parse::<BlockHash>()
|
||||
.map_err(|e| anyhow::anyhow!("Invalid CASHTOKEN_ACTIVATION_HEIGHT: {}", e))?,
|
||||
None => BlockHash::from_hex(CASHTOKEN_ACTIVATION_HEIGHT)
|
||||
None => CASHTOKEN_ACTIVATION_HEIGHT
|
||||
.parse::<BlockHash>()
|
||||
.map_err(|e| anyhow::anyhow!("Invalid CASHTOKEN_ACTIVATION_HEIGHT: {}", e))?,
|
||||
Some(net) => bail!("Unknown network: {:?}", net),
|
||||
};
|
||||
|
|
@ -220,8 +231,8 @@ pub async fn index_blocks(
|
|||
let chain_guard = chain.lock().unwrap();
|
||||
debug!(
|
||||
"Updating header chain from {} to {}",
|
||||
chain_guard.tip_hash().to_hex(),
|
||||
tip_header.block_hash().to_hex()
|
||||
chain_guard.tip_hash(),
|
||||
tip_header.block_hash()
|
||||
);
|
||||
}
|
||||
let client_for_headers = client.clone();
|
||||
|
|
@ -267,7 +278,7 @@ pub async fn index_blocks(
|
|||
if start_height > 0 {
|
||||
warn!("--start-height is ignored because database already has indexed blocks. Delete the database to re-index from a different height.");
|
||||
}
|
||||
BlockHash::from_hex(&last).unwrap()
|
||||
last.parse::<BlockHash>().unwrap()
|
||||
} else if start_height > 0 {
|
||||
// Resolve configured start height to a block hash via electrum
|
||||
let resp = client_cpy
|
||||
|
|
@ -283,7 +294,7 @@ pub async fn index_blocks(
|
|||
info!(
|
||||
"Starting indexing from configured height {} ({})",
|
||||
start_height,
|
||||
header.block_hash().to_hex()
|
||||
header.block_hash()
|
||||
);
|
||||
header.block_hash()
|
||||
} else {
|
||||
|
|
@ -295,20 +306,14 @@ pub async fn index_blocks(
|
|||
if chain_cpy.lock().unwrap().contains(&last_indexed) {
|
||||
break;
|
||||
}
|
||||
info!(
|
||||
"Last indexed block ({}) has been orphaned",
|
||||
last_indexed.to_hex()
|
||||
);
|
||||
info!("Last indexed block ({}) has been orphaned", last_indexed);
|
||||
let header = handle
|
||||
.block_on(db_get_header(&db.cauldron_r, &last_indexed))
|
||||
.context("Failed to get header for last indexed")
|
||||
.unwrap();
|
||||
last_indexed = header.prev_blockhash;
|
||||
|
||||
info!(
|
||||
"Last indexed block rolled back to {}",
|
||||
last_indexed.to_hex()
|
||||
);
|
||||
info!("Last indexed block rolled back to {}", last_indexed);
|
||||
}
|
||||
|
||||
loop {
|
||||
|
|
@ -373,39 +378,55 @@ pub async fn index_blocks(
|
|||
let mut total_cauldrons = 0;
|
||||
|
||||
let mut all_cauldrons = vec![];
|
||||
let mut all_tokentokens = vec![];
|
||||
|
||||
let sorted_txs = ttor_sorted_kahn(block.txdata);
|
||||
|
||||
for tx in &sorted_txs {
|
||||
let cauldrons = parse_cauldrons_from_tx(tx);
|
||||
if cauldrons.is_empty() {
|
||||
// Note: parse_tokentoken_from_tx skips the pool-creation check when
|
||||
// any input spends an existing pool, so a single tx that both swaps
|
||||
// pool P and creates pool Q drops Q (upstream riftenlabs-defi
|
||||
// limitation, shared with parse_cauldrons_from_tx).
|
||||
let tokentokens = parse_tokentoken_from_tx(tx);
|
||||
if cauldrons.is_empty() && tokentokens.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let txid = tx.txid();
|
||||
let txid = tx.compute_txid();
|
||||
insert_block_tx(&mut db_tx, &txid, &blockhash, mtp as i64)
|
||||
.await
|
||||
.context("inserting block tx")?;
|
||||
insert_utxo_funding(&mut db_tx, &cauldrons, &txid)
|
||||
.await
|
||||
.context("inserting funding utxos")?;
|
||||
insert_utxo_spending(&mut db_tx, &cauldrons, &txid, true)
|
||||
.await
|
||||
.context("inserting spending utxos")?;
|
||||
insert_user_action(&mut db_tx, &cauldrons, tx, true)
|
||||
.await
|
||||
.context("inserting user actions")?;
|
||||
if !cauldrons.is_empty() {
|
||||
insert_utxo_funding(&mut db_tx, &cauldrons, &txid)
|
||||
.await
|
||||
.context("inserting funding utxos")?;
|
||||
insert_utxo_spending(&mut db_tx, &cauldrons, &txid, true)
|
||||
.await
|
||||
.context("inserting spending utxos")?;
|
||||
insert_user_action(&mut db_tx, &cauldrons, tx, true)
|
||||
.await
|
||||
.context("inserting user actions")?;
|
||||
|
||||
total_cauldrons += cauldrons.len();
|
||||
total_cauldrons += cauldrons.len();
|
||||
all_cauldrons.extend(cauldrons);
|
||||
}
|
||||
|
||||
all_cauldrons.extend(cauldrons);
|
||||
all_tokentokens.extend(tokentokens);
|
||||
}
|
||||
|
||||
// Figuring out initial utxo needs to be done on all cauldrons in a block.
|
||||
db::cauldron::pool::update_pool_history(&mut db_tx, all_cauldrons, Some(mtp), None).await?;
|
||||
db::cauldron::tokentoken::update_tokentoken_pool_history(
|
||||
&mut db_tx,
|
||||
all_tokentokens,
|
||||
Some(mtp),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
// config_set must be inside the cauldron transaction, as it tracks what the
|
||||
// last successful block indexed was. It must commit atomically with block data.
|
||||
config_set(&mut *db_tx, KEY_LAST_INDEXED, &blockhash.to_hex()).await;
|
||||
config_set(&mut *db_tx, KEY_LAST_INDEXED, &blockhash.to_string()).await;
|
||||
|
||||
// crc20
|
||||
index_crc20(&db.crc20_w, &sorted_txs).await?;
|
||||
|
|
@ -439,12 +460,7 @@ pub async fn index_blocks(
|
|||
|
||||
info!(
|
||||
"Indexed {}; mtp: {}, height {}, {} trades, {} moria, {} autheader updates.",
|
||||
blockhash.to_hex(),
|
||||
mtp,
|
||||
block_height,
|
||||
total_cauldrons,
|
||||
moria_actions,
|
||||
autheader_updates,
|
||||
blockhash, mtp, block_height, total_cauldrons, moria_actions, autheader_updates,
|
||||
);
|
||||
}
|
||||
Ok(tip_header.block_hash())
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ async fn start_program(
|
|||
let chain = Arc::new(Mutex::new(chain::Chain::new(genesis.header)));
|
||||
|
||||
db::cauldron::pool::initialize_seq(&db.cauldron_r).await;
|
||||
db::cauldron::tokentoken::initialize_seq(&db.cauldron_r).await;
|
||||
|
||||
info!("Loading block headers...");
|
||||
let all_headers = load_all_headers(&db.cauldron_r).await.unwrap();
|
||||
|
|
@ -625,6 +626,13 @@ async fn launch() -> _ {
|
|||
rpc::moria::moria_stats,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
"/tokentoken",
|
||||
routes![
|
||||
rpc::tokentoken::list_active_pools,
|
||||
rpc::tokentoken::list_tokens,
|
||||
],
|
||||
)
|
||||
.mount("/", routes![rpc::health::health])
|
||||
.attach(cors)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
use anyhow::Context;
|
||||
use bitcoin_hashes::hex::{FromHex, ToHex};
|
||||
use bitcoincash::TokenID;
|
||||
use rocket::{get, State};
|
||||
use serde_json::json;
|
||||
|
|
@ -50,13 +49,13 @@ pub async fn token_bcmr(category: Option<&str>, db: &State<DB>) -> CachedApiResu
|
|||
.context("category missing")
|
||||
.map_err(|e| bad_request(ApiErrorCode::MissingCategory, &format!("Error: {e}")))?;
|
||||
|
||||
let token_id = TokenID::from_hex(token_id_hex).map_err(|e| {
|
||||
let token_id = token_id_hex.parse::<TokenID>().map_err(|e| {
|
||||
bad_request(
|
||||
ApiErrorCode::InvalidTokenId,
|
||||
&format!("Invalid token ID: {e}"),
|
||||
)
|
||||
})?;
|
||||
let bcmr = get_token_bcmr(&db.bcmr_r, &token_id.to_hex())
|
||||
let bcmr = get_token_bcmr(&db.bcmr_r, &token_id.to_string())
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
|
||||
|
|
@ -73,18 +72,18 @@ pub async fn token_bcmr_all(category: Option<&str>, db: &State<DB>) -> CachedApi
|
|||
.context("category missing")
|
||||
.map_err(|e| bad_request(ApiErrorCode::MissingCategory, &format!("Error: {e}")))?;
|
||||
|
||||
let token_id = TokenID::from_hex(token_id_hex).map_err(|e| {
|
||||
let token_id = token_id_hex.parse::<TokenID>().map_err(|e| {
|
||||
bad_request(
|
||||
ApiErrorCode::InvalidTokenId,
|
||||
&format!("Invalid token ID: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let onchain_bcmr = get_token_bcmr(&db.bcmr_r, &token_id.to_hex())
|
||||
let onchain_bcmr = get_token_bcmr(&db.bcmr_r, &token_id.to_string())
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
|
||||
let mut bcmr_entries = get_well_known_bcmr(&db.bcmr_r, &token_id.to_hex())
|
||||
let mut bcmr_entries = get_well_known_bcmr(&db.bcmr_r, &token_id.to_string())
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -551,63 +551,63 @@ mod tests {
|
|||
let pkh_zero = PubkeyHash::all_zeros();
|
||||
|
||||
// We'll make 4 trades with distinct times
|
||||
let txid1 = Txid::from_inner([0xf1; 32]);
|
||||
let txid2 = Txid::from_inner([0xf2; 32]);
|
||||
let txid3 = Txid::from_inner([0xf3; 32]);
|
||||
let txid4 = Txid::from_inner([0xf4; 32]);
|
||||
let utxo1 = OutPointHash::from_inner([0xe1; 32]);
|
||||
let utxo2 = OutPointHash::from_inner([0xe2; 32]);
|
||||
let utxo3 = OutPointHash::from_inner([0xe3; 32]);
|
||||
let utxo4 = OutPointHash::from_inner([0xe4; 32]);
|
||||
let txid1 = Txid::from_byte_array([0xf1; 32]);
|
||||
let txid2 = Txid::from_byte_array([0xf2; 32]);
|
||||
let txid3 = Txid::from_byte_array([0xf3; 32]);
|
||||
let txid4 = Txid::from_byte_array([0xf4; 32]);
|
||||
let utxo1 = OutPointHash::from_byte_array([0xe1; 32]);
|
||||
let utxo2 = OutPointHash::from_byte_array([0xe2; 32]);
|
||||
let utxo3 = OutPointHash::from_byte_array([0xe3; 32]);
|
||||
let utxo4 = OutPointHash::from_byte_array([0xe4; 32]);
|
||||
|
||||
let cauldron1 = dummy_cauldron(&txid1, &utxo1, &token_zero, 80_000, 2_000, &pkh_zero);
|
||||
let cauldron2 = dummy_cauldron(&txid2, &utxo2, &token_zero, 120_000, 2_000, &pkh_zero);
|
||||
let cauldron3 = dummy_cauldron(&txid3, &utxo3, &token_zero, 160_000, 2_000, &pkh_zero);
|
||||
let cauldron4 = dummy_cauldron(&txid4, &utxo4, &token_zero, 200_000, 2_000, &pkh_zero);
|
||||
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron1.clone()], &txid1)
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron1.clone()], &txid1)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron2.clone()], &txid2)
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron2.clone()], &txid2)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron3.clone()], &txid3)
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron3.clone()], &txid3)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron4.clone()], &txid4)
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron4.clone()], &txid4)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert tx rows in realistic order: mempool first, then confirmed.
|
||||
let block_zero = BlockHash::all_zeros();
|
||||
insert_mempool_tx(&mut *conn, &txid1, TIME_1).await.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid1, &block_zero, TIME_1 as i64)
|
||||
insert_mempool_tx(&mut conn, &txid1, TIME_1).await.unwrap();
|
||||
insert_block_tx(&mut conn, &txid1, &block_zero, TIME_1 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
insert_mempool_tx(&mut *conn, &txid2, TIME_2).await.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid2, &block_zero, TIME_2 as i64)
|
||||
insert_mempool_tx(&mut conn, &txid2, TIME_2).await.unwrap();
|
||||
insert_block_tx(&mut conn, &txid2, &block_zero, TIME_2 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
insert_mempool_tx(&mut *conn, &txid3, TIME_3).await.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid3, &block_zero, TIME_3 as i64)
|
||||
insert_mempool_tx(&mut conn, &txid3, TIME_3).await.unwrap();
|
||||
insert_block_tx(&mut conn, &txid3, &block_zero, TIME_3 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
insert_mempool_tx(&mut *conn, &txid4, TIME_4).await.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid4, &block_zero, TIME_4 as i64)
|
||||
insert_mempool_tx(&mut conn, &txid4, TIME_4).await.unwrap();
|
||||
insert_block_tx(&mut conn, &txid4, &block_zero, TIME_4 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert pool_history_entry
|
||||
let pool1 = OutPointHash::from_inner([0x0a; 32]);
|
||||
let pool2 = OutPointHash::from_inner([0x0b; 32]);
|
||||
let pool3 = OutPointHash::from_inner([0x0c; 32]);
|
||||
let pool4 = OutPointHash::from_inner([0x0d; 32]);
|
||||
let pool1 = OutPointHash::from_byte_array([0x0a; 32]);
|
||||
let pool2 = OutPointHash::from_byte_array([0x0b; 32]);
|
||||
let pool3 = OutPointHash::from_byte_array([0x0c; 32]);
|
||||
let pool4 = OutPointHash::from_byte_array([0x0d; 32]);
|
||||
|
||||
pool::insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool1,
|
||||
&cauldron1,
|
||||
Some(TIME_1),
|
||||
|
|
@ -618,7 +618,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
pool::insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool2,
|
||||
&cauldron2,
|
||||
Some(TIME_2),
|
||||
|
|
@ -629,7 +629,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
pool::insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool3,
|
||||
&cauldron3,
|
||||
Some(TIME_3),
|
||||
|
|
@ -640,7 +640,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
pool::insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool4,
|
||||
&cauldron4,
|
||||
Some(TIME_4),
|
||||
|
|
@ -652,28 +652,28 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// Insert pools
|
||||
let token1 = TokenID::from_inner([0xda; 32]);
|
||||
let pkh1 = PubkeyHash::from_inner([0xca; 20]);
|
||||
let token1 = TokenID::from_byte_array([0xda; 32]);
|
||||
let pkh1 = PubkeyHash::from_byte_array([0xca; 20]);
|
||||
insert_new_pool(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&dummy_cauldron(&Txid::all_zeros(), &pool1, &token1, 0, 0, &pkh1),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_new_pool(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&dummy_cauldron(&Txid::all_zeros(), &pool2, &token1, 0, 0, &pkh1),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_new_pool(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&dummy_cauldron(&Txid::all_zeros(), &pool3, &token1, 0, 0, &pkh1),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_new_pool(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&dummy_cauldron(&Txid::all_zeros(), &pool4, &token1, 0, 0, &pkh1),
|
||||
)
|
||||
.await
|
||||
|
|
@ -823,15 +823,15 @@ mod tests {
|
|||
let pkh_zero = PubkeyHash::all_zeros();
|
||||
|
||||
// Create a single transaction that will be used for multiple pool trades
|
||||
let txid_multi = Txid::from_inner([0xaa; 32]);
|
||||
let txid_multi = Txid::from_byte_array([0xaa; 32]);
|
||||
let block_zero = BlockHash::all_zeros();
|
||||
|
||||
// Create multiple pool and pool_history_entry records for the same txid
|
||||
let mut pools = Vec::new();
|
||||
let times = [TIME_1, TIME_2];
|
||||
for (i, &time) in times.iter().enumerate() {
|
||||
let utxo = OutPointHash::from_inner([0xe1 + i as u8; 32]);
|
||||
let pool_hash = OutPointHash::from_inner([0x0a + i as u8; 32]);
|
||||
let utxo = OutPointHash::from_byte_array([0xe1 + i as u8; 32]);
|
||||
let pool_hash = OutPointHash::from_byte_array([0x0a + i as u8; 32]);
|
||||
let cauldron = dummy_cauldron(
|
||||
&txid_multi,
|
||||
&utxo,
|
||||
|
|
@ -842,14 +842,14 @@ mod tests {
|
|||
);
|
||||
pools.push(pool_hash);
|
||||
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron.clone()], &txid_multi)
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron.clone()], &txid_multi)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
insert_block_tx(&mut *conn, &txid_multi, &block_zero, time as i64)
|
||||
insert_block_tx(&mut conn, &txid_multi, &block_zero, time as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_mempool_tx(&mut *conn, &txid_multi, time)
|
||||
insert_mempool_tx(&mut conn, &txid_multi, time)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -857,7 +857,7 @@ mod tests {
|
|||
let token_delta = 2_000i64;
|
||||
|
||||
pool::insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool_hash,
|
||||
&cauldron,
|
||||
Some(time),
|
||||
|
|
@ -870,11 +870,11 @@ mod tests {
|
|||
}
|
||||
|
||||
// Insert pools into the pool table
|
||||
let token1 = TokenID::from_inner([0xda; 32]);
|
||||
let pkh1 = PubkeyHash::from_inner([0xca; 20]);
|
||||
let token1 = TokenID::from_byte_array([0xda; 32]);
|
||||
let pkh1 = PubkeyHash::from_byte_array([0xca; 20]);
|
||||
for pool_hash in &pools {
|
||||
insert_new_pool(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&dummy_cauldron(&Txid::all_zeros(), pool_hash, &token1, 0, 0, &pkh1),
|
||||
)
|
||||
.await
|
||||
|
|
@ -941,14 +941,14 @@ mod tests {
|
|||
let block_zero = BlockHash::all_zeros();
|
||||
|
||||
// Two pools (identities)
|
||||
let pool_a = OutPointHash::from_inner([0x1a; 32]);
|
||||
let pool_b = OutPointHash::from_inner([0x1b; 32]);
|
||||
let pool_a = OutPointHash::from_byte_array([0x1a; 32]);
|
||||
let pool_b = OutPointHash::from_byte_array([0x1b; 32]);
|
||||
|
||||
// Register pools in `pool` table
|
||||
let token1 = TokenID::from_inner([0xda; 32]);
|
||||
let pkh1 = PubkeyHash::from_inner([0xca; 20]);
|
||||
let token1 = TokenID::from_byte_array([0xda; 32]);
|
||||
let pkh1 = PubkeyHash::from_byte_array([0xca; 20]);
|
||||
insert_new_pool(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&ParsedContract {
|
||||
pkh: pkh1,
|
||||
is_withdrawn: false,
|
||||
|
|
@ -964,7 +964,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
insert_new_pool(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&ParsedContract {
|
||||
pkh: pkh1,
|
||||
is_withdrawn: false,
|
||||
|
|
@ -986,8 +986,8 @@ mod tests {
|
|||
let t1: u64 = t0 + step;
|
||||
|
||||
// -------- Candle #1 (normal priceable trade) --------
|
||||
let txid_price = Txid::from_inner([0x90; 32]);
|
||||
let utxo_p = OutPointHash::from_inner([0x21; 32]);
|
||||
let txid_price = Txid::from_byte_array([0x90; 32]);
|
||||
let utxo_p = OutPointHash::from_byte_array([0x21; 32]);
|
||||
let cauldron_p = ParsedContract {
|
||||
pkh: pkh_zero,
|
||||
is_withdrawn: false,
|
||||
|
|
@ -999,18 +999,16 @@ mod tests {
|
|||
sats: Some(0),
|
||||
token_amount: Some(0),
|
||||
};
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron_p.clone()], &txid_price)
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron_p.clone()], &txid_price)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid_price, &block_zero, t0 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_mempool_tx(&mut *conn, &txid_price, t0)
|
||||
insert_block_tx(&mut conn, &txid_price, &block_zero, t0 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_mempool_tx(&mut conn, &txid_price, t0).await.unwrap();
|
||||
// sats_delta=+10_000, token_delta=+200 -> price = 10000/200 = 50
|
||||
pool::insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool_a,
|
||||
&cauldron_p,
|
||||
Some(t0),
|
||||
|
|
@ -1022,9 +1020,9 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// -------- Candle #2 (net zero tokens but non-zero volume) --------
|
||||
let txid_net0 = Txid::from_inner([0x91; 32]);
|
||||
let utxo_a1 = OutPointHash::from_inner([0x22; 32]);
|
||||
let utxo_b1 = OutPointHash::from_inner([0x23; 32]);
|
||||
let txid_net0 = Txid::from_byte_array([0x91; 32]);
|
||||
let utxo_a1 = OutPointHash::from_byte_array([0x22; 32]);
|
||||
let utxo_b1 = OutPointHash::from_byte_array([0x23; 32]);
|
||||
|
||||
let ca_a1 = ParsedContract {
|
||||
pkh: pkh_zero,
|
||||
|
|
@ -1043,21 +1041,21 @@ mod tests {
|
|||
..ca_a1
|
||||
};
|
||||
|
||||
insert_utxo_funding(&mut *conn, &vec![ca_a1.clone()], &txid_net0)
|
||||
insert_utxo_funding(&mut conn, &vec![ca_a1.clone()], &txid_net0)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![ca_b1.clone()], &txid_net0)
|
||||
insert_utxo_funding(&mut conn, &vec![ca_b1.clone()], &txid_net0)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
insert_block_tx(&mut *conn, &txid_net0, &block_zero, t1 as i64)
|
||||
insert_block_tx(&mut conn, &txid_net0, &block_zero, t1 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_mempool_tx(&mut *conn, &txid_net0, t1).await.unwrap();
|
||||
insert_mempool_tx(&mut conn, &txid_net0, t1).await.unwrap();
|
||||
|
||||
// Opposite deltas within the same tx
|
||||
pool::insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool_a,
|
||||
&ca_a1,
|
||||
Some(t1),
|
||||
|
|
@ -1068,7 +1066,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
pool::insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool_b,
|
||||
&ca_b1,
|
||||
Some(t1),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ pub mod pool;
|
|||
pub mod price;
|
||||
pub mod response;
|
||||
pub mod tokens;
|
||||
pub mod tokentoken;
|
||||
pub mod tvl;
|
||||
pub mod tx;
|
||||
pub mod user;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ use crate::db::DB;
|
|||
use crate::rpc::err::{bad_request, db_error, not_found, ApiErrorCode, CachedApiResult};
|
||||
use crate::rpc::response::{cached_ok, CACHE_AGGREGATE};
|
||||
use crate::timeutil::time_now;
|
||||
use bitcoin_hashes::hex::FromHex;
|
||||
use bitcoincash::TokenID;
|
||||
use rocket::{get, State};
|
||||
use serde_json::{json, Value};
|
||||
|
|
@ -60,7 +59,7 @@ pub async fn oracle_get_closest(
|
|||
let current_timestamp = timestamp.unwrap_or_else(time_now);
|
||||
let token_id = token_id
|
||||
.map(|t| {
|
||||
TokenID::from_hex(&t).map_err(|e| {
|
||||
t.parse::<TokenID>().map_err(|e| {
|
||||
bad_request(
|
||||
ApiErrorCode::InvalidTokenId,
|
||||
&format!("Invalid token ID: {e}"),
|
||||
|
|
@ -91,7 +90,7 @@ pub async fn oracle_get_range(
|
|||
let start_timestamp = start.unwrap_or_else(|| end_timestamp - 86400);
|
||||
let token_id = token_id
|
||||
.map(|t| {
|
||||
TokenID::from_hex(&t).map_err(|e| {
|
||||
t.parse::<TokenID>().map_err(|e| {
|
||||
bad_request(
|
||||
ApiErrorCode::InvalidTokenId,
|
||||
&format!("Invalid token ID: {e}"),
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ use crate::{
|
|||
timeutil::time_now,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use bitcoin_hashes::hex::{FromHex, ToHex};
|
||||
use bitcoincash::Txid;
|
||||
use riftenlabs_defi::chainutil::compute_outpoint_hash;
|
||||
use rocket::{get, State};
|
||||
|
|
@ -166,7 +165,7 @@ pub async fn pool_history(
|
|||
) -> CachedApiResult<Value> {
|
||||
let start = start.unwrap_or(time_now() as u64 - (30 * 3600 * 24) /* 30 days ago */);
|
||||
|
||||
let pool_id = PoolID::from_hex(pool_id).map_err(|e| {
|
||||
let pool_id = pool_id.parse::<PoolID>().map_err(|e| {
|
||||
bad_request(
|
||||
ApiErrorCode::InvalidPoolId,
|
||||
&format!("Invalid pool ID: {e}"),
|
||||
|
|
@ -211,7 +210,8 @@ pub async fn pool_history(
|
|||
/// ```
|
||||
#[get("/pool/id_from_utxo?<txid>&<n>")]
|
||||
pub async fn pool_id_from_utxo(txid: &str, n: u32, conn: &State<DB>) -> CachedApiResult<Value> {
|
||||
let txid = Txid::from_hex(txid)
|
||||
let txid = txid
|
||||
.parse::<Txid>()
|
||||
.map_err(|e| bad_request(ApiErrorCode::InvalidTxid, &format!("Invalid txid: {e}")))?;
|
||||
|
||||
let utxo_hash = compute_outpoint_hash(&txid, n);
|
||||
|
|
@ -229,11 +229,7 @@ pub async fn pool_id_from_utxo(txid: &str, n: u32, conn: &State<DB>) -> CachedAp
|
|||
)),
|
||||
None => Err(not_found(
|
||||
ApiErrorCode::PoolNotFound,
|
||||
&format!(
|
||||
"No pool found for UTXO: txid={}, input_pos={}",
|
||||
txid.to_hex(),
|
||||
n
|
||||
),
|
||||
&format!("No pool found for UTXO: txid={}, input_pos={}", txid, n),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
223
src/rpc/price.rs
223
src/rpc/price.rs
|
|
@ -4,7 +4,6 @@
|
|||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use bitcoin_hashes::hex::{FromHex, ToHex};
|
||||
use bitcoincash::TokenID;
|
||||
use rocket::{get, State};
|
||||
use rust_decimal::prelude::*;
|
||||
|
|
@ -253,14 +252,14 @@ pub async fn price_at_or_before_2(
|
|||
&mut visitor,
|
||||
PoolFilters::new()
|
||||
.lte(timestamp as u64)
|
||||
.token_id(&token_id.to_hex()),
|
||||
.token_id(&token_id.to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !visitor.has_data() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"No price data found for token {} at timestamp {}",
|
||||
token_id.to_hex(),
|
||||
token_id.to_string(),
|
||||
timestamp
|
||||
));
|
||||
}
|
||||
|
|
@ -314,7 +313,7 @@ pub async fn price_at(token: &str, timestamp: &str, conn: &State<DB>) -> CachedA
|
|||
"Timestamp is in the future",
|
||||
));
|
||||
}
|
||||
let token = TokenID::from_hex(token).map_err(|e| {
|
||||
let token = token.parse::<TokenID>().map_err(|e| {
|
||||
bad_request(
|
||||
ApiErrorCode::InvalidTokenId,
|
||||
&format!("Invalid token ID: {e}"),
|
||||
|
|
@ -516,12 +515,12 @@ mod tests {
|
|||
let token_zero = TokenID::all_zeros();
|
||||
let pkh_zero = PubkeyHash::all_zeros();
|
||||
|
||||
let txid1 = Txid::from_inner([0xf0; 32]);
|
||||
let txid2 = Txid::from_inner([0xf1; 32]);
|
||||
let txid3 = Txid::from_inner([0xf2; 32]);
|
||||
let utxo1 = OutPointHash::from_inner([0xe0; 32]); // pool 1
|
||||
let utxo2 = OutPointHash::from_inner([0xe1; 32]); // pool 2
|
||||
let utxo3 = OutPointHash::from_inner([0xe2; 32]); // pool 3
|
||||
let txid1 = Txid::from_byte_array([0xf0; 32]);
|
||||
let txid2 = Txid::from_byte_array([0xf1; 32]);
|
||||
let txid3 = Txid::from_byte_array([0xf2; 32]);
|
||||
let utxo1 = OutPointHash::from_byte_array([0xe0; 32]); // pool 1
|
||||
let utxo2 = OutPointHash::from_byte_array([0xe1; 32]); // pool 2
|
||||
let utxo3 = OutPointHash::from_byte_array([0xe2; 32]); // pool 3
|
||||
|
||||
// Insert mock data into utxo_funding
|
||||
let cauldron1 = dummy_cauldron(
|
||||
|
|
@ -551,49 +550,49 @@ mod tests {
|
|||
&pkh_zero,
|
||||
&OutPointHash::all_zeros(),
|
||||
);
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron1.clone()], &txid1)
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron1.clone()], &txid1)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron2.clone()], &txid2)
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron2.clone()], &txid2)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron3.clone()], &txid3)
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron3.clone()], &txid3)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert mock data into tx table
|
||||
let block_zero = BlockHash::all_zeros();
|
||||
insert_block_tx(&mut *conn, &txid1, &block_zero, TIME_1 as i64)
|
||||
insert_block_tx(&mut conn, &txid1, &block_zero, TIME_1 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_mempool_tx(&mut *conn, &txid1, TIME_1).await.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid2, &block_zero, TIME_2 as i64)
|
||||
insert_mempool_tx(&mut conn, &txid1, TIME_1).await.unwrap();
|
||||
insert_block_tx(&mut conn, &txid2, &block_zero, TIME_2 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_mempool_tx(&mut *conn, &txid2, TIME_2).await.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid3, &block_zero, TIME_3 as i64)
|
||||
insert_mempool_tx(&mut conn, &txid2, TIME_2).await.unwrap();
|
||||
insert_block_tx(&mut conn, &txid3, &block_zero, TIME_3 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_mempool_tx(&mut *conn, &txid3, TIME_3).await.unwrap();
|
||||
insert_mempool_tx(&mut conn, &txid3, TIME_3).await.unwrap();
|
||||
|
||||
// Insert mock data into `pool_history_entry`
|
||||
update_pool_history(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
vec![cauldron1.clone()],
|
||||
Some(TIME_1),
|
||||
Some(TIME_1),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
update_pool_history(&mut *conn, vec![cauldron2], Some(TIME_2), Some(TIME_2))
|
||||
update_pool_history(&mut conn, vec![cauldron2], Some(TIME_2), Some(TIME_2))
|
||||
.await
|
||||
.unwrap();
|
||||
update_pool_history(&mut *conn, vec![cauldron3], Some(TIME_3), Some(TIME_3))
|
||||
update_pool_history(&mut conn, vec![cauldron3], Some(TIME_3), Some(TIME_3))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let txid1_newer = Txid::from_inner([0xf3; 32]);
|
||||
let utxo1_newer = OutPointHash::from_inner([0xe3; 32]);
|
||||
let txid1_newer = Txid::from_byte_array([0xf3; 32]);
|
||||
let utxo1_newer = OutPointHash::from_byte_array([0xe3; 32]);
|
||||
|
||||
// Insert newer entry for pool1
|
||||
let cauldron1_newer = dummy_cauldron(
|
||||
|
|
@ -605,11 +604,11 @@ mod tests {
|
|||
&pkh_zero,
|
||||
&utxo1,
|
||||
);
|
||||
insert_utxo_spending(&mut *conn, &vec![cauldron1], &txid1_newer, true)
|
||||
insert_utxo_spending(&mut conn, &vec![cauldron1], &txid1_newer, true)
|
||||
.await
|
||||
.unwrap();
|
||||
update_pool_history(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
vec![cauldron1_newer],
|
||||
Some(1727963500),
|
||||
Some(1727963500),
|
||||
|
|
@ -618,10 +617,10 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// Insert an inactive pool
|
||||
let inactive_pool = OutPointHash::from_inner([0xaa; 32]);
|
||||
let inactive_token = TokenID::from_inner([0xac; 32]);
|
||||
let inactive_pkh = PubkeyHash::from_inner([0xac; 20]);
|
||||
let inactive_txid = Txid::from_inner([0xad; 32]);
|
||||
let inactive_pool = OutPointHash::from_byte_array([0xaa; 32]);
|
||||
let inactive_token = TokenID::from_byte_array([0xac; 32]);
|
||||
let inactive_pkh = PubkeyHash::from_byte_array([0xac; 20]);
|
||||
let inactive_txid = Txid::from_byte_array([0xad; 32]);
|
||||
let inactive_cauldron = dummy_cauldron(
|
||||
&inactive_txid,
|
||||
&inactive_pool,
|
||||
|
|
@ -643,14 +642,14 @@ mod tests {
|
|||
token_amount: None,
|
||||
};
|
||||
|
||||
insert_new_pool(&mut *conn, &inactive_cauldron)
|
||||
insert_new_pool(&mut conn, &inactive_cauldron)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![inactive_cauldron.clone()], &inactive_txid)
|
||||
insert_utxo_funding(&mut conn, &vec![inactive_cauldron.clone()], &inactive_txid)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&inactive_pool,
|
||||
&inactive_cauldron,
|
||||
Some(1727963350),
|
||||
|
|
@ -660,7 +659,7 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
flag_as_withdrawn(&mut *conn, &inactive_pool, &inactive_cauldron_withdraw)
|
||||
flag_as_withdrawn(&mut conn, &inactive_pool, &inactive_cauldron_withdraw)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
@ -677,7 +676,7 @@ mod tests {
|
|||
.await
|
||||
.expect("valid rocket instance");
|
||||
|
||||
let token_id = TokenID::all_zeros().to_hex();
|
||||
let token_id = TokenID::all_zeros().to_string();
|
||||
|
||||
// Test 1: Querying at a timestamp that exactly matches test_txid1
|
||||
let timestamp = TIME_1;
|
||||
|
|
@ -752,7 +751,7 @@ mod tests {
|
|||
.await
|
||||
.expect("valid rocket instance");
|
||||
|
||||
let token_id = TokenID::all_zeros().to_hex();
|
||||
let token_id = TokenID::all_zeros().to_string();
|
||||
|
||||
// Test: Query at a newer timestamp (1727963500) for pool1 and ensure it only accounts for the newer entry
|
||||
let timestamp = 1727963500; // Newer timestamp
|
||||
|
|
@ -796,13 +795,13 @@ mod tests {
|
|||
.await
|
||||
.expect("valid rocket instance");
|
||||
|
||||
let token_id = TokenID::all_zeros().to_hex();
|
||||
let token_id = TokenID::all_zeros().to_string();
|
||||
|
||||
// Insert additional entries with the same timestamp for an existing pool (e.g., pool1)
|
||||
let mut conn = mock_db.cauldron_w.acquire().await.unwrap();
|
||||
|
||||
// These entries should have the same timestamp as previous mockdata and be counted in the price calculation
|
||||
let pool1 = OutPointHash::from_inner([0xe0; 32]);
|
||||
let pool1 = OutPointHash::from_byte_array([0xe0; 32]);
|
||||
let txid = Txid::hash("txid_extra".as_bytes());
|
||||
|
||||
let cauldron = dummy_cauldron(
|
||||
|
|
@ -814,7 +813,7 @@ mod tests {
|
|||
&PubkeyHash::all_zeros(),
|
||||
&pool1,
|
||||
);
|
||||
update_pool_history(&mut *conn, vec![cauldron], Some(TIME_1), Some(TIME_1))
|
||||
update_pool_history(&mut conn, vec![cauldron], Some(TIME_1), Some(TIME_1))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -882,7 +881,7 @@ mod tests {
|
|||
.await
|
||||
.expect("valid rocket instance");
|
||||
|
||||
let token_id = TokenID::all_zeros().to_hex();
|
||||
let token_id = TokenID::all_zeros().to_string();
|
||||
let future_timestamp = (time_now() + 100000).to_string();
|
||||
|
||||
let response = client
|
||||
|
|
@ -913,7 +912,7 @@ mod tests {
|
|||
.expect("valid rocket instance");
|
||||
|
||||
// Test the `/price/<token>/at/<timestamp>` endpoint with an invalid timestamp
|
||||
let token_id = TokenID::all_zeros().to_hex();
|
||||
let token_id = TokenID::all_zeros().to_string();
|
||||
let invalid_timestamp = "ASDASD:";
|
||||
|
||||
let response = client
|
||||
|
|
@ -947,7 +946,7 @@ mod tests {
|
|||
let pkh = PubkeyHash::all_zeros();
|
||||
let block = BlockHash::all_zeros();
|
||||
// Fake creation txid — not referenced by poolvisitor; FK disabled so no tx row needed
|
||||
let fake_txid = Txid::from_inner(*withdraw_txid.as_inner()); // unique: reuse withdraw bytes
|
||||
let fake_txid = Txid::from_byte_array(withdraw_txid.to_byte_array()); // unique: reuse withdraw bytes
|
||||
let cauldron = dummy_cauldron(
|
||||
&fake_txid,
|
||||
&pool_utxo,
|
||||
|
|
@ -1002,9 +1001,9 @@ mod tests {
|
|||
let mut conn = pool.acquire().await.unwrap();
|
||||
let pkh = PubkeyHash::all_zeros();
|
||||
let block = BlockHash::all_zeros();
|
||||
let creation_txid = Txid::from_inner([0xa0; 32]);
|
||||
let creation_utxo = OutPointHash::from_inner([0xb0; 32]);
|
||||
let withdraw_txid = Txid::from_inner([0xa1; 32]);
|
||||
let creation_txid = Txid::from_byte_array([0xa0; 32]);
|
||||
let creation_utxo = OutPointHash::from_byte_array([0xb0; 32]);
|
||||
let withdraw_txid = Txid::from_byte_array([0xa1; 32]);
|
||||
|
||||
let cauldron = dummy_cauldron(
|
||||
&creation_txid,
|
||||
|
|
@ -1027,29 +1026,29 @@ mod tests {
|
|||
token_amount: None,
|
||||
};
|
||||
|
||||
insert_new_pool(&mut *conn, &cauldron).await.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron.clone()], &creation_txid)
|
||||
insert_new_pool(&mut conn, &cauldron).await.unwrap();
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron.clone()], &creation_txid)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_block_tx(&mut *conn, &creation_txid, &block, TIME_1 as i64)
|
||||
insert_block_tx(&mut conn, &creation_txid, &block, TIME_1 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
update_pool_history(&mut *conn, vec![cauldron], Some(TIME_1), Some(TIME_1))
|
||||
update_pool_history(&mut conn, vec![cauldron], Some(TIME_1), Some(TIME_1))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
insert_block_tx(&mut *conn, &withdraw_txid, &block, TIME_3 as i64)
|
||||
insert_block_tx(&mut conn, &withdraw_txid, &block, TIME_3 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_utxo_spending(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&vec![cauldron_withdraw.clone()],
|
||||
&withdraw_txid,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
flag_as_withdrawn(&mut *conn, &creation_utxo, &cauldron_withdraw)
|
||||
flag_as_withdrawn(&mut conn, &creation_utxo, &cauldron_withdraw)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
@ -1061,11 +1060,11 @@ mod tests {
|
|||
/// the pool's price because the pool was active at that moment.
|
||||
#[rocket::async_test]
|
||||
async fn test_price_at_includes_withdrawn_pool_when_active_at_query_time() {
|
||||
let token = TokenID::from_inner([0x11; 32]);
|
||||
let token_hex = token.to_hex();
|
||||
let token = TokenID::from_byte_array([0x11; 32]);
|
||||
let token_hex = token.to_string();
|
||||
|
||||
let mock_db = mock_db_pool(|pool: sqlx::SqlitePool| async move {
|
||||
setup_single_withdrawn_pool(&pool, &TokenID::from_inner([0x11; 32])).await;
|
||||
setup_single_withdrawn_pool(&pool, &TokenID::from_byte_array([0x11; 32])).await;
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
@ -1096,11 +1095,11 @@ mod tests {
|
|||
/// withdrawal_ts >= withdraw_time_filter → TIME_3 >= TIME_3 → true, so pool is included.
|
||||
#[rocket::async_test]
|
||||
async fn test_price_at_includes_withdrawn_pool_at_exact_withdrawal_timestamp() {
|
||||
let token = TokenID::from_inner([0x12; 32]);
|
||||
let token_hex = token.to_hex();
|
||||
let token = TokenID::from_byte_array([0x12; 32]);
|
||||
let token_hex = token.to_string();
|
||||
|
||||
let mock_db = mock_db_pool(|pool: sqlx::SqlitePool| async move {
|
||||
setup_single_withdrawn_pool(&pool, &TokenID::from_inner([0x12; 32])).await;
|
||||
setup_single_withdrawn_pool(&pool, &TokenID::from_byte_array([0x12; 32])).await;
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
@ -1128,11 +1127,11 @@ mod tests {
|
|||
/// Query at TIME_4 (after withdrawal TIME_3): pool must be excluded → 404.
|
||||
#[rocket::async_test]
|
||||
async fn test_price_at_excludes_withdrawn_pool_after_withdrawal() {
|
||||
let token = TokenID::from_inner([0x13; 32]);
|
||||
let token_hex = token.to_hex();
|
||||
let token = TokenID::from_byte_array([0x13; 32]);
|
||||
let token_hex = token.to_string();
|
||||
|
||||
let mock_db = mock_db_pool(|pool: sqlx::SqlitePool| async move {
|
||||
setup_single_withdrawn_pool(&pool, &TokenID::from_inner([0x13; 32])).await;
|
||||
setup_single_withdrawn_pool(&pool, &TokenID::from_byte_array([0x13; 32])).await;
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
@ -1164,8 +1163,8 @@ mod tests {
|
|||
/// Query at TIME_4 → only A contributes: 100_000/2_000 = 50.0
|
||||
#[rocket::async_test]
|
||||
async fn test_price_at_mixed_active_and_withdrawn_pools() {
|
||||
let token = TokenID::from_inner([0x14; 32]);
|
||||
let token_hex = token.to_hex();
|
||||
let token = TokenID::from_byte_array([0x14; 32]);
|
||||
let token_hex = token.to_string();
|
||||
|
||||
let mock_db = mock_db_pool(|pool: sqlx::SqlitePool| async move {
|
||||
utxo_funding::create_table(&pool).await;
|
||||
|
|
@ -1175,13 +1174,13 @@ mod tests {
|
|||
dummy_init_seq();
|
||||
|
||||
let mut conn = pool.acquire().await.unwrap();
|
||||
let token = TokenID::from_inner([0x14; 32]);
|
||||
let token = TokenID::from_byte_array([0x14; 32]);
|
||||
let pkh = PubkeyHash::all_zeros();
|
||||
let block = BlockHash::all_zeros();
|
||||
|
||||
// Pool A: always active
|
||||
let txid_a = Txid::from_inner([0xc0; 32]);
|
||||
let utxo_a = OutPointHash::from_inner([0xd0; 32]);
|
||||
let txid_a = Txid::from_byte_array([0xc0; 32]);
|
||||
let utxo_a = OutPointHash::from_byte_array([0xd0; 32]);
|
||||
let cauldron_a = dummy_cauldron(
|
||||
&txid_a,
|
||||
&utxo_a,
|
||||
|
|
@ -1191,21 +1190,21 @@ mod tests {
|
|||
&pkh,
|
||||
&OutPointHash::all_zeros(),
|
||||
);
|
||||
insert_new_pool(&mut *conn, &cauldron_a).await.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron_a.clone()], &txid_a)
|
||||
insert_new_pool(&mut conn, &cauldron_a).await.unwrap();
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron_a.clone()], &txid_a)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid_a, &block, TIME_1 as i64)
|
||||
insert_block_tx(&mut conn, &txid_a, &block, TIME_1 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
update_pool_history(&mut *conn, vec![cauldron_a], Some(TIME_1), Some(TIME_1))
|
||||
update_pool_history(&mut conn, vec![cauldron_a], Some(TIME_1), Some(TIME_1))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Pool B: active at TIME_1, withdrawn at TIME_3
|
||||
let txid_b = Txid::from_inner([0xc1; 32]);
|
||||
let utxo_b = OutPointHash::from_inner([0xd1; 32]);
|
||||
let txid_b_withdraw = Txid::from_inner([0xc2; 32]);
|
||||
let txid_b = Txid::from_byte_array([0xc1; 32]);
|
||||
let utxo_b = OutPointHash::from_byte_array([0xd1; 32]);
|
||||
let txid_b_withdraw = Txid::from_byte_array([0xc2; 32]);
|
||||
let cauldron_b = dummy_cauldron(
|
||||
&txid_b,
|
||||
&utxo_b,
|
||||
|
|
@ -1226,28 +1225,28 @@ mod tests {
|
|||
sats: None,
|
||||
token_amount: None,
|
||||
};
|
||||
insert_new_pool(&mut *conn, &cauldron_b).await.unwrap();
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron_b.clone()], &txid_b)
|
||||
insert_new_pool(&mut conn, &cauldron_b).await.unwrap();
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron_b.clone()], &txid_b)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid_b, &block, TIME_1 as i64)
|
||||
insert_block_tx(&mut conn, &txid_b, &block, TIME_1 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
update_pool_history(&mut *conn, vec![cauldron_b], Some(TIME_1), Some(TIME_1))
|
||||
update_pool_history(&mut conn, vec![cauldron_b], Some(TIME_1), Some(TIME_1))
|
||||
.await
|
||||
.unwrap();
|
||||
insert_block_tx(&mut *conn, &txid_b_withdraw, &block, TIME_3 as i64)
|
||||
insert_block_tx(&mut conn, &txid_b_withdraw, &block, TIME_3 as i64)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_utxo_spending(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&vec![cauldron_b_withdraw.clone()],
|
||||
&txid_b_withdraw,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
flag_as_withdrawn(&mut *conn, &utxo_b, &cauldron_b_withdraw)
|
||||
flag_as_withdrawn(&mut conn, &utxo_b, &cauldron_b_withdraw)
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
|
|
@ -1293,11 +1292,11 @@ mod tests {
|
|||
/// price; query after returns 404.
|
||||
#[rocket::async_test]
|
||||
async fn test_price_at_all_pools_withdrawn() {
|
||||
let token = TokenID::from_inner([0x15; 32]);
|
||||
let token_hex = token.to_hex();
|
||||
let token = TokenID::from_byte_array([0x15; 32]);
|
||||
let token_hex = token.to_string();
|
||||
|
||||
let mock_db = mock_db_pool(|pool: sqlx::SqlitePool| async move {
|
||||
setup_single_withdrawn_pool(&pool, &TokenID::from_inner([0x15; 32])).await;
|
||||
setup_single_withdrawn_pool(&pool, &TokenID::from_byte_array([0x15; 32])).await;
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
@ -1362,11 +1361,11 @@ mod tests {
|
|||
&PubkeyHash::all_zeros(),
|
||||
&OutPointHash::all_zeros(),
|
||||
);
|
||||
insert_utxo_funding(&mut *conn, &vec![cauldron.clone()], &txid)
|
||||
insert_utxo_funding(&mut conn, &vec![cauldron.clone()], &txid)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&pool,
|
||||
&cauldron,
|
||||
Some(TIME_1),
|
||||
|
|
@ -1379,7 +1378,7 @@ mod tests {
|
|||
|
||||
// Test: Querying the price with high tokens and low sats
|
||||
let response = client
|
||||
.get(format!("/cauldron/price/{}/at/{}", token.to_hex(), TIME_1))
|
||||
.get(format!("/cauldron/price/{}/at/{}", token, TIME_1))
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
|
|
@ -1430,8 +1429,8 @@ mod tests {
|
|||
#[rocket::async_test]
|
||||
async fn test_price_at_multi_generation_all_eventually_withdrawn() {
|
||||
const BASE: u64 = 1_756_684_800; // 2025-09-01
|
||||
let token = TokenID::from_inner([0x30; 32]);
|
||||
let token_hex = token.to_hex();
|
||||
let token = TokenID::from_byte_array([0x30; 32]);
|
||||
let token_hex = token.to_string();
|
||||
|
||||
let mock_db = mock_db_pool(|pool: sqlx::SqlitePool| async move {
|
||||
utxo_spending::create_table(&pool).await;
|
||||
|
|
@ -1440,41 +1439,41 @@ mod tests {
|
|||
dummy_init_seq();
|
||||
|
||||
let mut conn = pool.acquire().await.unwrap();
|
||||
let token = TokenID::from_inner([0x30; 32]);
|
||||
let token = TokenID::from_byte_array([0x30; 32]);
|
||||
|
||||
// Pool A: T1 → T3
|
||||
insert_pool_and_withdraw(
|
||||
&mut *conn,
|
||||
OutPointHash::from_inner([0xa1; 32]),
|
||||
&mut conn,
|
||||
OutPointHash::from_byte_array([0xa1; 32]),
|
||||
token,
|
||||
120_000,
|
||||
2_000,
|
||||
BASE,
|
||||
Txid::from_inner([0xb1; 32]),
|
||||
Txid::from_byte_array([0xb1; 32]),
|
||||
BASE + 200,
|
||||
)
|
||||
.await;
|
||||
// Pool B: T2 → T4
|
||||
insert_pool_and_withdraw(
|
||||
&mut *conn,
|
||||
OutPointHash::from_inner([0xa2; 32]),
|
||||
&mut conn,
|
||||
OutPointHash::from_byte_array([0xa2; 32]),
|
||||
token,
|
||||
80_000,
|
||||
2_000,
|
||||
BASE + 100,
|
||||
Txid::from_inner([0xb2; 32]),
|
||||
Txid::from_byte_array([0xb2; 32]),
|
||||
BASE + 300,
|
||||
)
|
||||
.await;
|
||||
// Pool C: T5 → T6
|
||||
insert_pool_and_withdraw(
|
||||
&mut *conn,
|
||||
OutPointHash::from_inner([0xa3; 32]),
|
||||
&mut conn,
|
||||
OutPointHash::from_byte_array([0xa3; 32]),
|
||||
token,
|
||||
90_000,
|
||||
3_000,
|
||||
BASE + 400,
|
||||
Txid::from_inner([0xb3; 32]),
|
||||
Txid::from_byte_array([0xb3; 32]),
|
||||
BASE + 500,
|
||||
)
|
||||
.await;
|
||||
|
|
@ -1554,8 +1553,8 @@ mod tests {
|
|||
#[rocket::async_test]
|
||||
async fn test_price_at_historical_price_reflects_pools_active_at_query_time() {
|
||||
const BASE: u64 = 1_756_684_800;
|
||||
let token = TokenID::from_inner([0x31; 32]);
|
||||
let token_hex = token.to_hex();
|
||||
let token = TokenID::from_byte_array([0x31; 32]);
|
||||
let token_hex = token.to_string();
|
||||
|
||||
let mock_db = mock_db_pool(|pool: sqlx::SqlitePool| async move {
|
||||
utxo_spending::create_table(&pool).await;
|
||||
|
|
@ -1563,13 +1562,13 @@ mod tests {
|
|||
pool::create_table(&pool).await;
|
||||
dummy_init_seq();
|
||||
|
||||
let token = TokenID::from_inner([0x31; 32]);
|
||||
let token = TokenID::from_byte_array([0x31; 32]);
|
||||
let pkh = PubkeyHash::all_zeros();
|
||||
let mut conn = pool.acquire().await.unwrap();
|
||||
|
||||
// Pool A: always active — no withdrawal
|
||||
let utxo_a = OutPointHash::from_inner([0xc1; 32]);
|
||||
let txid_a = Txid::from_inner([0xd1; 32]);
|
||||
let utxo_a = OutPointHash::from_byte_array([0xc1; 32]);
|
||||
let txid_a = Txid::from_byte_array([0xd1; 32]);
|
||||
let ca = dummy_cauldron(
|
||||
&txid_a,
|
||||
&utxo_a,
|
||||
|
|
@ -1579,27 +1578,27 @@ mod tests {
|
|||
&pkh,
|
||||
&OutPointHash::all_zeros(),
|
||||
);
|
||||
insert_new_pool(&mut *conn, &ca).await.unwrap();
|
||||
insert_pool_history_entry(&mut *conn, &utxo_a, &ca, Some(BASE), Some(BASE), 0, 0)
|
||||
insert_new_pool(&mut conn, &ca).await.unwrap();
|
||||
insert_pool_history_entry(&mut conn, &utxo_a, &ca, Some(BASE), Some(BASE), 0, 0)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Pool B: active at T1, withdrawn at T2
|
||||
insert_pool_and_withdraw(
|
||||
&mut *conn,
|
||||
OutPointHash::from_inner([0xc2; 32]),
|
||||
&mut conn,
|
||||
OutPointHash::from_byte_array([0xc2; 32]),
|
||||
token,
|
||||
60_000,
|
||||
3_000,
|
||||
BASE,
|
||||
Txid::from_inner([0xd2; 32]),
|
||||
Txid::from_byte_array([0xd2; 32]),
|
||||
BASE + 200,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Pool C: created at T2, never withdrawn — no withdrawal
|
||||
let utxo_c = OutPointHash::from_inner([0xc3; 32]);
|
||||
let txid_c = Txid::from_inner([0xd3; 32]);
|
||||
let utxo_c = OutPointHash::from_byte_array([0xc3; 32]);
|
||||
let txid_c = Txid::from_byte_array([0xd3; 32]);
|
||||
let cc = dummy_cauldron(
|
||||
&txid_c,
|
||||
&utxo_c,
|
||||
|
|
@ -1609,9 +1608,9 @@ mod tests {
|
|||
&pkh,
|
||||
&OutPointHash::all_zeros(),
|
||||
);
|
||||
insert_new_pool(&mut *conn, &cc).await.unwrap();
|
||||
insert_new_pool(&mut conn, &cc).await.unwrap();
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo_c,
|
||||
&cc,
|
||||
Some(BASE + 200),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
// 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
|
||||
|
||||
use bitcoin_hashes::hex::{FromHex, ToHex};
|
||||
use bitcoincash::TokenID;
|
||||
use rocket::{get, State};
|
||||
use serde_json::json;
|
||||
|
|
@ -292,13 +291,13 @@ pub async fn list_cached_by_ids(
|
|||
/// ```
|
||||
#[get("/token/<token>/first_pool")]
|
||||
pub async fn first_pool_creation(token: &str, dbp: &State<DB>) -> CachedApiResult<Value> {
|
||||
let token = TokenID::from_hex(token).map_err(|e| {
|
||||
let token = token.parse::<TokenID>().map_err(|e| {
|
||||
bad_request(
|
||||
ApiErrorCode::InvalidTokenId,
|
||||
&format!("Invalid token id: {e}"),
|
||||
)
|
||||
})?;
|
||||
let token_hex = token.to_hex();
|
||||
let token_hex = token.to_string();
|
||||
|
||||
match db_first_pool_creation_row(&dbp.cauldron_r, &token_hex).await {
|
||||
Ok(Some((creation_utxo, txid, timestamp, block_height))) => {
|
||||
|
|
|
|||
69
src/rpc/tokentoken.rs
Normal file
69
src/rpc/tokentoken.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (C) 2025-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
|
||||
|
||||
//! RPC endpoints for native token-A <-> token-B (`TokenToken`) pools.
|
||||
|
||||
use crate::{
|
||||
db::{
|
||||
cauldron::tokentoken::{db_active_pools_for_pair, db_pair_tokens},
|
||||
DB,
|
||||
},
|
||||
rpc::err::{bad_request, db_error, ApiErrorCode, CachedApiResult},
|
||||
rpc::response::{cached_ok, CACHE_AGGREGATE, CACHE_NONE},
|
||||
};
|
||||
use bitcoincash::TokenID;
|
||||
use rocket::{get, State};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// List active TokenToken pools that trade the given token pair.
|
||||
///
|
||||
/// The query is order-insensitive: a pool containing both tokens is returned
|
||||
/// regardless of which token is held in the main vs storage UTXO. Each pool's
|
||||
/// response labels which token is `a` (main) vs `b` (storage) so the caller can
|
||||
/// build the correct swap direction.
|
||||
///
|
||||
/// Both `token_a` and `token_b` are required (token ids in display hex).
|
||||
#[get("/pool/active?<token_a>&<token_b>")]
|
||||
pub async fn list_active_pools(
|
||||
token_a: Option<&str>,
|
||||
token_b: Option<&str>,
|
||||
conn: &State<DB>,
|
||||
) -> CachedApiResult<Value> {
|
||||
let (token_a, token_b) = match (token_a, token_b) {
|
||||
(Some(a), Some(b)) => (a, b),
|
||||
_ => {
|
||||
return Err(bad_request(
|
||||
ApiErrorCode::MissingParameters,
|
||||
"Provide token_a and token_b",
|
||||
))
|
||||
}
|
||||
};
|
||||
let token_a = token_a.parse::<TokenID>().map_err(|e| {
|
||||
bad_request(
|
||||
ApiErrorCode::InvalidTokenId,
|
||||
&format!("Invalid token_a: {e}"),
|
||||
)
|
||||
})?;
|
||||
let token_b = token_b.parse::<TokenID>().map_err(|e| {
|
||||
bad_request(
|
||||
ApiErrorCode::InvalidTokenId,
|
||||
&format!("Invalid token_b: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let active = db_active_pools_for_pair(&conn.cauldron_r, &token_a, &token_b)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
|
||||
Ok(cached_ok(json!({ "active": active }), CACHE_NONE))
|
||||
}
|
||||
|
||||
/// List the token ids that appear in at least one active TokenToken pool.
|
||||
/// Drives the frontend token selector and the "no route" decision.
|
||||
#[get("/tokens")]
|
||||
pub async fn list_tokens(conn: &State<DB>) -> CachedApiResult<Value> {
|
||||
let tokens = db_pair_tokens(&conn.cauldron_r).await.map_err(db_error)?;
|
||||
Ok(cached_ok(json!({ "tokens": tokens }), CACHE_AGGREGATE))
|
||||
}
|
||||
|
|
@ -295,21 +295,21 @@ pub mod tests {
|
|||
let block_zero = BlockHash::all_zeros();
|
||||
|
||||
// Create test data with different timestamps
|
||||
let txid1 = Txid::from_inner([0xf1; 32]);
|
||||
let txid2 = Txid::from_inner([0xf2; 32]);
|
||||
let txid3 = Txid::from_inner([0xf3; 32]);
|
||||
let utxo1 = OutPointHash::from_inner([0xe1; 32]);
|
||||
let utxo2 = OutPointHash::from_inner([0xe2; 32]);
|
||||
let utxo3 = OutPointHash::from_inner([0xe3; 32]);
|
||||
let txid1 = Txid::from_byte_array([0xf1; 32]);
|
||||
let txid2 = Txid::from_byte_array([0xf2; 32]);
|
||||
let txid3 = Txid::from_byte_array([0xf3; 32]);
|
||||
let utxo1 = OutPointHash::from_byte_array([0xe1; 32]);
|
||||
let utxo2 = OutPointHash::from_byte_array([0xe2; 32]);
|
||||
let utxo3 = OutPointHash::from_byte_array([0xe3; 32]);
|
||||
|
||||
// Insert transactions with different timestamps
|
||||
tx::insert_block_tx(&mut *conn, &txid1, &block_zero, 1000)
|
||||
tx::insert_block_tx(&mut conn, &txid1, &block_zero, 1000)
|
||||
.await
|
||||
.unwrap();
|
||||
tx::insert_block_tx(&mut *conn, &txid2, &block_zero, 2000)
|
||||
tx::insert_block_tx(&mut conn, &txid2, &block_zero, 2000)
|
||||
.await
|
||||
.unwrap();
|
||||
tx::insert_block_tx(&mut *conn, &txid3, &block_zero, 3000)
|
||||
tx::insert_block_tx(&mut conn, &txid3, &block_zero, 3000)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -319,24 +319,24 @@ pub mod tests {
|
|||
let cauldron3 = dummy_cauldron(&txid3, &utxo3, &token_zero, 300_000, 3_000, &pkh_zero);
|
||||
|
||||
// Insert utxo funding data
|
||||
utxo_funding::insert_utxo_funding(&mut *conn, &vec![cauldron1.clone()], &txid1)
|
||||
utxo_funding::insert_utxo_funding(&mut conn, &vec![cauldron1.clone()], &txid1)
|
||||
.await
|
||||
.unwrap();
|
||||
utxo_funding::insert_utxo_funding(&mut *conn, &vec![cauldron2.clone()], &txid2)
|
||||
utxo_funding::insert_utxo_funding(&mut conn, &vec![cauldron2.clone()], &txid2)
|
||||
.await
|
||||
.unwrap();
|
||||
utxo_funding::insert_utxo_funding(&mut *conn, &vec![cauldron3.clone()], &txid3)
|
||||
utxo_funding::insert_utxo_funding(&mut conn, &vec![cauldron3.clone()], &txid3)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert pool data
|
||||
pool::insert_new_pool(&mut *conn, &cauldron1).await.unwrap();
|
||||
pool::insert_new_pool(&mut *conn, &cauldron2).await.unwrap();
|
||||
pool::insert_new_pool(&mut *conn, &cauldron3).await.unwrap();
|
||||
pool::insert_new_pool(&mut conn, &cauldron1).await.unwrap();
|
||||
pool::insert_new_pool(&mut conn, &cauldron2).await.unwrap();
|
||||
pool::insert_new_pool(&mut conn, &cauldron3).await.unwrap();
|
||||
|
||||
// Insert pool history entries
|
||||
pool::insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo1,
|
||||
&cauldron1,
|
||||
Some(1000),
|
||||
|
|
@ -347,7 +347,7 @@ pub mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
pool::insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo2,
|
||||
&cauldron2,
|
||||
Some(2000),
|
||||
|
|
@ -358,7 +358,7 @@ pub mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
pool::insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo3,
|
||||
&cauldron3,
|
||||
Some(3000),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
// 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
|
||||
|
||||
use bitcoin_hashes::hex::FromHex;
|
||||
use bitcoincash::TokenID;
|
||||
use rocket::{get, State};
|
||||
use serde_json::json;
|
||||
|
|
@ -46,7 +45,7 @@ pub async fn tx_latest(
|
|||
|
||||
let token = match token {
|
||||
None => None,
|
||||
Some(tokenhex) => match TokenID::from_hex(tokenhex) {
|
||||
Some(tokenhex) => match tokenhex.parse::<TokenID>() {
|
||||
Ok(token) => Some(token),
|
||||
Err(_) => {
|
||||
return Err(bad_request(
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@ mod tests {
|
|||
use crate::db::cauldron::tx::{create_table as create_tx_table, insert_block_tx};
|
||||
use crate::db::cauldron::utxo_funding::create_table as create_utxo_funding_table;
|
||||
use crate::utiltest::mock_db_pool;
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
use bitcoin_hashes::Hash;
|
||||
use bitcoincash::{BlockHash, PubkeyHash, TokenID, Txid};
|
||||
use riftenlabs_defi::cauldron::ParsedContract;
|
||||
|
|
@ -149,23 +148,27 @@ mod tests {
|
|||
let mut conn = mock_db.cauldron_w.acquire().await.unwrap();
|
||||
|
||||
// Insert test data
|
||||
let owner_pkh =
|
||||
PubkeyHash::from_hash(bitcoin_hashes::hash160::Hash::from_slice(&[0xca; 20]).unwrap());
|
||||
let block_zero =
|
||||
BlockHash::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0x00; 32]).unwrap());
|
||||
let owner_pkh = PubkeyHash::from_raw_hash(
|
||||
bitcoin_hashes::hash160::Hash::from_slice(&[0xca; 20]).unwrap(),
|
||||
);
|
||||
let block_zero = BlockHash::from_raw_hash(
|
||||
bitcoin_hashes::sha256d::Hash::from_slice(&[0x00; 32]).unwrap(),
|
||||
);
|
||||
let current_time = time_now() as u64;
|
||||
|
||||
// Create test pool and history entries
|
||||
let utxo =
|
||||
OutPointHash::from_hash(bitcoin_hashes::sha256::Hash::from_slice(&[0xda; 32]).unwrap());
|
||||
let utxo = OutPointHash::from_raw_hash(
|
||||
bitcoin_hashes::sha256::Hash::from_slice(&[0xda; 32]).unwrap(),
|
||||
);
|
||||
let token_id =
|
||||
TokenID::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdb; 32]).unwrap());
|
||||
let txid = Txid::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdc; 32]).unwrap());
|
||||
TokenID::from_raw_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdb; 32]).unwrap());
|
||||
let txid =
|
||||
Txid::from_raw_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdc; 32]).unwrap());
|
||||
|
||||
let cauldron = ParsedContract {
|
||||
pkh: owner_pkh,
|
||||
is_withdrawn: false,
|
||||
spent_utxo_hash: OutPointHash::from_hash(
|
||||
spent_utxo_hash: OutPointHash::from_raw_hash(
|
||||
bitcoin_hashes::sha256::Hash::from_slice(&[0x00; 32]).unwrap(),
|
||||
),
|
||||
new_utxo_hash: Some(utxo),
|
||||
|
|
@ -176,9 +179,9 @@ mod tests {
|
|||
token_amount: Some(100),
|
||||
};
|
||||
|
||||
insert_new_pool(&mut *conn, &cauldron).await.unwrap();
|
||||
insert_new_pool(&mut conn, &cauldron).await.unwrap();
|
||||
insert_block_tx(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&txid,
|
||||
&block_zero,
|
||||
current_time.try_into().unwrap(),
|
||||
|
|
@ -188,7 +191,7 @@ mod tests {
|
|||
|
||||
// Insert pool history entry with volume
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo,
|
||||
&cauldron,
|
||||
Some(current_time),
|
||||
|
|
@ -216,23 +219,27 @@ mod tests {
|
|||
let mut conn = mock_db.cauldron_w.acquire().await.unwrap();
|
||||
|
||||
// Insert test data
|
||||
let owner_pkh =
|
||||
PubkeyHash::from_hash(bitcoin_hashes::hash160::Hash::from_slice(&[0xca; 20]).unwrap());
|
||||
let block_zero =
|
||||
BlockHash::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0x00; 32]).unwrap());
|
||||
let owner_pkh = PubkeyHash::from_raw_hash(
|
||||
bitcoin_hashes::hash160::Hash::from_slice(&[0xca; 20]).unwrap(),
|
||||
);
|
||||
let block_zero = BlockHash::from_raw_hash(
|
||||
bitcoin_hashes::sha256d::Hash::from_slice(&[0x00; 32]).unwrap(),
|
||||
);
|
||||
let current_time = time_now() as u64;
|
||||
|
||||
// Create test pool and history entries
|
||||
let utxo =
|
||||
OutPointHash::from_hash(bitcoin_hashes::sha256::Hash::from_slice(&[0xda; 32]).unwrap());
|
||||
let utxo = OutPointHash::from_raw_hash(
|
||||
bitcoin_hashes::sha256::Hash::from_slice(&[0xda; 32]).unwrap(),
|
||||
);
|
||||
let token_id =
|
||||
TokenID::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdb; 32]).unwrap());
|
||||
let txid = Txid::from_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdc; 32]).unwrap());
|
||||
TokenID::from_raw_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdb; 32]).unwrap());
|
||||
let txid =
|
||||
Txid::from_raw_hash(bitcoin_hashes::sha256d::Hash::from_slice(&[0xdc; 32]).unwrap());
|
||||
|
||||
let cauldron = ParsedContract {
|
||||
pkh: owner_pkh,
|
||||
is_withdrawn: false,
|
||||
spent_utxo_hash: OutPointHash::from_hash(
|
||||
spent_utxo_hash: OutPointHash::from_raw_hash(
|
||||
bitcoin_hashes::sha256::Hash::from_slice(&[0x00; 32]).unwrap(),
|
||||
),
|
||||
new_utxo_hash: Some(utxo),
|
||||
|
|
@ -243,9 +250,9 @@ mod tests {
|
|||
token_amount: Some(100),
|
||||
};
|
||||
|
||||
insert_new_pool(&mut *conn, &cauldron).await.unwrap();
|
||||
insert_new_pool(&mut conn, &cauldron).await.unwrap();
|
||||
insert_block_tx(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&txid,
|
||||
&block_zero,
|
||||
current_time.try_into().unwrap(),
|
||||
|
|
@ -255,7 +262,7 @@ mod tests {
|
|||
|
||||
// Insert pool history entry with volume
|
||||
insert_pool_history_entry(
|
||||
&mut *conn,
|
||||
&mut conn,
|
||||
&utxo,
|
||||
&cauldron,
|
||||
Some(current_time),
|
||||
|
|
@ -271,7 +278,7 @@ mod tests {
|
|||
&mock_db.cauldron_w,
|
||||
current_time - 3600,
|
||||
current_time + 3600,
|
||||
&token_id.to_hex(),
|
||||
&token_id.to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ pub fn is_genesis_tx(tx: &Transaction) -> Option<TokenID> {
|
|||
.input
|
||||
.iter()
|
||||
.filter(|i| i.previous_output.vout == 0)
|
||||
.map(|i| i.previous_output.txid.into_inner())
|
||||
.map(|i| i.previous_output.txid.to_byte_array())
|
||||
.collect();
|
||||
|
||||
for o in &tx.output {
|
||||
match &o.token {
|
||||
Some(token) => {
|
||||
if potential_token_ids.contains(token.id.as_inner()) {
|
||||
if potential_token_ids.contains(&token.id.to_byte_array()) {
|
||||
return Some(token.id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ pub fn ttor_sorted(txs: Vec<Transaction>) -> Vec<Transaction> {
|
|||
let txs = {
|
||||
let mut queue: VecDeque<Transaction> = txs.into_iter().collect();
|
||||
|
||||
let mut queue_txids: HashSet<Txid> = queue.iter().map(|tx| tx.txid()).collect();
|
||||
let mut queue_txids: HashSet<Txid> = queue.iter().map(|tx| tx.compute_txid()).collect();
|
||||
|
||||
let mut txs: Vec<Transaction> = Vec::with_capacity(queue.len());
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ pub fn ttor_sorted(txs: Vec<Transaction>) -> Vec<Transaction> {
|
|||
if has_parent {
|
||||
queue.push_back(tx);
|
||||
} else {
|
||||
queue_txids.remove(&tx.txid());
|
||||
queue_txids.remove(&tx.compute_txid());
|
||||
txs.push(tx);
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ pub fn ttor_sorted_kahn(txs: Vec<Transaction>) -> Vec<Transaction> {
|
|||
|
||||
// Initialize data structures
|
||||
for tx in txs {
|
||||
let txid = tx.txid();
|
||||
let txid = tx.compute_txid();
|
||||
tx_map.insert(txid, tx);
|
||||
in_degree.insert(txid, 0);
|
||||
graph.insert(txid, Vec::new());
|
||||
|
|
@ -123,17 +123,20 @@ pub fn ttor_sorted_kahn(txs: Vec<Transaction>) -> Vec<Transaction> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use bitcoin_hashes::{sha256d, Hash};
|
||||
use bitcoincash::{OutPoint, PackedLockTime, Script, Sequence, TxIn, TxOut, Witness};
|
||||
use bitcoincash::{
|
||||
locktime::absolute::LockTime, transaction::Version, Amount, OutPoint, ScriptBuf, Sequence,
|
||||
TxIn, TxOut, Witness,
|
||||
};
|
||||
|
||||
fn create_mock_transaction(txid: [u8; 32], inputs: Vec<[u8; 32]>) -> Transaction {
|
||||
let tx_inputs: Vec<TxIn> = inputs
|
||||
.into_iter()
|
||||
.map(|input_txid| TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: Txid::from_hash(sha256d::Hash::from_inner(input_txid)),
|
||||
txid: Txid::from_raw_hash(sha256d::Hash::from_byte_array(input_txid)),
|
||||
vout: 0,
|
||||
},
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0),
|
||||
witness: Witness::new(),
|
||||
})
|
||||
|
|
@ -142,19 +145,19 @@ mod tests {
|
|||
// Create a transaction with a unique txid by using the provided txid parameter
|
||||
// We need to create a transaction that has the desired txid when txid() is called
|
||||
let mut tx = Transaction {
|
||||
version: 1,
|
||||
lock_time: PackedLockTime(0),
|
||||
version: Version::ONE,
|
||||
lock_time: LockTime::ZERO,
|
||||
input: tx_inputs,
|
||||
output: vec![TxOut {
|
||||
value: 1000,
|
||||
script_pubkey: Script::new(),
|
||||
value: Amount::from_sat(1000),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
token: None,
|
||||
}],
|
||||
};
|
||||
|
||||
// For testing purposes, we'll create transactions with different outputs to ensure different txids
|
||||
// The txid is calculated from the transaction content, so we need to make them different
|
||||
tx.output[0].value = txid[0] as u64 * 1000; // Use first byte of txid to make value unique
|
||||
tx.output[0].value = Amount::from_sat(txid[0] as u64 * 1000); // Use first byte of txid to make value unique
|
||||
|
||||
tx
|
||||
}
|
||||
|
|
@ -169,28 +172,28 @@ mod tests {
|
|||
.iter()
|
||||
.map(|parent_tx| TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: parent_tx.txid(),
|
||||
txid: parent_tx.compute_txid(),
|
||||
vout: 0,
|
||||
},
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence(0),
|
||||
witness: Witness::new(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut tx = Transaction {
|
||||
version: 1,
|
||||
lock_time: PackedLockTime(0),
|
||||
version: Version::ONE,
|
||||
lock_time: LockTime::ZERO,
|
||||
input: tx_inputs,
|
||||
output: vec![TxOut {
|
||||
value: 1000,
|
||||
script_pubkey: Script::new(),
|
||||
value: Amount::from_sat(1000),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
token: None,
|
||||
}],
|
||||
};
|
||||
|
||||
// Make the transaction unique by using the base_txid
|
||||
tx.output[0].value = base_txid[0] as u64 * 1000;
|
||||
tx.output[0].value = Amount::from_sat(base_txid[0] as u64 * 1000);
|
||||
|
||||
tx
|
||||
}
|
||||
|
|
@ -212,8 +215,9 @@ mod tests {
|
|||
assert_eq!(result_kahn.len(), 3);
|
||||
|
||||
// Both should contain all transactions
|
||||
let original_txids: HashSet<Txid> = result_original.iter().map(|tx| tx.txid()).collect();
|
||||
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.txid()).collect();
|
||||
let original_txids: HashSet<Txid> =
|
||||
result_original.iter().map(|tx| tx.compute_txid()).collect();
|
||||
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.compute_txid()).collect();
|
||||
assert_eq!(original_txids, kahn_txids);
|
||||
|
||||
// Verify dependencies are respected in both results
|
||||
|
|
@ -239,8 +243,9 @@ mod tests {
|
|||
assert_eq!(result_kahn.len(), 4);
|
||||
|
||||
// Both should contain all transactions
|
||||
let original_txids: HashSet<Txid> = result_original.iter().map(|tx| tx.txid()).collect();
|
||||
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.txid()).collect();
|
||||
let original_txids: HashSet<Txid> =
|
||||
result_original.iter().map(|tx| tx.compute_txid()).collect();
|
||||
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.compute_txid()).collect();
|
||||
assert_eq!(original_txids, kahn_txids);
|
||||
|
||||
// Verify dependencies are respected in both results
|
||||
|
|
@ -265,8 +270,9 @@ mod tests {
|
|||
assert_eq!(result_kahn.len(), 3);
|
||||
|
||||
// Both should contain all transactions
|
||||
let original_txids: HashSet<Txid> = result_original.iter().map(|tx| tx.txid()).collect();
|
||||
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.txid()).collect();
|
||||
let original_txids: HashSet<Txid> =
|
||||
result_original.iter().map(|tx| tx.compute_txid()).collect();
|
||||
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.compute_txid()).collect();
|
||||
assert_eq!(original_txids, kahn_txids);
|
||||
|
||||
// For independent transactions, both should produce valid orderings
|
||||
|
|
@ -295,8 +301,8 @@ mod tests {
|
|||
|
||||
assert_eq!(result_original.len(), 1);
|
||||
assert_eq!(result_kahn.len(), 1);
|
||||
assert_eq!(result_original[0].txid(), tx.txid());
|
||||
assert_eq!(result_kahn[0].txid(), tx.txid());
|
||||
assert_eq!(result_original[0].compute_txid(), tx.compute_txid());
|
||||
assert_eq!(result_kahn[0].compute_txid(), tx.compute_txid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -334,8 +340,9 @@ mod tests {
|
|||
assert_eq!(result_kahn.len(), 8);
|
||||
|
||||
// Both should contain all transactions
|
||||
let original_txids: HashSet<Txid> = result_original.iter().map(|tx| tx.txid()).collect();
|
||||
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.txid()).collect();
|
||||
let original_txids: HashSet<Txid> =
|
||||
result_original.iter().map(|tx| tx.compute_txid()).collect();
|
||||
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.compute_txid()).collect();
|
||||
assert_eq!(original_txids, kahn_txids);
|
||||
|
||||
// Verify dependencies are respected in both results
|
||||
|
|
@ -348,7 +355,7 @@ mod tests {
|
|||
let tx_positions: HashMap<Txid, usize> = txs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(pos, tx)| (tx.txid(), pos))
|
||||
.map(|(pos, tx)| (tx.compute_txid(), pos))
|
||||
.collect();
|
||||
|
||||
for (pos, tx) in txs.iter().enumerate() {
|
||||
|
|
@ -358,7 +365,7 @@ mod tests {
|
|||
assert!(
|
||||
parent_pos < pos,
|
||||
"Dependency violation: transaction {} (pos {}) depends on {} (pos {})",
|
||||
tx.txid(),
|
||||
tx.compute_txid(),
|
||||
pos,
|
||||
input.previous_output.txid,
|
||||
parent_pos
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue