diff --git a/.gitignore b/.gitignore index 2b6fa34..600d236 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,8 @@ logs/* /.simplicity-dex.config.toml /.cache taker/ -simplicity-dex \ No newline at end of file +simplicity-dex + +# DB +**/dcd_cache.db +/db \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 32937c6..12e453f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,11 @@ readme = "Readme.md" [workspace.dependencies] anyhow = { version = "1.0.100" } +async-trait = { version = "0.1.89" } bincode = { version = "2.0.1" } chrono = { version = "0.4.42" } clap = { version = "4.5.49", features = ["derive"] } +coin-selection = { path = "./crates/coin-selection" } config = { version = "0.15.18" } contracts = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "baa8ab7", package = "contracts" } contracts-adapter = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "baa8ab7", package = "contracts-adapter" } @@ -31,12 +33,14 @@ humantime = { version = "2.3.0" } nostr = { version = "0.43.1", features = ["std"] } nostr-sdk = { version = "0.43.0" } proptest = { version = "1.9.0" } +reqwest = { version = "0.12.24" } serde = { version = "1.0.228" } serde_json = { version = "1.0.145" } simplicity-lang = { version = "0.6.0" } simplicityhl = { version = "0.2.0" } simplicityhl-core = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "baa8ab7", package = "simplicityhl-core", features = ["encoding"] } sled = { version = "0.34.7" } +sqlx = { version = "0.8.6", features = ["runtime-tokio-native-tls", "sqlite"] } thiserror = { version = "2.0.17" } tokio = { version = "1.48.0", features = ["macros", "test-util", "rt", "rt-multi-thread", "tracing" ] } tracing = { version = "0.1.41" } diff --git a/crates/coin-selection/Cargo.toml b/crates/coin-selection/Cargo.toml new file mode 100644 index 0000000..f4312cf --- /dev/null +++ b/crates/coin-selection/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "coin-selection" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +bincode = { workspace = true } +contracts = { workspace = true } +contracts-adapter = { workspace = true } +global-utils = { workspace = true } +hex = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +simplicity-lang = { workspace = true } +simplicityhl = { workspace = true } +simplicityhl-core = { workspace = true } +sqlx = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +dotenvy = { workspace = true } \ No newline at end of file diff --git a/crates/coin-selection/migrations/20251204084835_init.sql b/crates/coin-selection/migrations/20251204084835_init.sql new file mode 100644 index 0000000..a2b6c7f --- /dev/null +++ b/crates/coin-selection/migrations/20251204084835_init.sql @@ -0,0 +1,29 @@ +CREATE TABLE IF NOT EXISTS outpoints +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tx_id VARYING CHARACTER(64) NOT NULL, + vout INTEGER NOT NULL, + owner_script_pubkey TEXT NOT NULL, + asset_id TEXT NOT NULL, + spent BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (tx_id, vout) +); + +CREATE TABLE IF NOT EXISTS dcd_params +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + taproot_pubkey_gen TEXT NOT NULL UNIQUE, + dcd_args_blob BLOB NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS dcd_token_entropies +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + taproot_pubkey_gen TEXT NOT NULL UNIQUE, + token_entropies_blob BLOB NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- todo: create indexes diff --git a/crates/coin-selection/src/common.rs b/crates/coin-selection/src/common.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/coin-selection/src/common.rs @@ -0,0 +1 @@ + diff --git a/crates/coin-selection/src/lib.rs b/crates/coin-selection/src/lib.rs new file mode 100644 index 0000000..488b69c --- /dev/null +++ b/crates/coin-selection/src/lib.rs @@ -0,0 +1,4 @@ +pub mod common; +pub mod sqlite_db; +pub mod types; +pub mod utils; diff --git a/crates/coin-selection/src/sqlite_db.rs b/crates/coin-selection/src/sqlite_db.rs new file mode 100644 index 0000000..b3d079e --- /dev/null +++ b/crates/coin-selection/src/sqlite_db.rs @@ -0,0 +1,250 @@ +use crate::types::{CoinSelectionStorage, DcdContractTokenEntropies, GetTokenFilter, OutPointInfo, OutPointInfoRaw}; +use crate::types::{DcdParamsStorage, EntropyStorage, Result}; +use anyhow::{Context, anyhow}; +use async_trait::async_trait; +use contracts::DCDArguments; +use simplicityhl::elements::OutPoint; +use sqlx::{Connection, Sqlite, SqlitePool, migrate::MigrateDatabase}; +use std::path::PathBuf; +use tracing::instrument; + +const CARGO_MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); + +#[derive(Clone)] +pub struct SqliteRepo { + pool: SqlitePool, +} + +const SQLITE_DB_NAME: &str = "dcd_cache.db"; + +impl SqliteRepo { + pub async fn from_url(db_url: &str) -> Result { + Self::create_database(db_url).await?; + let pool = SqlitePool::connect(db_url).await?; + sqlx::migrate!().run(&pool).await?; + Ok(Self { pool }) + } + + #[inline] + async fn create_database(database_url: &str) -> Result<()> { + if !Sqlite::database_exists(database_url).await? { + Sqlite::create_database(database_url).await?; + } + Ok(()) + } + + pub async fn from_pool(pool: SqlitePool) -> Result { + sqlx::migrate!().run(&pool).await?; + Ok(Self { pool }) + } + + pub async fn new() -> Result { + let db_path = Self::default_db_path()?; + let sqlite_db_url = Self::get_db_path( + db_path + .to_str() + .with_context(|| anyhow!("No path found in db_path: '{db_path:?}'"))?, + ); + Self::from_url(&sqlite_db_url).await + } + + #[inline] + fn get_db_path(path: impl AsRef) -> String { + format!("sqlite://{}", path.as_ref()) + } + + fn default_db_path() -> Result { + let manifest_dir = PathBuf::from(CARGO_MANIFEST_DIR); + let workspace_root = manifest_dir + .parent() + .and_then(|p| p.parent()) + .ok_or_else(|| anyhow::anyhow!("Could not determine workspace root"))?; + + let db_dir = workspace_root.join("db"); + + if !db_dir.exists() { + std::fs::create_dir_all(&db_dir)?; + } + + Ok(db_dir.join(SQLITE_DB_NAME)) + } + + pub async fn healthcheck(&self) -> Result<()> { + self.pool.acquire().await?.ping().await?; + Ok(()) + } +} + +#[async_trait] +impl CoinSelectionStorage for SqliteRepo { + #[instrument(level = "debug", skip_all, err)] + async fn mark_outpoints_spent(&self, outpoints: &[OutPoint]) -> Result<()> { + tracing::debug!("Input params, outpoints: {outpoints:?}"); + + // mark given outpoints as spent in a single transaction + let mut tx = self.pool.begin().await?; + for op in outpoints { + sqlx::query( + r#" + UPDATE outpoints + SET spent = 1 + WHERE tx_id = ? AND vout = ? + "#, + ) + .bind(op.txid.to_string()) + .bind(op.vout as i64) + .execute(&mut *tx) + .await?; + } + tx.commit().await?; + Ok(()) + } + + #[instrument(level = "debug", skip_all, err)] + async fn add_outpoint(&self, info: OutPointInfo) -> Result<()> { + tracing::debug!("Input params, info: {info:?}"); + + sqlx::query( + r#" + INSERT INTO outpoints (tx_id, vout, owner_script_pubkey, asset_id, spent) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(tx_id, vout) DO UPDATE + SET owner_script_pubkey = excluded.owner_script_pubkey, + asset_id = excluded.asset_id, + spent = excluded.spent + "#, + ) + .bind(info.outpoint.txid.to_string()) + .bind(info.outpoint.vout as i64) + .bind(info.owner_script_pubkey) + .bind(info.asset_id) + .bind(info.spent) + .execute(&self.pool) + .await?; + + Ok(()) + } + + #[instrument(level = "debug", skip_all, err)] + async fn get_token_outpoints(&self, filter: GetTokenFilter) -> Result> { + tracing::debug!("Input params, filter: {filter:?}"); + + let base = "SELECT id, tx_id, vout, owner_script_pubkey, asset_id, spent FROM outpoints"; + let where_clause = filter.get_sql_filter(); + let query = format!("{base}{where_clause}"); + + let mut sql_query = sqlx::query_as::<_, (i64, String, i64, String, String, bool)>(&query); + + if let Some(asset_id) = &filter.asset_id { + sql_query = sql_query.bind(asset_id); + } + if let Some(spent) = filter.spent { + sql_query = sql_query.bind(spent); + } + if let Some(owner) = &filter.owner { + sql_query = sql_query.bind(owner); + } + + let rows = sql_query.fetch_all(&self.pool).await?; + + let outpoints = rows + .into_iter() + .filter_map(|(id, tx_id, vout, owner_script_pubkey, asset_id, spent)| { + let tx_id = tx_id.parse().ok()?; + let vout = vout as u32; + Some(OutPointInfoRaw { + id: id as u64, + outpoint: OutPoint::new(tx_id, vout), + owner_script_pubkey, + asset_id, + spent, + }) + }) + .collect(); + + Ok(outpoints) + } +} + +#[async_trait] +impl DcdParamsStorage for SqliteRepo { + #[instrument(level = "debug", skip_all, err)] + async fn add_dcd_params(&self, taproot_pubkey_gen: &str, dcd_args: &DCDArguments) -> Result<()> { + tracing::debug!("Input params, taproot: {taproot_pubkey_gen}, dcd_args: {dcd_args:?}"); + let serialized = bincode::encode_to_vec(dcd_args, bincode::config::standard())?; + + sqlx::query( + "INSERT INTO dcd_params (taproot_pubkey_gen, dcd_args_blob) + VALUES (?, ?) + ON CONFLICT(taproot_pubkey_gen) DO UPDATE SET dcd_args_blob = excluded.dcd_args_blob", + ) + .bind(taproot_pubkey_gen) + .bind(serialized) + .execute(&self.pool) + .await?; + + Ok(()) + } + + #[instrument(level = "debug", skip_all, err)] + async fn get_dcd_params(&self, taproot_pubkey_gen: &str) -> Result> { + tracing::debug!("Input params, taproot: {taproot_pubkey_gen}"); + let row = sqlx::query_as::<_, (Vec,)>("SELECT dcd_args_blob FROM dcd_params WHERE taproot_pubkey_gen = ?") + .bind(taproot_pubkey_gen) + .fetch_optional(&self.pool) + .await?; + + match row { + Some((blob,)) => { + let (dcd_args, _) = bincode::decode_from_slice(&blob, bincode::config::standard())?; + Ok(Some(dcd_args)) + } + None => Ok(None), + } + } +} + +#[async_trait] +impl EntropyStorage for SqliteRepo { + #[instrument(level = "debug", skip_all, err)] + async fn add_dcd_contract_token_entropies( + &self, + taproot_pubkey_gen: &str, + token_entropies: DcdContractTokenEntropies, + ) -> Result<()> { + let serialized = bincode::encode_to_vec(&token_entropies, bincode::config::standard())?; + + sqlx::query( + "INSERT INTO dcd_token_entropies (taproot_pubkey_gen, token_entropies_blob) + VALUES (?, ?) + ON CONFLICT(taproot_pubkey_gen) DO UPDATE SET token_entropies_blob = excluded.token_entropies_blob", + ) + .bind(taproot_pubkey_gen) + .bind(serialized) + .execute(&self.pool) + .await?; + + Ok(()) + } + + #[instrument(level = "debug", skip_all, err)] + async fn get_dcd_contract_token_entropies( + &self, + taproot_pubkey_gen: &str, + ) -> Result> { + let row = sqlx::query_as::<_, (Vec,)>( + "SELECT token_entropies_blob FROM dcd_token_entropies WHERE taproot_pubkey_gen = ?", + ) + .bind(taproot_pubkey_gen) + .fetch_optional(&self.pool) + .await?; + + match row { + Some((blob,)) => { + let (token_entropies, _) = bincode::decode_from_slice(&blob, bincode::config::standard())?; + Ok(Some(token_entropies)) + } + None => Ok(None), + } + } +} diff --git a/crates/coin-selection/src/types.rs b/crates/coin-selection/src/types.rs new file mode 100644 index 0000000..f13ec86 --- /dev/null +++ b/crates/coin-selection/src/types.rs @@ -0,0 +1,303 @@ +use crate::utils::{extract_outpoint_info_from_tx_in, extract_outpoint_info_from_tx_out, fetch_tx}; +use async_trait::async_trait; +use contracts::DCDArguments; +use simplicityhl::elements::{OutPoint, Txid}; +use simplicityhl_core::AssetEntropyHex; + +pub type Result = anyhow::Result; + +#[async_trait] +pub trait CoinSelectionStorage: Send + Sync { + async fn mark_outpoints_spent(&self, outpoint: &[OutPoint]) -> Result<()>; + async fn add_outpoint(&self, info: OutPointInfo) -> Result<()>; + async fn get_token_outpoints(&self, filter: GetTokenFilter) -> Result>; +} + +#[async_trait] +pub trait DcdParamsStorage: Send + Sync { + async fn add_dcd_params(&self, taproot_pubkey_gen: &str, dcd_args: &DCDArguments) -> Result<()>; + async fn get_dcd_params(&self, taproot_pubkey_gen: &str) -> Result>; +} + +#[async_trait] +pub trait EntropyStorage: Send + Sync { + async fn add_dcd_contract_token_entropies( + &self, + taproot_pubkey_gen: &str, + token_entropies: DcdContractTokenEntropies, + ) -> Result<()>; + async fn get_dcd_contract_token_entropies( + &self, + taproot_pubkey_gen: &str, + ) -> Result>; +} + +#[derive(Clone, Debug)] +pub enum TransactionOption { + TakerFundOrder(Txid), + TakerTerminationEarly(Txid), + TakerSettlement(Txid), + MakerFund(Txid), + MakerTerminationCollateral(Txid), + MakerTerminationSettlement(Txid), + MakerSettlement(Txid), +} + +#[derive(Clone, Debug)] +pub enum TransactionInputsOption { + TakerFundOrder(TakerFundInputs), + TakerTerminationEarly(TakerTerminationEarlyInputs), + TakerSettlement(TakerSettlementInputs), + MakerFund(MakerFundInputs), + MakerTerminationCollateral(MakerTerminationCollateralInputs), + MakerTerminationSettlement(MakerTerminationSettlmentInputs), + MakerSettlement(MakerSettlementInputs), +} + +#[derive(Clone, Debug)] +pub enum TransactionInputs { + MakerFund { + taproot_pubkey_gen: String, + script_pubkey: String, + }, + TakerFundOrder { + taproot_pubkey_gen: String, + }, + TakerTerminationEarly { + taproot_pubkey_gen: String, + }, + TakerSettlement { + taproot_pubkey_gen: String, + filter: GetSettlementFilter, + }, + MakerTerminationCollateral { + taproot_pubkey_gen: String, + }, + MakerTerminationSettlement { + taproot_pubkey_gen: String, + }, + MakerSettlement { + taproot_pubkey_gen: String, + filter: GetSettlementFilter, + }, +} + +fn extract_one_value_from_vec(vec: Vec) -> Option { + if vec.is_empty() { None } else { Some(vec[0].clone()) } +} + +#[async_trait] +pub trait CoinSelector: Send + Sync + CoinSelectionStorage + DcdParamsStorage + EntropyStorage { + async fn add_outputs(&self, option: TransactionOption) -> Result<()> { + match option { + TransactionOption::TakerFundOrder(_) => {} + TransactionOption::TakerTerminationEarly(_) => {} + TransactionOption::TakerSettlement(_) => {} + TransactionOption::MakerFund(order) => { + // TODO: add fetching of only needed outs - not all + let fetched_transaction = fetch_tx(order).await?; + let mut outpoints_to_add = + Vec::with_capacity(fetched_transaction.input.len() + fetched_transaction.output.len()); + for tx_in in fetched_transaction.input { + outpoints_to_add.push(extract_outpoint_info_from_tx_in(tx_in).await?); + } + for (i, tx_out) in fetched_transaction.output.into_iter().enumerate() { + outpoints_to_add + .push(extract_outpoint_info_from_tx_out(OutPoint::new(order, i as u32), tx_out).await?); + } + for outpoint in outpoints_to_add { + tracing::debug!("[Coinselector] Adding outpoint {:?}", outpoint); + self.add_outpoint(outpoint).await?; + } + } + TransactionOption::MakerTerminationCollateral(_) => {} + TransactionOption::MakerTerminationSettlement(_) => {} + TransactionOption::MakerSettlement(_) => {} + } + Ok(()) + } + + async fn get_inputs(&self, option: TransactionInputs) -> Result { + let res = match option { + TransactionInputs::MakerFund { + taproot_pubkey_gen, + script_pubkey, + } => { + let dcd_params = self + .get_dcd_params(&taproot_pubkey_gen) + .await? + .ok_or_else(|| anyhow::anyhow!("No dcd params found, taproot_pubkey_gen: {taproot_pubkey_gen}"))?; + + let filler = extract_one_value_from_vec( + self.get_token_outpoints(GetTokenFilter { + asset_id: Some(dcd_params.filler_token_asset_id_hex_le), + spent: Some(false), + owner: Some(script_pubkey), + }) + .await?, + ); + + TransactionInputsOption::MakerFund(MakerFundInputs { + filler_reissuance_tx: filler, + grantor_collateral_reissuance_tx: None, + grantor_settlement_reissuance_tx: None, + asset_settlement_tx: None, + }) + } + TransactionInputs::TakerFundOrder { .. } => TransactionInputsOption::TakerFundOrder(TakerFundInputs { + filler_token: None, + collateral_token: None, + }), + TransactionInputs::TakerTerminationEarly { .. } => { + TransactionInputsOption::TakerTerminationEarly(TakerTerminationEarlyInputs { + filler_token: None, + collateral_token: None, + }) + } + TransactionInputs::TakerSettlement { .. } => { + TransactionInputsOption::TakerSettlement(TakerSettlementInputs { + filler_token: None, + asset_token: None, + }) + } + TransactionInputs::MakerTerminationCollateral { .. } => { + TransactionInputsOption::MakerTerminationCollateral(MakerTerminationCollateralInputs { + collateral_token_utxo: None, + grantor_collateral_token_utxo: None, + }) + } + TransactionInputs::MakerTerminationSettlement { .. } => { + TransactionInputsOption::MakerTerminationSettlement(MakerTerminationSettlmentInputs { + settlement_asset_utxo: None, + grantor_settlement_token_utxo: None, + }) + } + TransactionInputs::MakerSettlement { .. } => { + TransactionInputsOption::MakerSettlement(MakerSettlementInputs { + asset_utxo: None, + grantor_collateral_token_utxo: None, + grantor_settlement_token_utxo: None, + }) + } + }; + Ok(res) + } +} + +impl CoinSelector for T {} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode, PartialEq)] +pub struct DcdContractTokenEntropies { + pub filler_token_entropy: AssetEntropyHex, + pub grantor_collateral_token_entropy: AssetEntropyHex, + pub grantor_settlement_token_entropy: AssetEntropyHex, +} + +#[expect(dead_code)] +#[derive(Clone, Copy, Debug)] +pub struct GetSettlementFilter { + /// Flag used to correctly choose between Collateral and Settlement tokens + price_go_higher: bool, +} + +#[derive(Debug, Clone)] +pub struct OutPointInfo { + pub outpoint: OutPoint, + pub owner_script_pubkey: String, + pub asset_id: String, + pub spent: bool, +} + +#[derive(Debug, Clone)] +pub struct OutPointInfoRaw { + pub id: u64, + pub outpoint: OutPoint, + pub owner_script_pubkey: String, + pub asset_id: String, + pub spent: bool, +} + +#[derive(Debug, Clone)] +pub struct GetTokenFilter { + /// Token asset id to look for + pub asset_id: Option, + /// Whether transaction is spent or not according to db or not + pub spent: Option, + /// Owner of token + pub owner: Option, +} + +#[expect(dead_code)] +#[derive(Debug, Clone)] +pub struct TakerFundInputs { + filler_token: Option, + collateral_token: Option, +} + +#[expect(dead_code)] +#[derive(Debug, Clone)] +pub struct TakerTerminationEarlyInputs { + filler_token: Option, + collateral_token: Option, +} + +#[expect(dead_code)] +#[derive(Debug, Clone)] +pub struct TakerSettlementInputs { + filler_token: Option, + asset_token: Option, +} + +#[expect(dead_code)] +#[derive(Debug, Clone)] +pub struct MakerFundInputs { + filler_reissuance_tx: Option, + grantor_collateral_reissuance_tx: Option, + grantor_settlement_reissuance_tx: Option, + asset_settlement_tx: Option, +} + +#[expect(dead_code)] +#[derive(Debug, Clone)] +pub struct MakerTerminationCollateralInputs { + collateral_token_utxo: Option, + grantor_collateral_token_utxo: Option, +} + +#[expect(dead_code)] +#[derive(Debug, Clone)] +pub struct MakerTerminationSettlmentInputs { + settlement_asset_utxo: Option, + grantor_settlement_token_utxo: Option, +} + +#[expect(dead_code)] +#[derive(Debug, Clone)] +pub struct MakerSettlementInputs { + asset_utxo: Option, + grantor_collateral_token_utxo: Option, + grantor_settlement_token_utxo: Option, +} + +impl GetTokenFilter { + pub fn get_sql_filter(&self) -> String { + let mut query = String::new(); + + if self.asset_id.is_some() { + query.push_str(" AND asset_id = ?"); + } + if self.spent.is_some() { + query.push_str(" AND spent = ?"); + } + if self.owner.is_some() { + query.push_str(" AND owner_script_pubkey = ?"); + } + + if !query.is_empty() { + let substr = query.trim_start_matches(" AND"); + let substr = substr.trim(); + query = format!(" WHERE ({})", substr); + } + query + } +} diff --git a/crates/coin-selection/src/utils.rs b/crates/coin-selection/src/utils.rs new file mode 100644 index 0000000..151f9a0 --- /dev/null +++ b/crates/coin-selection/src/utils.rs @@ -0,0 +1,43 @@ +use crate::types::OutPointInfo; +use reqwest::Client; +use simplicity::elements::hex::ToHex; +use simplicityhl::elements::{TxIn, Txid}; +use simplicityhl::simplicity::elements::{OutPoint, Transaction, TxOut, encode}; +use std::time::Duration; + +const BASE_URL: &str = "https://blockstream.info/liquidtestnet"; + +// TODO: reuse from simplicity-core +pub async fn fetch_tx(tx_id: Txid) -> anyhow::Result { + let url = format!("{BASE_URL}/api/tx/{}/hex", tx_id); + + let client = Client::builder().timeout(Duration::from_secs(10)).build()?; + + let tx_hex = client.get(&url).send().await?.error_for_status()?.text().await?; + let tx_bytes = hex::decode(tx_hex.trim())?; + let transaction: Transaction = encode::deserialize(&tx_bytes)?; + Ok(transaction) +} + +pub async fn extract_outpoint_info_from_tx_in(tx_in: TxIn) -> anyhow::Result { + let outpoint = tx_in.previous_output; + let tx = fetch_tx(outpoint.txid).await?; + let tx_out = tx.output[outpoint.vout as usize].clone(); + + let info = OutPointInfo { + outpoint, + owner_script_pubkey: tx_out.script_pubkey.to_hex(), + asset_id: tx_out.asset.to_string(), + spent: true, + }; + Ok(info) +} + +pub async fn extract_outpoint_info_from_tx_out(outpoint: OutPoint, tx_out: TxOut) -> anyhow::Result { + Ok(OutPointInfo { + outpoint, + owner_script_pubkey: tx_out.script_pubkey.to_hex(), + asset_id: tx_out.asset.to_string(), + spent: false, + }) +} diff --git a/crates/coin-selection/tests/fixtures/default_outpoints.sql b/crates/coin-selection/tests/fixtures/default_outpoints.sql new file mode 100644 index 0000000..7e4fd57 --- /dev/null +++ b/crates/coin-selection/tests/fixtures/default_outpoints.sql @@ -0,0 +1,86 @@ +-- tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm maker +-- tex1p3tzxsj4cs64a6qwpcc68aev4xx38mcqmrya9r3587jy49sk40z3qk6d9el taker +-- tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n dcd + +-- FILLER_ASSET_ID +insert into outpoints (vout, owner_script_pubkey, asset_id, spent, tx_id) +values (1, 'tex1p3tzxsj4cs64a6qwpcc68aev4xx38mcqmrya9r3587jy49sk40z3qk6d9el', + '2c3aa8ae0e199f9609e2e4b60a97a1f4b52c5d76d916b0a51e18ecded3d057b1', true, + '1b993898b41c31cd88781e68ab9b2f6856c1c7d68921c74ef347412feac8ad6c'), + (0, 'tex1p3tzxsj4cs64a6qwpcc68aev4xx38mcqmrya9r3587jy49sk40z3qk6d9el', + '2c3aa8ae0e199f9609e2e4b60a97a1f4b52c5d76d916b0a51e18ecded3d057b1', false, + '929d5526c41712d5cdf7b7406f645df229a62d24a1c76f63201d8e896da389ce'), + (5, 'tex1p3tzxsj4cs64a6qwpcc68aev4xx38mcqmrya9r3587jy49sk40z3qk6d9el', + '2c3aa8ae0e199f9609e2e4b60a97a1f4b52c5d76d916b0a51e18ecded3d057b1', true, + 'cd1e4aa43251fa1ebbf32e8f9b2e66358b746a32d6bcc0420a0a2e24e0393f4e'), + (5, 'tex1p3tzxsj4cs64a6qwpcc68aev4xx38mcqmrya9r3587jy49sk40z3qk6d9el', + '2c3aa8ae0e199f9609e2e4b60a97a1f4b52c5d76d916b0a51e18ecded3d057b1', false, + '403c9bca043cbfb692bfad8ff7ea09634a838ae833f9a62aa043d2ffa4458387') +on conflict DO NOTHING; + +-- GRANTOR_COLLATERAL_ASSET_ID +insert into outpoints (vout, owner_script_pubkey, asset_id, spent, tx_id) +values (6, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', + 'ba817efa46ffb5dd5b985d2c6657376ceaf748eedfda3f88e273260c18538d73', true, + 'cd1e4aa43251fa1ebbf32e8f9b2e66358b746a32d6bcc0420a0a2e24e0393f4e'), + (6, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', + 'ba817efa46ffb5dd5b985d2c6657376ceaf748eedfda3f88e273260c18538d73', false, + '403c9bca043cbfb692bfad8ff7ea09634a838ae833f9a62aa043d2ffa4458387'), + (1, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', + 'ba817efa46ffb5dd5b985d2c6657376ceaf748eedfda3f88e273260c18538d73', false, + '1eb9bed5e3954d0556de572ea12c73d6b4d7f62a4d11646cf1a07d943c2cb50e'), + (6, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', + 'ba817efa46ffb5dd5b985d2c6657376ceaf748eedfda3f88e273260c18538d73', true, + 'e9fdd8eb41f7a87f101d9a2ae38a4b8584c892d9b7f22896696c79e805a30e95'), + (1, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', + 'ba817efa46ffb5dd5b985d2c6657376ceaf748eedfda3f88e273260c18538d73', false, + '929d5526c41712d5cdf7b7406f645df229a62d24a1c76f63201d8e896da389ce') +on conflict DO NOTHING; + +-- GRANTOR_SETTLEMENT_ASSET_ID +insert into outpoints (vout, owner_script_pubkey, asset_id, spent, tx_id) +values (7, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', + '82b7bba397cafbf1918cc8fee11aa636eba97ee4c88a6efe954b90e8a85806ea', false, + 'cd1e4aa43251fa1ebbf32e8f9b2e66358b746a32d6bcc0420a0a2e24e0393f4e'), + (7, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', + '82b7bba397cafbf1918cc8fee11aa636eba97ee4c88a6efe954b90e8a85806ea', true, + '403c9bca043cbfb692bfad8ff7ea09634a838ae833f9a62aa043d2ffa4458387'), + (1, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', + '82b7bba397cafbf1918cc8fee11aa636eba97ee4c88a6efe954b90e8a85806ea', true, + '1eb9bed5e3954d0556de572ea12c73d6b4d7f62a4d11646cf1a07d943c2cb50e'), + (7, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', + '82b7bba397cafbf1918cc8fee11aa636eba97ee4c88a6efe954b90e8a85806ea', false, + 'e9fdd8eb41f7a87f101d9a2ae38a4b8584c892d9b7f22896696c79e805a30e95'), + (1, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', + '82b7bba397cafbf1918cc8fee11aa636eba97ee4c88a6efe954b90e8a85806ea', true, + '929d5526c41712d5cdf7b7406f645df229a62d24a1c76f63201d8e896da389ce') +on conflict DO NOTHING; + +-- SETTLEMENT_ASSET_ID +insert into outpoints (vout, owner_script_pubkey, asset_id, spent, tx_id) +values (4, 'tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n', + '420561859e4217f0def578911bbf68d7d3f75d664b978de39083269994eecd4b', false, + '403c9bca043cbfb692bfad8ff7ea09634a838ae833f9a62aa043d2ffa4458387'), + (1, 'tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n', + '420561859e4217f0def578911bbf68d7d3f75d664b978de39083269994eecd4b', true, + '1f3b3199bc5da2991d47fc9f30027393a09787308426a56abdcf336184213a22'), + (3, 'tex1p3tzxsj4cs64a6qwpcc68aev4xx38mcqmrya9r3587jy49sk40z3qk6d9el', + '420561859e4217f0def578911bbf68d7d3f75d664b978de39083269994eecd4b', false, + '2655da67204d0c34b936b4a394e63ab84da421772d1e7779c1e257f7acb32d9b') +on conflict DO NOTHING; + +-- COLLATERAL_ASSET_ID +insert into outpoints (vout, owner_script_pubkey, asset_id, spent, tx_id) +values (0, 'tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n', + '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49', false, + '1eb9bed5e3954d0556de572ea12c73d6b4d7f62a4d11646cf1a07d943c2cb50e'), + (3, 'tex1p3tzxsj4cs64a6qwpcc68aev4xx38mcqmrya9r3587jy49sk40z3qk6d9el', + '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49', true, + '2655da67204d0c34b936b4a394e63ab84da421772d1e7779c1e257f7acb32d9b'), + (2, 'tex1p3tzxsj4cs64a6qwpcc68aev4xx38mcqmrya9r3587jy49sk40z3qk6d9el', + '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49', false, + '2655da67204d0c34b936b4a394e63ab84da421772d1e7779c1e257f7acb32d9b'), + (2, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', + '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49', false, + '0abf93fe8ae69f790e898c7b0a1f1b2ce2eb5e059d9ef6550fbd04ca8becf55d') +on conflict DO NOTHING; diff --git a/crates/coin-selection/tests/testing_sqlite_db.rs b/crates/coin-selection/tests/testing_sqlite_db.rs new file mode 100644 index 0000000..74f0103 --- /dev/null +++ b/crates/coin-selection/tests/testing_sqlite_db.rs @@ -0,0 +1,498 @@ +mod utils; + +#[cfg(test)] +mod tests { + use coin_selection::sqlite_db::SqliteRepo; + use coin_selection::types::{CoinSelectionStorage, GetTokenFilter, OutPointInfo}; + use simplicityhl::elements::{OutPoint, Txid}; + use sqlx::SqlitePool; + use std::str::FromStr; + + mod init { + use super::*; + #[tokio::test] + async fn test_sqlite_db_init_with_url() -> anyhow::Result<()> { + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join("test_coin_selection_with_url.db"); + let _ = std::fs::remove_file(&db_path); + + let db_url = format!("sqlite://{}", db_path.display()); + let db = SqliteRepo::from_url(&db_url).await?; + + assert!(db_path.exists()); + assert!(db.healthcheck().await.is_ok()); + + let _ = std::fs::remove_file(&db_path); + Ok(()) + } + + #[tokio::test] + async fn test_sqlite_db_init_default() -> anyhow::Result<()> { + let db = SqliteRepo::new().await?; + assert!(db.healthcheck().await.is_ok()); + Ok(()) + } + + #[sqlx::test] + async fn test_database_migration_runs(pool: SqlitePool) -> anyhow::Result<()> { + let db = SqliteRepo::from_pool(pool).await?; + assert!(db.healthcheck().await.is_ok()); + Ok(()) + } + } + + mod db_token_logic { + use super::*; + use crate::utils::TEST_LOGGER; + + const MAKER_TOKEN_ADDRESS: &str = "tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm"; + const TAKER_TOKEN_ADDRESS: &str = "tex1p3tzxsj4cs64a6qwpcc68aev4xx38mcqmrya9r3587jy49sk40z3qk6d9el"; + const DCD_TOKEN_ADDRESS: &str = "tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n"; + + const FILLER_ASSET_ID: &str = "2c3aa8ae0e199f9609e2e4b60a97a1f4b52c5d76d916b0a51e18ecded3d057b1"; + const GRANTOR_COLLATERAL_ASSET_ID: &str = "ba817efa46ffb5dd5b985d2c6657376ceaf748eedfda3f88e273260c18538d73"; + const GRANTOR_SETTLEMENT_ASSET_ID: &str = "82b7bba397cafbf1918cc8fee11aa636eba97ee4c88a6efe954b90e8a85806ea"; + const SETTLEMENT_ASSET_ID: &str = "420561859e4217f0def578911bbf68d7d3f75d664b978de39083269994eecd4b"; + const COLLATERAL_ASSET_ID: &str = "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; + + type TestResult = anyhow::Result<()>; + + fn txid(s: &str) -> Txid { + Txid::from_str(s).expect("valid txid") + } + + #[sqlx::test(fixtures("default_outpoints"))] + async fn test_get_unspent_filler_for_taker_with_fixture(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let filter = GetTokenFilter { + asset_id: Some(FILLER_ASSET_ID.to_string()), + owner: Some(TAKER_TOKEN_ADDRESS.to_string()), + spent: Some(false), + }; + + let outpoints = repo.get_token_outpoints(filter).await?; + assert_eq!(outpoints.len(), 2); + assert!( + outpoints + .iter() + .all(|op| op.owner_script_pubkey == TAKER_TOKEN_ADDRESS && op.asset_id == FILLER_ASSET_ID) + ); + + Ok(()) + } + + #[sqlx::test(fixtures("default_outpoints"))] + async fn test_get_all_maker_grantor_collateral(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + let repo = SqliteRepo::from_pool(pool).await?; + + let filter = GetTokenFilter { + asset_id: Some(GRANTOR_COLLATERAL_ASSET_ID.to_string()), + owner: Some(MAKER_TOKEN_ADDRESS.to_string()), + spent: None, + }; + println!("{filter:#?}"); + let outpoints = repo.get_token_outpoints(filter).await?; + println!("outpoints: {:#?}", outpoints); + assert_eq!(outpoints.len(), 5); + assert!(outpoints.iter().all(|op| { + op.owner_script_pubkey == MAKER_TOKEN_ADDRESS && op.asset_id == GRANTOR_COLLATERAL_ASSET_ID + })); + + Ok(()) + } + + #[sqlx::test(fixtures("default_outpoints"))] + async fn test_get_only_unspent_collateral_for_dcd(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let filter = GetTokenFilter { + asset_id: Some(COLLATERAL_ASSET_ID.to_string()), + owner: Some(DCD_TOKEN_ADDRESS.to_string()), + spent: Some(false), + }; + + let outpoints = repo.get_token_outpoints(filter).await?; + assert_eq!(outpoints.len(), 1); + assert_eq!(outpoints[0].outpoint.vout, 0); + assert_eq!(outpoints[0].owner_script_pubkey, DCD_TOKEN_ADDRESS); + assert_eq!(outpoints[0].asset_id, COLLATERAL_ASSET_ID); + + Ok(()) + } + + #[sqlx::test(fixtures("default_outpoints"))] + async fn test_mark_outpoints_spent_with_fixture(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let filter = GetTokenFilter { + asset_id: Some(GRANTOR_COLLATERAL_ASSET_ID.to_string()), + owner: Some(MAKER_TOKEN_ADDRESS.to_string()), + spent: Some(false), + }; + let before = repo.get_token_outpoints(filter.clone()).await?; + assert!(!before.is_empty()); + assert!(before.iter().all(|op| { + op.owner_script_pubkey == MAKER_TOKEN_ADDRESS && op.asset_id == GRANTOR_COLLATERAL_ASSET_ID + })); + + repo.mark_outpoints_spent(&before.iter().map(|x| x.outpoint).collect::>()) + .await?; + + let after = repo.get_token_outpoints(filter).await?; + assert!(after.is_empty()); + + Ok(()) + } + + #[sqlx::test(fixtures("default_outpoints"))] + async fn test_combined_filters_owner_and_asset(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let filter = GetTokenFilter { + asset_id: Some(SETTLEMENT_ASSET_ID.to_string()), + owner: Some(TAKER_TOKEN_ADDRESS.to_string()), + spent: Some(false), + }; + + let outpoints = repo.get_token_outpoints(filter).await?; + assert_eq!(outpoints.len(), 1); + // assert_eq!(outpoints[0].vout, 3); + assert_eq!(outpoints[0].owner_script_pubkey, TAKER_TOKEN_ADDRESS); + assert_eq!(outpoints[0].asset_id, SETTLEMENT_ASSET_ID); + + Ok(()) + } + + #[sqlx::test(fixtures())] + async fn test_get_token_outpoints_empty_db(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let filter = GetTokenFilter { + asset_id: Some(FILLER_ASSET_ID.to_string()), + owner: Some(TAKER_TOKEN_ADDRESS.to_string()), + spent: Some(false), + }; + + let outpoints = repo.get_token_outpoints(filter).await?; + assert!(outpoints.is_empty()); + + Ok(()) + } + + #[sqlx::test(fixtures())] + async fn test_add_and_get_single_outpoint_empty_db(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let op = OutPoint { + txid: txid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + vout: 0, + }; + + let info = OutPointInfo { + outpoint: op, + owner_script_pubkey: MAKER_TOKEN_ADDRESS.to_string(), + asset_id: FILLER_ASSET_ID.to_string(), + spent: false, + }; + + repo.add_outpoint(info).await?; + + let filter = GetTokenFilter { + asset_id: Some(FILLER_ASSET_ID.to_string()), + owner: Some(MAKER_TOKEN_ADDRESS.to_string()), + spent: Some(false), + }; + let outpoints = repo.get_token_outpoints(filter).await?; + + assert_eq!(outpoints.len(), 1); + assert_eq!(outpoints[0].outpoint, op); + assert_eq!(outpoints[0].owner_script_pubkey, MAKER_TOKEN_ADDRESS); + assert_eq!(outpoints[0].asset_id, FILLER_ASSET_ID); + + Ok(()) + } + + #[sqlx::test(fixtures())] + async fn test_add_multiple_assets_and_filter_empty_db(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let ops = [ + ( + "1111111111111111111111111111111111111111111111111111111111111111", + 0u32, + MAKER_TOKEN_ADDRESS, + SETTLEMENT_ASSET_ID, + ), + ( + "2222222222222222222222222222222222222222222222222222222222222222", + 1u32, + TAKER_TOKEN_ADDRESS, + COLLATERAL_ASSET_ID, + ), + ( + "3333333333333333333333333333333333333333333333333333333333333333", + 2u32, + DCD_TOKEN_ADDRESS, + GRANTOR_SETTLEMENT_ASSET_ID, + ), + ]; + + for (tx, vout, owner, asset) in ops { + let info = OutPointInfo { + outpoint: OutPoint { txid: txid(tx), vout }, + owner_script_pubkey: owner.to_string(), + asset_id: asset.to_string(), + spent: false, + }; + repo.add_outpoint(info).await?; + } + + let filter1 = GetTokenFilter { + asset_id: Some(SETTLEMENT_ASSET_ID.to_string()), + owner: Some(MAKER_TOKEN_ADDRESS.to_string()), + spent: Some(false), + }; + let r1 = repo.get_token_outpoints(filter1).await?; + assert_eq!(r1.len(), 1); + assert_eq!(r1[0].owner_script_pubkey, MAKER_TOKEN_ADDRESS); + assert_eq!(r1[0].asset_id, SETTLEMENT_ASSET_ID); + + let filter2 = GetTokenFilter { + asset_id: Some(COLLATERAL_ASSET_ID.to_string()), + owner: Some(TAKER_TOKEN_ADDRESS.to_string()), + spent: Some(false), + }; + let r2 = repo.get_token_outpoints(filter2).await?; + assert_eq!(r2.len(), 1); + assert_eq!(r2[0].owner_script_pubkey, TAKER_TOKEN_ADDRESS); + assert_eq!(r2[0].asset_id, COLLATERAL_ASSET_ID); + + let filter3 = GetTokenFilter { + asset_id: Some(GRANTOR_SETTLEMENT_ASSET_ID.to_string()), + owner: Some(DCD_TOKEN_ADDRESS.to_string()), + spent: Some(false), + }; + let r3 = repo.get_token_outpoints(filter3).await?; + assert_eq!(r3.len(), 1); + assert_eq!(r3[0].owner_script_pubkey, DCD_TOKEN_ADDRESS); + assert_eq!(r3[0].asset_id, GRANTOR_SETTLEMENT_ASSET_ID); + + Ok(()) + } + + #[sqlx::test(fixtures())] + async fn test_mark_outpoints_spent_on_added_rows(pool: SqlitePool) -> TestResult { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let op = OutPoint { + txid: txid("4444444444444444444444444444444444444444444444444444444444444444"), + vout: 0, + }; + + let info = OutPointInfo { + outpoint: op, + owner_script_pubkey: TAKER_TOKEN_ADDRESS.to_string(), + asset_id: FILLER_ASSET_ID.to_string(), + spent: false, + }; + repo.add_outpoint(info).await?; + + let filter_unspent = GetTokenFilter { + asset_id: Some(FILLER_ASSET_ID.to_string()), + owner: Some(TAKER_TOKEN_ADDRESS.to_string()), + spent: Some(false), + }; + let unspent_before = repo.get_token_outpoints(filter_unspent.clone()).await?; + assert_eq!(unspent_before.len(), 1); + assert!( + unspent_before + .iter() + .all(|p| { p.owner_script_pubkey == TAKER_TOKEN_ADDRESS && p.asset_id == FILLER_ASSET_ID }) + ); + + repo.mark_outpoints_spent(&unspent_before.iter().map(|x| x.outpoint).collect::>()) + .await?; + + let unspent_after = repo.get_token_outpoints(filter_unspent).await?; + assert!(unspent_after.is_empty()); + + let filter_spent = GetTokenFilter { + asset_id: Some(FILLER_ASSET_ID.to_string()), + owner: Some(TAKER_TOKEN_ADDRESS.to_string()), + spent: Some(true), + }; + let spent_after = repo.get_token_outpoints(filter_spent).await?; + assert_eq!(spent_after.len(), 1); + assert_eq!(spent_after[0].owner_script_pubkey, TAKER_TOKEN_ADDRESS); + assert_eq!(spent_after[0].asset_id, FILLER_ASSET_ID); + + Ok(()) + } + } + + mod db_dcd_contract_logic { + use super::*; + use crate::utils::TEST_LOGGER; + use coin_selection::types::DcdParamsStorage; + use contracts::DCDArguments; + + const TAPROOT_PUBKEY_GEN: &str = "87259fcc2da8a92273f0a3305d9a706062b3d1377dd774739e16f7a4f0eae990:027aed3517dd4c6e3cea2d67c16648a9284afc1b154f17fead32152889def8ca3d:tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n"; + const MISSING_TAPROOT_PUBKEY_GEN: &str = "062b3d1377dd774739e16f7a4f0eae99087259fcc2da8a92273f0a3305d9a706:027aed3517dd4c6e3cea2d67c16648a9284afc1b154f17fead32152889def8ca3d:tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n"; + #[sqlx::test(fixtures())] + async fn test_insert_and_get_dcd_contract_with_fixture(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let params = DCDArguments::default(); + repo.add_dcd_params(TAPROOT_PUBKEY_GEN, ¶ms).await?; + + let loaded = repo + .get_dcd_params(TAPROOT_PUBKEY_GEN) + .await? + .expect("dcd params must exist"); + assert_eq!(loaded, params); + + Ok(()) + } + + #[sqlx::test(fixtures())] + async fn test_insert_and_get_dcd_contract_empty_db(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let params = DCDArguments::default(); + repo.add_dcd_params(TAPROOT_PUBKEY_GEN, ¶ms).await?; + + let loaded = repo + .get_dcd_params(TAPROOT_PUBKEY_GEN) + .await? + .expect("dcd params must exist"); + assert_eq!(loaded, params); + + let missing = repo.get_dcd_params(MISSING_TAPROOT_PUBKEY_GEN).await?; + assert!(missing.is_none()); + + Ok(()) + } + } + + mod db_entropies_logic { + use super::*; + use crate::utils::TEST_LOGGER; + use coin_selection::types::{DcdContractTokenEntropies, EntropyStorage}; + + const TAPROOT_PUBKEY_GEN_1: &str = "87259fcc2da8a92273f0a3305d9a706062b3d1377dd774739e16f7a4f0eae990:027aed3517dd4c6e3cea2d67c16648a9284afc1b154f17fead32152889def8ca3d:tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n"; + const TAPROOT_PUBKEY_GEN_2: &str = "062b3d1377dd774739e16f7a4f0eae99087259fcc2da8a92273f0a3305d9a706:027aed3517dd4c6e3cea2d67c16648a9284afc1b154f17fead32152889def8ca3d:tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n"; + const FILLER_TOKEN_ENTROPY_1: &str = "b958d36669a09ad9fe04dfd874d79d2fc99353602d0579f2675b73741659956e"; + const GRANTOR_COLLATERAL_TOKEN_ENTROPY_1: &str = + "74d79d2fc99353602d0579f2675b73741659956eb958d36669a09ad9fe04dfd8"; + const GRANTOR_SETTLEMENT_TOKEN_ENTROPY_1: &str = + "675b73741659956eb958d36669a09ad9fe04dfd874d79d2fc99353602d0579f2"; + const FILLER_TOKEN_ENTROPY_2: &str = "b958d36669a09ad9fe04dfd874d79d2fc99353602d0579f2675b73741659956e"; + const GRANTOR_COLLATERAL_TOKEN_ENTROPY_2: &str = + "74d79d2fc99353602d0579f2675b73741659956eb958d36669a09ad9fe04dfd8"; + const GRANTOR_SETTLEMENT_TOKEN_ENTROPY_2: &str = + "675b73741659956eb958d36669a09ad9fe04dfd874d79d2fc99353602d0579f2"; + + #[sqlx::test(fixtures())] + async fn test_entropies_insertion_and_retrieval_with_fixture(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let dcd_entropies_1 = DcdContractTokenEntropies { + filler_token_entropy: FILLER_TOKEN_ENTROPY_1.to_string(), + grantor_collateral_token_entropy: GRANTOR_COLLATERAL_TOKEN_ENTROPY_1.to_string(), + grantor_settlement_token_entropy: GRANTOR_SETTLEMENT_TOKEN_ENTROPY_1.to_string(), + }; + let dcd_entropies_2 = DcdContractTokenEntropies { + filler_token_entropy: FILLER_TOKEN_ENTROPY_2.to_string(), + grantor_collateral_token_entropy: GRANTOR_COLLATERAL_TOKEN_ENTROPY_2.to_string(), + grantor_settlement_token_entropy: GRANTOR_SETTLEMENT_TOKEN_ENTROPY_2.to_string(), + }; + + repo.add_dcd_contract_token_entropies(TAPROOT_PUBKEY_GEN_1, dcd_entropies_1.clone()) + .await?; + repo.add_dcd_contract_token_entropies(TAPROOT_PUBKEY_GEN_2, dcd_entropies_2.clone()) + .await?; + + let loaded_1 = repo + .get_dcd_contract_token_entropies(TAPROOT_PUBKEY_GEN_1) + .await? + .expect("entropy for TAPROOT_PUBKEY_GEN_1 must exist"); + let loaded_2 = repo + .get_dcd_contract_token_entropies(TAPROOT_PUBKEY_GEN_2) + .await? + .expect("entropy for TAPROOT_PUBKEY_GEN_2 must exist"); + + assert_eq!(loaded_1, dcd_entropies_1); + assert_eq!(loaded_2, dcd_entropies_2); + + Ok(()) + } + + #[sqlx::test(fixtures())] + async fn test_entropies_insertion_and_retrieval_empty_db(pool: SqlitePool) -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let _guard = &*TEST_LOGGER; + + let repo = SqliteRepo::from_pool(pool).await?; + + let dcd_entropies_1 = DcdContractTokenEntropies { + filler_token_entropy: FILLER_TOKEN_ENTROPY_1.to_string(), + grantor_collateral_token_entropy: GRANTOR_COLLATERAL_TOKEN_ENTROPY_1.to_string(), + grantor_settlement_token_entropy: GRANTOR_SETTLEMENT_TOKEN_ENTROPY_1.to_string(), + }; + + repo.add_dcd_contract_token_entropies(TAPROOT_PUBKEY_GEN_1, dcd_entropies_1.clone()) + .await?; + + let loaded_1 = repo + .get_dcd_contract_token_entropies(TAPROOT_PUBKEY_GEN_1) + .await? + .expect("entropy for TAPROOT_PUBKEY_GEN_1 must exist"); + assert_eq!(loaded_1, dcd_entropies_1); + + let missing = repo.get_dcd_contract_token_entropies(TAPROOT_PUBKEY_GEN_2).await?; + assert!(missing.is_none()); + + Ok(()) + } + } + + mod db_coin_selection { + // TODO + } +} diff --git a/crates/coin-selection/tests/utils.rs b/crates/coin-selection/tests/utils.rs new file mode 100644 index 0000000..5f1e86a --- /dev/null +++ b/crates/coin-selection/tests/utils.rs @@ -0,0 +1,4 @@ +use global_utils::logger::{LoggerGuard, init_logger}; +use std::sync::LazyLock; + +pub static TEST_LOGGER: LazyLock = LazyLock::new(init_logger); diff --git a/crates/dex-cli/Cargo.toml b/crates/dex-cli/Cargo.toml index f46104d..ff487c6 100644 --- a/crates/dex-cli/Cargo.toml +++ b/crates/dex-cli/Cargo.toml @@ -19,6 +19,7 @@ clap = { workspace = true, features = ["env"] } config = { workspace = true } contracts = { workspace = true } contracts-adapter = { workspace = true } +coin-selection = { workspace = true } dex-nostr-relay = { workspace = true } dotenvy = { workspace = true } elements = { workspace = true } diff --git a/crates/dex-cli/src/cli/helper.rs b/crates/dex-cli/src/cli/helper.rs index 0076b58..4bae4e8 100644 --- a/crates/dex-cli/src/cli/helper.rs +++ b/crates/dex-cli/src/cli/helper.rs @@ -2,6 +2,7 @@ use crate::cli::CommonOrderOptions; use clap::Subcommand; use nostr::EventId; use simplicity::elements::OutPoint; +use simplicityhl::elements::Txid; #[derive(Debug, Subcommand)] pub enum HelperCommands { @@ -141,4 +142,9 @@ pub enum HelperCommands { #[command(flatten)] common_options: CommonOrderOptions, }, + #[command(about = "Merge 4 token UTXOs into 1")] + AddOutsToSqliteCache { + #[arg(long = "tx-id")] + tx_id: Txid, + }, } diff --git a/crates/dex-cli/src/cli/processor.rs b/crates/dex-cli/src/cli/processor.rs index b645cc8..08d6bbb 100644 --- a/crates/dex-cli/src/cli/processor.rs +++ b/crates/dex-cli/src/cli/processor.rs @@ -3,7 +3,10 @@ use crate::cli::{DexCommands, MakerCommands, TakerCommands}; use crate::common::config::AggregatedConfig; use crate::common::{DEFAULT_CLIENT_TIMEOUT_SECS, InitOrderArgs, write_into_stdout}; use crate::contract_handlers; +use crate::error::CliError; use clap::{Parser, Subcommand}; +use coin_selection::sqlite_db::SqliteRepo; +use coin_selection::types::{CoinSelector, DcdParamsStorage, TransactionOption}; use dex_nostr_relay::relay_client::ClientConfig; use dex_nostr_relay::relay_processor::{ListOrdersEventFilter, RelayProcessor}; use dex_nostr_relay::types::ReplyOption; @@ -30,6 +33,10 @@ pub struct Cli { #[arg(short = 'c', long, default_value = DEFAULT_CONFIG_PATH, env = "DEX_NOSTR_CONFIG_PATH")] pub(crate) nostr_config_path: PathBuf, + /// Path to a config file containing the list of relays and(or) nostr keypair to use + #[arg(short = 's', long, env = "DEX_SQLITE_URL")] + pub(crate) sqlite_url: Option, + /// Command to execute #[command(subcommand)] command: Command, @@ -205,6 +212,13 @@ impl Cli { pub async fn process(self) -> crate::error::Result<()> { let agg_config = self.init_config()?; + // TODO: add generic type for sqlite storage + let sqlite_cache = match self.sqlite_url.as_ref() { + None => SqliteRepo::new().await, + Some(url) => SqliteRepo::from_url(&url).await, + } + .map_err(|err| crate::error::CliError::SqliteCache(err.to_string()))?; + let relay_processor = self .init_relays(&agg_config.relays, agg_config.nostr_keypair.clone()) .await?; @@ -218,9 +232,15 @@ impl Cli { Command::ShowConfig => { format!("Config: {:#?}", cli_app_context.agg_config) } - Command::Maker { action } => Self::process_maker_commands(&cli_app_context, action).await?, - Command::Taker { action } => Self::process_taker_commands(&cli_app_context, action).await?, - Command::Helpers { action } => Self::process_helper_commands(&cli_app_context, action).await?, + Command::Maker { action } => { + Self::process_maker_commands(&cli_app_context, action, &sqlite_cache).await? + } + Command::Taker { action } => { + Self::process_taker_commands(&cli_app_context, action, &sqlite_cache).await? + } + Command::Helpers { action } => { + Self::process_helper_commands(&cli_app_context, action, &sqlite_cache).await? + } Command::Dex { action } => Self::process_dex_commands(&cli_app_context, action).await?, } }; @@ -232,6 +252,7 @@ impl Cli { async fn process_maker_commands( cli_app_context: &CliAppContext, action: MakerCommands, + sqlite_cache: &SqliteRepo, ) -> crate::error::Result { Ok(match action { MakerCommands::InitOrder { @@ -276,6 +297,7 @@ impl Cli { dcd_taproot_pubkey_gen, }, common_options, + sqlite_cache, ) .await? } @@ -406,6 +428,7 @@ impl Cli { account_index, is_offline, }: CommonOrderOptions, + sqlite_repo: &SqliteRepo, ) -> crate::error::Result { use contract_handlers::maker_funding::{Utxos, handle, process_args, save_args_to_cache}; @@ -428,6 +451,25 @@ impl Cli { .await?; let res = relay_processor.place_order(event_to_publish, tx_id).await?; save_args_to_cache(&args_to_save)?; + + println!("[Pre defined msg Maker] Creating order, tx_id: {tx_id}, event_id: {res:#?}"); + + tracing::info!("Sleeping before writing into db"); + tokio::time::sleep(tokio::time::Duration::from_secs(30)).await; + + tracing::info!("Writing in db"); + sqlite_repo + .add_dcd_params( + &args_to_save.taproot_pubkey_gen.to_string(), + &args_to_save.dcd_arguments, + ) + .await + .map_err(|err| CliError::SqliteCache(err.to_string()))?; + sqlite_repo + .add_outputs(TransactionOption::MakerFund(tx_id)) + .await + .map_err(|err| CliError::SqliteCache(err.to_string()))?; + Ok(format!("[Maker] Creating order, tx_id: {tx_id}, event_id: {res:#?}")) } @@ -589,6 +631,7 @@ impl Cli { relay_processor, }: &CliAppContext, action: TakerCommands, + sqlite_repo: &SqliteRepo, ) -> crate::error::Result { Ok(match action { TakerCommands::FundOrder { @@ -599,9 +642,10 @@ impl Cli { common_options, maker_order_event_id, } => { - use contract_handlers::taker_funding::{Utxos, handle, process_args, save_args_to_cache}; + use contract_handlers::taker_funding::{handle, merge_utxos, process_args, save_args_to_cache}; agg_config.check_nostr_keypair_existence()?; + let processed_args = process_args( common_options.account_index, collateral_amount_to_deposit, @@ -609,16 +653,19 @@ impl Cli { relay_processor, ) .await?; - let (tx_id, args_to_save) = handle( - processed_args, - Utxos { - filler_token_utxo, - collateral_token_utxo, - }, - fee_amount, - common_options.is_offline, + + let utxos = merge_utxos( + sqlite_repo, + &processed_args.dcd_taproot_pubkey_gen, + filler_token_utxo, + collateral_token_utxo, + &processed_args.keypair, ) .await?; + tracing::info!("Chosen utxos: {utxos:#?}"); + + let (tx_id, args_to_save) = + handle(processed_args, utxos, fee_amount, common_options.is_offline).await?; let reply_event_id = relay_processor .reply_order(maker_order_event_id, ReplyOption::TakerFund { tx_id }) .await?; @@ -708,6 +755,7 @@ impl Cli { async fn process_helper_commands( cli_app_context: &CliAppContext, action: HelperCommands, + sqlite_repo: &SqliteRepo, ) -> crate::error::Result { Ok(match action { HelperCommands::Faucet { @@ -823,6 +871,13 @@ impl Cli { ) .await? } + HelperCommands::AddOutsToSqliteCache { tx_id } => { + sqlite_repo + .add_outputs(TransactionOption::MakerFund(tx_id)) + .await + .map_err(|err| CliError::SqliteCache(err.to_string()))?; + format!("[Add outs] Successfully added outs to sqlite cache, tx_id: {tx_id}") + } }) } diff --git a/crates/dex-cli/src/cli/taker.rs b/crates/dex-cli/src/cli/taker.rs index 8ec53a8..916282a 100644 --- a/crates/dex-cli/src/cli/taker.rs +++ b/crates/dex-cli/src/cli/taker.rs @@ -12,10 +12,10 @@ pub enum TakerCommands { FundOrder { /// UTXO containing filler tokens provided by the Taker to fund the contract #[arg(long = "filler-utxo")] - filler_token_utxo: OutPoint, + filler_token_utxo: Option, /// UTXO containing collateral asset that the Taker locks into the DCD contract #[arg(long = "collateral-utxo")] - collateral_token_utxo: OutPoint, + collateral_token_utxo: Option, /// Miner fee in satoshis (LBTC) for the Taker funding transaction #[arg(long = "fee-amount", default_value_t = 1500)] fee_amount: u64, diff --git a/crates/dex-cli/src/common/config.rs b/crates/dex-cli/src/common/config.rs index 6b87e91..9dd760f 100644 --- a/crates/dex-cli/src/common/config.rs +++ b/crates/dex-cli/src/common/config.rs @@ -16,6 +16,7 @@ use tracing::instrument; pub struct AggregatedConfig { pub nostr_keypair: Option, pub relays: Vec, + pub sql_url: Option, } #[derive(Debug, Clone)] @@ -53,12 +54,14 @@ impl AggregatedConfig { pub struct AggregatedConfigInner { pub nostr_keypair: Option, pub relays: Option>, + pub sqlite_url: Option, } let Cli { nostr_key, relays_list, nostr_config_path, + sqlite_url, .. } = cli; @@ -74,6 +77,11 @@ impl AggregatedConfig { config_builder.set_override_option("nostr_keypair", Some(KeysWrapper(nostr_key.clone())))?; } + if let Some(sqlite_url) = sqlite_url { + tracing::debug!("Adding sqlite url value from CLI"); + config_builder = config_builder.set_override_option("sqlite_url", Some(sqlite_url.clone()))?; + } + if let Some(relays) = relays_list { tracing::debug!("Adding relays values from CLI, relays: '{:?}'", relays); config_builder = config_builder.set_override_option( @@ -107,6 +115,7 @@ impl AggregatedConfig { let aggregated_config = AggregatedConfig { nostr_keypair: config.nostr_keypair.map(|x| x.0), relays, + sql_url: config.sqlite_url, }; tracing::debug!("Config gathered: '{:?}'", aggregated_config); diff --git a/crates/dex-cli/src/contract_handlers/maker_funding.rs b/crates/dex-cli/src/contract_handlers/maker_funding.rs index 671b77f..2eb1f4c 100644 --- a/crates/dex-cli/src/contract_handlers/maker_funding.rs +++ b/crates/dex-cli/src/contract_handlers/maker_funding.rs @@ -30,8 +30,8 @@ pub struct ProcessedArgs { #[derive(Debug)] pub struct ArgsToSave { - taproot_pubkey_gen: TaprootPubkeyGen, - dcd_arguments: DCDArguments, + pub taproot_pubkey_gen: TaprootPubkeyGen, + pub dcd_arguments: DCDArguments, } #[derive(Debug)] diff --git a/crates/dex-cli/src/contract_handlers/taker_funding.rs b/crates/dex-cli/src/contract_handlers/taker_funding.rs index 997328e..d76c16e 100644 --- a/crates/dex-cli/src/contract_handlers/taker_funding.rs +++ b/crates/dex-cli/src/contract_handlers/taker_funding.rs @@ -3,36 +3,116 @@ use crate::common::settings::Settings; use crate::common::store::SledError; use crate::common::store::utils::OrderParams; use crate::contract_handlers::common::{broadcast_or_get_raw_tx, get_order_params}; +use crate::error::CliError; +use coin_selection::sqlite_db::SqliteRepo; +use coin_selection::types::{CoinSelectionStorage, DcdParamsStorage, GetTokenFilter, OutPointInfoRaw}; use contracts::DCDArguments; use contracts_adapter::dcd::{BaseContractContext, CommonContext, DcdContractContext, DcdManager, TakerFundingContext}; use dex_nostr_relay::relay_processor::RelayProcessor; use elements::bitcoin::secp256k1; +use elements::hex::ToHex; +use elements::schnorr::Keypair; use nostr::EventId; use simplicity::elements::OutPoint; use simplicityhl::elements::{AddressParams, Txid}; -use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, TaprootPubkeyGen}; +use simplicityhl_core::{LIQUID_TESTNET_BITCOIN_ASSET, LIQUID_TESTNET_GENESIS, TaprootPubkeyGen, get_p2pk_address}; use tokio::task; use tracing::instrument; #[derive(Debug)] pub struct ProcessedArgs { - keypair: secp256k1::Keypair, - dcd_arguments: DCDArguments, - dcd_taproot_pubkey_gen: String, - collateral_amount_to_deposit: u64, + pub keypair: secp256k1::Keypair, + pub dcd_arguments: DCDArguments, + pub dcd_taproot_pubkey_gen: String, + pub collateral_amount_to_deposit: u64, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ArgsToSave { taproot_pubkey_gen: TaprootPubkeyGen, dcd_arguments: DCDArguments, } +#[derive(Debug, Clone)] pub struct Utxos { pub filler_token_utxo: OutPoint, pub collateral_token_utxo: OutPoint, } +fn merge_outpoints_sources(cli: Option, mut cached: Vec) -> Option { + match cli { + None => { + if cached.is_empty() { + None + } else { + Some(cached.remove(0).outpoint) + } + } + Some(x) => Some(x), + } +} + +#[instrument(level = "debug", skip_all, err)] +pub async fn merge_utxos( + sqlite_cache: &SqliteRepo, + dcd_taproot_pubkey_gen: &str, + filler_token_utxo: Option, + collateral_token_utxo: Option, + keypair: &Keypair, +) -> crate::error::Result { + let dcd_arguments = sqlite_cache + .get_dcd_params(dcd_taproot_pubkey_gen) + .await + .map_err(|err| CliError::SqliteCache(err.to_string()))? + .unwrap(); + + let base_contract_context = BaseContractContext { + address_params: &AddressParams::LIQUID_TESTNET, + lbtc_asset: LIQUID_TESTNET_BITCOIN_ASSET, + genesis_block_hash: *LIQUID_TESTNET_GENESIS, + }; + let dcd_taproot_pubkey_gen = TaprootPubkeyGen::build_from_str( + &dcd_taproot_pubkey_gen, + &dcd_arguments, + base_contract_context.address_params, + &contracts::get_dcd_address, + ) + .map_err(|e| SledError::TapRootGen(e.to_string()))?; + + let change_recipient = get_p2pk_address(&keypair.x_only_public_key().0, &AddressParams::LIQUID_TESTNET).unwrap(); + + let filler_token_utxo = merge_outpoints_sources( + filler_token_utxo, + sqlite_cache + .get_token_outpoints(GetTokenFilter { + asset_id: Some(dcd_arguments.filler_token_asset_id_hex_le.clone()), + spent: Some(false), + owner: Some(dcd_taproot_pubkey_gen.address.script_pubkey().to_hex()), + }) + .await + .map_err(|err| CliError::SqliteCache(err.to_string()))?, + ); + + let collateral_token_utxo = merge_outpoints_sources( + collateral_token_utxo, + sqlite_cache + .get_token_outpoints(GetTokenFilter { + asset_id: Some(dcd_arguments.collateral_asset_id_hex_le.clone()), + spent: Some(false), + owner: Some(change_recipient.script_pubkey().to_hex()), + }) + .await + .map_err(|err| CliError::SqliteCache(err.to_string()))?, + ); + + Ok(Utxos { + filler_token_utxo: filler_token_utxo + .ok_or_else(|| CliError::Custom("Can't find filler_token_utxo, pass it with cli".to_string()))?, + collateral_token_utxo: collateral_token_utxo + .ok_or_else(|| CliError::Custom("Can't find collateral_token_utxo, pass it with cli".to_string()))?, + }) +} + #[instrument(level = "debug", skip_all, err)] pub async fn process_args( account_index: u32, diff --git a/crates/dex-cli/src/error.rs b/crates/dex-cli/src/error.rs index e5328eb..3e22896 100644 --- a/crates/dex-cli/src/error.rs +++ b/crates/dex-cli/src/error.rs @@ -48,6 +48,8 @@ pub enum CliError { NoNostrKeypairListed, #[error("Failed to join task, err: '{0}'")] TokioJoinError(#[from] JoinError), + #[error("Occurred error with SqLite cache, err: '{0}'")] + SqliteCache(String), #[error("Occurred error with msg: '{0}'")] Custom(String), } diff --git a/crates/global-utils/src/logger.rs b/crates/global-utils/src/logger.rs index ecd0604..2e5503b 100644 --- a/crates/global-utils/src/logger.rs +++ b/crates/global-utils/src/logger.rs @@ -4,7 +4,6 @@ use tracing::{level_filters::LevelFilter, trace}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt}; -const ENV_VAR_NAME: &str = "DEX_LOG"; const DEFAULT_LOG_DIRECTIVE: LevelFilter = LevelFilter::ERROR; #[derive(Debug)] @@ -23,7 +22,6 @@ pub fn init_logger() -> LoggerGuard { .with_filter( EnvFilter::builder() .with_default_directive(DEFAULT_LOG_DIRECTIVE.into()) - .with_env_var(ENV_VAR_NAME) .from_env_lossy(), );