diff --git a/Cargo.lock b/Cargo.lock index 696b3d6..cc7a76f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 0685d63..078d99b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/bcmr/bcmrdownloader.rs b/src/bcmr/bcmrdownloader.rs index 82619c6..0c2a110 100644 --- a/src/bcmr/bcmrdownloader.rs +++ b/src/bcmr/bcmrdownloader.rs @@ -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, diff --git a/src/bcmr/mod.rs b/src/bcmr/mod.rs index c565cca..9187291 100644 --- a/src/bcmr/mod.rs +++ b/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 { - 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::() + .unwrap(); + let token_b = "0202020202020202020202020202020202020202020202020202020202020202" + .parse::() + .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::() + .unwrap(); + let prev_txid_b = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .parse::() + .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::() + .unwrap(); + let prev_txid = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .parse::() + .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::().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::().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::().unwrap().to_blob(); let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM auth_chain_entry WHERE txid = ?") .bind(&txid_blob) diff --git a/src/bcmr/parsedbcmr.rs b/src/bcmr/parsedbcmr.rs index 3063247..1fcc59a 100644 --- a/src/bcmr/parsedbcmr.rs +++ b/src/bcmr/parsedbcmr.rs @@ -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 = 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::() + .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::() .unwrap(), dummy_hash.clone(), dummy_hash, diff --git a/src/bcmr/utilurl.rs b/src/bcmr/utilurl.rs index 15808b3..c26fd9e 100644 --- a/src/bcmr/utilurl.rs +++ b/src/bcmr/utilurl.rs @@ -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())) } diff --git a/src/cashaddr/mod.rs b/src/cashaddr/mod.rs index 290883d..6e2f70b 100644 --- a/src/cashaddr/mod.rs +++ b/src/cashaddr/mod.rs @@ -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 diff --git a/src/chain.rs b/src/chain.rs index 096e228..a8c3088 100644 --- a/src/chain.rs +++ b/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> { pub fn download_block_header(electrum: &Client, blockhash: &BlockHash) -> Result { 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" ); } diff --git a/src/db/bcmr/mod.rs b/src/db/bcmr/mod.rs index 8f1356f..ab2dfd5 100644 --- a/src/db/bcmr/mod.rs +++ b/src/db/bcmr/mod.rs @@ -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>() ); } @@ -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::() + .unwrap(); + let token_b = "0202020202020202020202020202020202020202020202020202020202020202" + .parse::() + .unwrap(); - let utxo = OutPointHash::from_hex( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ) - .unwrap(); - let txid = - Txid::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") - .unwrap(); + let utxo = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .parse::() + .unwrap(); + let txid = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .parse::() + .unwrap(); update_bcmr_failure(&pool, &utxo, &txid, &token_a, "error A", false) .await diff --git a/src/db/blob.rs b/src/db/blob.rs index f5adf32..6561bd6 100644 --- a/src/db/blob.rs +++ b/src/db/blob.rs @@ -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::()` 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::(hex)` or `TokenID::from_hex(hex)?.to_blob()`. +//! Instead, use `display_hex_to_blob::(hex)` or `hex.parse::()?.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 { - self.into_inner().to_vec() + self.as_byte_array().to_vec() } } impl ToBlob for BlockHash { fn to_blob(&self) -> Vec { - self.into_inner().to_vec() + self.as_byte_array().to_vec() } } impl ToBlob for TokenID { fn to_blob(&self) -> Vec { - self.into_inner().to_vec() + self.as_byte_array().to_vec() } } impl ToBlob for OutPointHash { fn to_blob(&self) -> Vec { - self.into_inner().to_vec() + self.as_byte_array().to_vec() } } impl ToBlob for PoolID { fn to_blob(&self) -> Vec { - 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 { - 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 { 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 { 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 { 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(hex: &str) -> Result> 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(blob: &[u8]) -> Result 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::().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::().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::().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::().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::().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::().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::().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::(display_hex).unwrap(); - let manual_result = TokenID::from_hex(display_hex).unwrap().to_blob(); + let manual_result = display_hex.parse::().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::().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::().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::().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::().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::().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::().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::().unwrap().to_blob(); assert_ne!(raw_bytes, poolid_blob, "PoolID should reverse"); } } diff --git a/src/db/cauldron/header.rs b/src/db/cauldron/header.rs index 60e81b1..9f9deeb 100644 --- a/src/db/cauldron/header.rs +++ b/src/db/cauldron/header.rs @@ -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 Result { let row: Option<(Vec,)> = 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 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) diff --git a/src/db/cauldron/mod.rs b/src/db/cauldron/mod.rs index 977c1d0..253fe3c 100644 --- a/src/db/cauldron/mod.rs +++ b/src/db/cauldron/mod.rs @@ -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);", diff --git a/src/db/cauldron/ohlcv.rs b/src/db/cauldron/ohlcv.rs index fa32178..68b3c6a 100644 --- a/src/db/cauldron/ohlcv.rs +++ b/src/db/cauldron/ohlcv.rs @@ -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> { + 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> { - 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()) -} diff --git a/src/db/cauldron/pool.rs b/src/db/cauldron/pool.rs index 3082e0a..8e0416a 100644 --- a/src/db/cauldron/pool.rs +++ b/src/db/cauldron/pool.rs @@ -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" ); } diff --git a/src/db/cauldron/tokenlist/metrics_cache.rs b/src/db/cauldron/tokenlist/metrics_cache.rs index 0a3c072..c92930f 100644 --- a/src/db/cauldron/tokenlist/metrics_cache.rs +++ b/src/db/cauldron/tokenlist/metrics_cache.rs @@ -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::() { Ok(t) => t, Err(_) => { // still update the "now" fields and basics diff --git a/src/db/cauldron/tokenlist/test.rs b/src/db/cauldron/tokenlist/test.rs index bf3640a..09de3c0 100644 --- a/src/db/cauldron/tokenlist/test.rs +++ b/src/db/cauldron/tokenlist/test.rs @@ -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); diff --git a/src/db/cauldron/tokentoken.rs b/src/db/cauldron/tokentoken.rs new file mode 100644 index 0000000..95b7f51 --- /dev/null +++ b/src/db/cauldron/tokentoken.rs @@ -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> { + let row: Option<(Vec,)> = + 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, + first_seen_timestamp: Option, + 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, + mtp_timestamp: Option, + first_seen_timestamp: Option, +) -> 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 { + 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(value: &u64, serializer: S) -> Result +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> { + 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 = row.get(0); + let nft_owner_blob: Vec = row.get(1); + let token_a_blob: Vec = row.get(2); + let token_b_blob: Vec = 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 = 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::(&pool_blob)?, + nft_owner: hex::encode(&nft_owner_blob), + token_a_id: blob_to_display_hex::(&token_a_blob)?, + token_b_id: blob_to_display_hex::(&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::(&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> { + 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 = row.get(0); + out.push(blob_to_display_hex::(&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 = row.get(0); + let first_seen: Option = row.get(1); + let effective: Option = 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()); + } +} diff --git a/src/db/cauldron/user.rs b/src/db/cauldron/user.rs index 380c61c..79351c3 100644 --- a/src/db/cauldron/user.rs +++ b/src/db/cauldron/user.rs @@ -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"), diff --git a/src/db/crc20/mod.rs b/src/db/crc20/mod.rs index 77ec8fe..a772e24 100644 --- a/src/db/crc20/mod.rs +++ b/src/db/crc20/mod.rs @@ -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::() + .unwrap(); + let token_b = "0202020202020202020202020202020202020202020202020202020202020202" + .parse::() + .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" ); } diff --git a/src/db/init.rs b/src/db/init.rs index 0a6a47f..a4ed71f 100644 --- a/src/db/init.rs +++ b/src/db/init.rs @@ -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) = diff --git a/src/db/moria/mod.rs b/src/db/moria/mod.rs index a44b67a..07e4fc7 100644 --- a/src/db/moria/mod.rs +++ b/src/db/moria/mod.rs @@ -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] { diff --git a/src/db/oracle/mod.rs b/src/db/oracle/mod.rs index 537a2e0..bd23d98 100644 --- a/src/db/oracle/mod.rs +++ b/src/db/oracle/mod.rs @@ -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::() + .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. diff --git a/src/db/oracle/v2.rs b/src/db/oracle/v2.rs index 01c7e00..fe0284b 100644 --- a/src/db/oracle/v2.rs +++ b/src/db/oracle/v2.rs @@ -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 = 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::() + .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 = 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::() + .expect("v1 BCH/USD token hex is valid") }) } diff --git a/src/db/search/mod.rs b/src/db/search/mod.rs index 3d0ff17..ceac0fc 100644 --- a/src/db/search/mod.rs +++ b/src/db/search/mod.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::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> { - let is_full_hex_token_id = TokenID::from_hex(search_query).is_ok(); + let is_full_hex_token_id = search_query.parse::().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 Result> { - let is_full_hex_token_id = TokenID::from_hex(search_query).is_ok(); + let is_full_hex_token_id = search_query.parse::().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::().is_ok() { where_sql.push_str("WHERE token_id = ?"); Filter::Id(display_hex_to_blob::(&q.to_lowercase())?) } else { diff --git a/src/db/search/test.rs b/src/db/search/test.rs index 74bcf3d..f97b016 100644 --- a/src/db/search/test.rs +++ b/src/db/search/test.rs @@ -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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::().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::().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::().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::().unwrap().to_blob(); // They MUST differ assert_ne!( diff --git a/src/def.rs b/src/def.rs index d085068..3c2d537 100644 --- a/src/def.rs +++ b/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); +} diff --git a/src/electrum.rs b/src/electrum.rs index 9fbd81a..9455255 100644 --- a/src/electrum.rs +++ b/src/electrum.rs @@ -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, HashSet)> { 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, 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 = [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, 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 { 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(), )?; diff --git a/src/index.rs b/src/index.rs index 578a3a8..a2a5f9a 100644 --- a/src/index.rs +++ b/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) -> 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::() + .expect("valid chipnet moria token_id"), + bp_oracle: "f3d6b85bfb0eaaf417ccabc8c8032464c5ec410e40b3b13f0c369a541bfb2a6a" + .parse::() + .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::() + .expect("valid mainnet moria token_id"), + bp_oracle: "01711e39e7bf3b8ca0d9a6fc6ea32e340caa1d64dc7d1dc51fae20fd66755558" + .parse::() + .expect("valid mainnet bp_oracle token_id"), }, } } @@ -77,13 +72,13 @@ pub async fn update_mempool(db: &DB, electrum: Arc>) -> 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 = 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 = 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 = txs_to_add.iter().map(|t| **t).collect(); @@ -111,28 +106,34 @@ pub async fn update_mempool(db: &DB, electrum: Arc>) -> 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 = 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>) -> 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::() .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::() .map_err(|e| anyhow::anyhow!("Invalid CASHTOKEN_ACTIVATION_HEIGHT: {}", e))?, - None => BlockHash::from_hex(CASHTOKEN_ACTIVATION_HEIGHT) + None => CASHTOKEN_ACTIVATION_HEIGHT + .parse::() .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::().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()) diff --git a/src/main.rs b/src/main.rs index b4ed56e..1f1fcaf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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) } diff --git a/src/rpc/bcmr.rs b/src/rpc/bcmr.rs index a9c53d9..1eda675 100644 --- a/src/rpc/bcmr.rs +++ b/src/rpc/bcmr.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::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) -> 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::().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) -> 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::().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)?; diff --git a/src/rpc/candlesticks.rs b/src/rpc/candlesticks.rs index f2f3427..e8dcaaa 100644 --- a/src/rpc/candlesticks.rs +++ b/src/rpc/candlesticks.rs @@ -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), diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 3faad84..52d186f 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -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; diff --git a/src/rpc/oracle.rs b/src/rpc/oracle.rs index 1888791..12c995f 100644 --- a/src/rpc/oracle.rs +++ b/src/rpc/oracle.rs @@ -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::().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::().map_err(|e| { bad_request( ApiErrorCode::InvalidTokenId, &format!("Invalid token ID: {e}"), diff --git a/src/rpc/pool.rs b/src/rpc/pool.rs index deb8d58..a15c5b2 100644 --- a/src/rpc/pool.rs +++ b/src/rpc/pool.rs @@ -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 { 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::().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?&")] pub async fn pool_id_from_utxo(txid: &str, n: u32, conn: &State) -> CachedApiResult { - let txid = Txid::from_hex(txid) + let txid = txid + .parse::() .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) -> 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), )), } } diff --git a/src/rpc/price.rs b/src/rpc/price.rs index 919e733..63a0756 100644 --- a/src/rpc/price.rs +++ b/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) -> CachedA "Timestamp is in the future", )); } - let token = TokenID::from_hex(token).map_err(|e| { + let token = token.parse::().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//at/` 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), diff --git a/src/rpc/tokens.rs b/src/rpc/tokens.rs index 6bc53db..24558af 100644 --- a/src/rpc/tokens.rs +++ b/src/rpc/tokens.rs @@ -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//first_pool")] pub async fn first_pool_creation(token: &str, dbp: &State) -> CachedApiResult { - let token = TokenID::from_hex(token).map_err(|e| { + let token = token.parse::().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))) => { diff --git a/src/rpc/tokentoken.rs b/src/rpc/tokentoken.rs new file mode 100644 index 0000000..c92c43a --- /dev/null +++ b/src/rpc/tokentoken.rs @@ -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?&")] +pub async fn list_active_pools( + token_a: Option<&str>, + token_b: Option<&str>, + conn: &State, +) -> CachedApiResult { + 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::().map_err(|e| { + bad_request( + ApiErrorCode::InvalidTokenId, + &format!("Invalid token_a: {e}"), + ) + })?; + let token_b = token_b.parse::().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) -> CachedApiResult { + let tokens = db_pair_tokens(&conn.cauldron_r).await.map_err(db_error)?; + Ok(cached_ok(json!({ "tokens": tokens }), CACHE_AGGREGATE)) +} diff --git a/src/rpc/tvl.rs b/src/rpc/tvl.rs index d463916..6a5ebf8 100644 --- a/src/rpc/tvl.rs +++ b/src/rpc/tvl.rs @@ -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), diff --git a/src/rpc/tx.rs b/src/rpc/tx.rs index 6b55a59..778826a 100644 --- a/src/rpc/tx.rs +++ b/src/rpc/tx.rs @@ -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::() { Ok(token) => Some(token), Err(_) => { return Err(bad_request( diff --git a/src/rpc/volume.rs b/src/rpc/volume.rs index 9e7b7ee..8ff7558 100644 --- a/src/rpc/volume.rs +++ b/src/rpc/volume.rs @@ -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(); diff --git a/src/utiltoken.rs b/src/utiltoken.rs index 1016ddd..18d3526 100644 --- a/src/utiltoken.rs +++ b/src/utiltoken.rs @@ -13,13 +13,13 @@ pub fn is_genesis_tx(tx: &Transaction) -> Option { .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); } } diff --git a/src/utiltx.rs b/src/utiltx.rs index c3b6006..0d597fd 100644 --- a/src/utiltx.rs +++ b/src/utiltx.rs @@ -16,7 +16,7 @@ pub fn ttor_sorted(txs: Vec) -> Vec { let txs = { let mut queue: VecDeque = txs.into_iter().collect(); - let mut queue_txids: HashSet = queue.iter().map(|tx| tx.txid()).collect(); + let mut queue_txids: HashSet = queue.iter().map(|tx| tx.compute_txid()).collect(); let mut txs: Vec = Vec::with_capacity(queue.len()); @@ -34,7 +34,7 @@ pub fn ttor_sorted(txs: Vec) -> Vec { 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) -> Vec { // 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) -> Vec { 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 = 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 = result_original.iter().map(|tx| tx.txid()).collect(); - let kahn_txids: HashSet = result_kahn.iter().map(|tx| tx.txid()).collect(); + let original_txids: HashSet = + result_original.iter().map(|tx| tx.compute_txid()).collect(); + let kahn_txids: HashSet = 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 = result_original.iter().map(|tx| tx.txid()).collect(); - let kahn_txids: HashSet = result_kahn.iter().map(|tx| tx.txid()).collect(); + let original_txids: HashSet = + result_original.iter().map(|tx| tx.compute_txid()).collect(); + let kahn_txids: HashSet = 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 = result_original.iter().map(|tx| tx.txid()).collect(); - let kahn_txids: HashSet = result_kahn.iter().map(|tx| tx.txid()).collect(); + let original_txids: HashSet = + result_original.iter().map(|tx| tx.compute_txid()).collect(); + let kahn_txids: HashSet = 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 = result_original.iter().map(|tx| tx.txid()).collect(); - let kahn_txids: HashSet = result_kahn.iter().map(|tx| tx.txid()).collect(); + let original_txids: HashSet = + result_original.iter().map(|tx| tx.compute_txid()).collect(); + let kahn_txids: HashSet = 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 = 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