From 72be78eb95dd2fb0f679df7881d9e288a3d75aa4 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Thu, 4 Dec 2025 12:32:14 +0200 Subject: [PATCH 1/6] Initialize traits for coin selector --- Cargo.toml | 2 + crates/coinselection/Cargo.toml | 19 ++ .../migrations/20251204084835_init.sql | 1 + crates/coinselection/src/lib.rs | 49 +++++ crates/coinselection/src/sqlite_db.rs | 40 ++++ crates/coinselection/src/types.rs | 180 ++++++++++++++++++ 6 files changed, 291 insertions(+) create mode 100644 crates/coinselection/Cargo.toml create mode 100644 crates/coinselection/migrations/20251204084835_init.sql create mode 100644 crates/coinselection/src/lib.rs create mode 100644 crates/coinselection/src/sqlite_db.rs create mode 100644 crates/coinselection/src/types.rs diff --git a/Cargo.toml b/Cargo.toml index 32937c6..8fea49f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ 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"] } @@ -37,6 +38,7 @@ 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/coinselection/Cargo.toml b/crates/coinselection/Cargo.toml new file mode 100644 index 0000000..ab38097 --- /dev/null +++ b/crates/coinselection/Cargo.toml @@ -0,0 +1,19 @@ +[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 } +simplicity-lang = { workspace = true } +simplicityhl = { workspace = true } +simplicityhl-core = { workspace = true } +sqlx = { workspace = true } +tokio = { workspace = true } \ No newline at end of file diff --git a/crates/coinselection/migrations/20251204084835_init.sql b/crates/coinselection/migrations/20251204084835_init.sql new file mode 100644 index 0000000..8ddc1d3 --- /dev/null +++ b/crates/coinselection/migrations/20251204084835_init.sql @@ -0,0 +1 @@ +-- Add migration script here diff --git a/crates/coinselection/src/lib.rs b/crates/coinselection/src/lib.rs new file mode 100644 index 0000000..765c581 --- /dev/null +++ b/crates/coinselection/src/lib.rs @@ -0,0 +1,49 @@ +pub mod sqlite_db; +pub mod types; + +#[cfg(test)] +mod tests { + use sqlx::{Row, Sqlite, SqlitePool, migrate::MigrateDatabase}; + const DB_URL: &str = "sqlite://sqlite.db"; + + #[tokio::test] + async fn it_works() { + if !Sqlite::database_exists(DB_URL).await.unwrap_or(false) { + println!("Creating database {}", DB_URL); + match Sqlite::create_database(DB_URL).await { + Ok(_) => println!("Create db success"), + Err(error) => panic!("error: {}", error), + } + } else { + println!("Database already exists"); + } + let db = SqlitePool::connect(DB_URL).await.unwrap(); + let result = sqlx::query( + "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY NOT NULL, name VARCHAR(250) NOT NULL);", + ) + .execute(&db) + .await + .unwrap(); + println!("Create user table result: {:?}", result); + let result = sqlx::query( + "SELECT name + FROM sqlite_schema + WHERE type ='table' + AND name NOT LIKE 'sqlite_%';", + ) + .fetch_all(&db) + .await + .unwrap(); + for (idx, row) in result.iter().enumerate() { + println!("[{}]: {:?}", idx, row.get::("name")); + } + } +} + +// todo: create interface with functions +// add_maker_fund_tx +// add_taker_fund_tx +// get_utxos_maker_fund +// Tokens has to be returned -> Result +// }> diff --git a/crates/coinselection/src/sqlite_db.rs b/crates/coinselection/src/sqlite_db.rs new file mode 100644 index 0000000..4a96d93 --- /dev/null +++ b/crates/coinselection/src/sqlite_db.rs @@ -0,0 +1,40 @@ +use crate::types::{CoinSelectionStorage, DcdContractTokenEntropies, GetTokenFilter, OutPointInfo}; +use async_trait::async_trait; +use contracts::DCDArguments; +use simplicity::bitcoin::OutPoint; + +pub struct SqliteDb {} + +#[async_trait] +impl CoinSelectionStorage for SqliteDb { + async fn add_outpoint(&self, info: OutPointInfo) -> crate::types::Result<()> { + todo!() + } + + async fn get_token_outpoint(&self, filter: GetTokenFilter) -> crate::types::Result> { + todo!() + } + + async fn add_dcd_params(&self, taproot_pubkey_gen: &str, dcd_args: &DCDArguments) -> crate::types::Result<()> { + todo!() + } + + async fn get_dcd_params(&self, taproot_pubkey_gen: &str) -> crate::types::Result> { + todo!() + } + + async fn add_dcd_contract_token_entropies( + &self, + taproot_pubkey_gen: &str, + token_entropies: DcdContractTokenEntropies, + ) -> crate::types::Result<()> { + todo!() + } + + async fn get_dcd_contract_token_entropies( + &self, + taproot_pubkey_gen: &str, + ) -> crate::types::Result> { + todo!() + } +} diff --git a/crates/coinselection/src/types.rs b/crates/coinselection/src/types.rs new file mode 100644 index 0000000..4412552 --- /dev/null +++ b/crates/coinselection/src/types.rs @@ -0,0 +1,180 @@ +use async_trait::async_trait; +use contracts::DCDArguments; +use simplicity::bitcoin::Txid; +use simplicityhl::elements::bitcoin::OutPoint; +use simplicityhl_core::AssetEntropyHex; + +pub type Result = anyhow::Result; + +#[async_trait] +pub trait CoinSelectionStorage: Send + Sync { + async fn add_outpoint(&self, info: OutPointInfo) -> Result<()>; + async fn get_token_outpoint(&self, filter: GetTokenFilter) -> Result>; + 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 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>; +} + +#[async_trait] +pub trait CoinSelector: Send + Sync + CoinSelectionStorage { + async fn add_taker_fund_order_outputs(&self, tx_id: Txid) -> Result<()> { + //todo: add inputs of transaction, mark the mas spent and + + //todo: add implementation + Ok(()) + } + async fn add_taker_termination_early_outputs(&self, tx_id: Txid) -> Result<()> { + //todo: add inputs of transaction, mark the mas spent and + //todo: add implementation + Ok(()) + } + async fn add_taker_settlement_outputs(&self, tx_id: Txid) -> Result<()> { + //todo: add inputs of transaction, mark the mas spent and + //todo: add implementation + Ok(()) + } + async fn add_maker_fund_outputs(&self, tx_id: Txid) -> Result<()> { + //todo: add inputs of transaction, mark the mas spent and + //todo: add implementation + Ok(()) + } + async fn add_maker_termination_collateral_outputs(&self, tx_id: Txid) -> Result<()> { + //todo: add inputs of transaction, mark the mas spent and + //todo: add implementation + Ok(()) + } + async fn add_maker_termination_settlement_outputs(&self, tx_id: Txid) -> Result<()> { + //todo: add inputs of transaction, mark the mas spent and + //todo: add implementation + Ok(()) + } + async fn add_maker_settlement_outputs(&self, tx_id: Txid) -> Result<()> { + //todo: add inputs of transaction, mark the mas spent and + //todo: add implementation + Ok(()) + } + async fn get_taker_fund_order_inputs(&self) -> Result { + //todo: add implementation + Ok(TakerFundInputs { + filler_token: None, + collateral_token: None, + }) + } + async fn get_taker_termination_early_inputs(&self) -> Result { + //todo: add implementation + Ok(TakerTerminationEarlyInputs { + filler_token: None, + collateral_token: None, + }) + } + async fn get_taker_settlement_inputs(&self, filter: GetSettlementFilter) -> Result { + //todo: add implementation + Ok(TakerSettlementInputs { + filler_token: None, + asset_token: None, + }) + } + async fn get_maker_fund_inputs(&self) -> Result { + //todo: add implementation + Ok(MakerFundInputs { + filler_reissuance_tx: None, + grantor_collateral_reissuance_tx: None, + grantor_settlement_reissuance_tx: None, + asset_settlement_tx: None, + }) + } + async fn get_maker_termination_collateral_inputs(&self) -> Result { + //todo: add implementation + Ok(MakerTerminationCollateralInputs { + collateral_token_utxo: None, + grantor_collateral_token_utxo: None, + }) + } + async fn get_maker_termination_settlement_inputs(&self) -> Result { + //todo: add implementation + Ok(MakerTerminationSettlmentInputs { + settlement_asset_utxo: None, + grantor_settlement_token_utxo: None, + }) + } + async fn get_maker_settlement_inputs(&self, filter: GetSettlementFilter) -> Result { + //todo: add implementation + Ok(MakerSettlementInputs { + asset_utxo: None, + grantor_collateral_token_utxo: None, + grantor_settlement_token_utxo: None, + }) + } +} + +impl CoinSelector for T {} + +pub struct DcdContractTokenEntropies { + filler_token_entropy: AssetEntropyHex, + grantor_collateral_token_entropy: AssetEntropyHex, + grantor_settlement_token_entropy: AssetEntropyHex, +} + +#[derive(Clone, Copy, Debug)] +pub struct GetSettlementFilter { + /// Flag used to correctly choose between Collateral and Settlement tokens + price_go_higher: bool, +} + +pub struct OutPointInfo { + pub outpoint: OutPoint, + pub owner_addr: String, +} + +pub struct GetTokenFilter { + /// Token asset id to look for + asset_id: Option, + /// Whether transaction is spent or not according to db or not + spent: Option, + /// Owner of + owner: Option, +} + +pub struct TakerFundInputs { + filler_token: Option, + collateral_token: Option, +} + +pub struct TakerTerminationEarlyInputs { + filler_token: Option, + collateral_token: Option, +} + +pub struct TakerSettlementInputs { + filler_token: Option, + asset_token: Option, +} + +pub struct MakerFundInputs { + filler_reissuance_tx: Option, + grantor_collateral_reissuance_tx: Option, + grantor_settlement_reissuance_tx: Option, + asset_settlement_tx: Option, +} + +pub struct MakerTerminationCollateralInputs { + collateral_token_utxo: Option, + grantor_collateral_token_utxo: Option, +} +pub struct MakerTerminationSettlmentInputs { + settlement_asset_utxo: Option, + grantor_settlement_token_utxo: Option, +} +pub struct MakerSettlementInputs { + asset_utxo: Option, + grantor_collateral_token_utxo: Option, + grantor_settlement_token_utxo: Option, +} From 67a0912131a3bf7224c67351ab673b2584a5fdae Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Fri, 5 Dec 2025 11:34:16 +0200 Subject: [PATCH 2/6] Add sqlite implementation with tests chore: * rename crate * update .gitignore --- .gitignore | 6 +- Cargo.toml | 1 + .../Cargo.toml | 9 +- .../migrations/20251204084835_init.sql | 29 + crates/coin-selection/src/common.rs | 1 + .../src/lib.rs | 1 + crates/coin-selection/src/sqlite_db.rs | 234 ++++++++ .../src/types.rs | 102 +++- .../tests/fixtures/default_outpoints.sql | 86 +++ .../coin-selection/tests/testing_sqlite_db.rs | 502 ++++++++++++++++++ crates/coin-selection/tests/utils.rs | 4 + .../migrations/20251204084835_init.sql | 1 - crates/coinselection/src/sqlite_db.rs | 40 -- crates/global-utils/src/logger.rs | 2 - 14 files changed, 951 insertions(+), 67 deletions(-) rename crates/{coinselection => coin-selection}/Cargo.toml (73%) create mode 100644 crates/coin-selection/migrations/20251204084835_init.sql create mode 100644 crates/coin-selection/src/common.rs rename crates/{coinselection => coin-selection}/src/lib.rs (98%) create mode 100644 crates/coin-selection/src/sqlite_db.rs rename crates/{coinselection => coin-selection}/src/types.rs (62%) create mode 100644 crates/coin-selection/tests/fixtures/default_outpoints.sql create mode 100644 crates/coin-selection/tests/testing_sqlite_db.rs create mode 100644 crates/coin-selection/tests/utils.rs delete mode 100644 crates/coinselection/migrations/20251204084835_init.sql delete mode 100644 crates/coinselection/src/sqlite_db.rs 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 8fea49f..143df6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ clap = { version = "4.5.49", features = ["derive"] } 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" } +coin-selection = { path = "./crate/coin-selection" } dex-nostr-relay = { path = "./crates/dex-nostr-relay" } dirs = { version = "6.0.0" } dotenvy = { version = "0.15.7" } diff --git a/crates/coinselection/Cargo.toml b/crates/coin-selection/Cargo.toml similarity index 73% rename from crates/coinselection/Cargo.toml rename to crates/coin-selection/Cargo.toml index ab38097..6cf55e0 100644 --- a/crates/coinselection/Cargo.toml +++ b/crates/coin-selection/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "coin_selection" +name = "coin-selection" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -12,8 +12,13 @@ async-trait = { workspace = true } bincode = { workspace = true } contracts = { workspace = true } contracts-adapter = { workspace = true } +global-utils = { workspace = true } +serde = { workspace = true } simplicity-lang = { workspace = true } simplicityhl = { workspace = true } simplicityhl-core = { workspace = true } sqlx = { workspace = true } -tokio = { workspace = true } \ No newline at end of file +tokio = { 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..c9ff41f --- /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_address 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/coinselection/src/lib.rs b/crates/coin-selection/src/lib.rs similarity index 98% rename from crates/coinselection/src/lib.rs rename to crates/coin-selection/src/lib.rs index 765c581..efaccb7 100644 --- a/crates/coinselection/src/lib.rs +++ b/crates/coin-selection/src/lib.rs @@ -1,3 +1,4 @@ +pub mod common; pub mod sqlite_db; pub mod types; diff --git a/crates/coin-selection/src/sqlite_db.rs b/crates/coin-selection/src/sqlite_db.rs new file mode 100644 index 0000000..cdc2e9d --- /dev/null +++ b/crates/coin-selection/src/sqlite_db.rs @@ -0,0 +1,234 @@ +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 simplicity::bitcoin::OutPoint; +use sqlx::{Connection, Sqlite, SqlitePool, migrate::MigrateDatabase}; +use std::path::PathBuf; + +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 { + async fn mark_outpoints_spent(&self, outpoints: &[OutPoint]) -> Result<()> { + // 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(()) + } + + async fn add_outpoint(&self, info: OutPointInfo) -> Result<()> { + sqlx::query( + r#" + INSERT INTO outpoints (tx_id, vout, owner_address, asset_id, spent) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(tx_id, vout) DO UPDATE + SET owner_address = excluded.owner_address, + asset_id = excluded.asset_id, + spent = excluded.spent + "#, + ) + .bind(info.outpoint.txid.to_string()) + .bind(info.outpoint.vout as i64) + .bind(info.owner_addr) + .bind(info.asset_id) + .bind(info.spent) + .execute(&self.pool) + .await?; + + Ok(()) + } + + async fn get_token_outpoints(&self, filter: GetTokenFilter) -> Result> { + let base = "SELECT id, tx_id, vout, owner_address, 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_address, 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_address, + asset_id, + spent, + }) + }) + .collect(); + + Ok(outpoints) + } +} + +#[async_trait] +impl DcdParamsStorage for SqliteRepo { + async fn add_dcd_params(&self, taproot_pubkey_gen: &str, dcd_args: &DCDArguments) -> Result<()> { + 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(()) + } + + async fn get_dcd_params(&self, taproot_pubkey_gen: &str) -> Result> { + 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 { + 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(()) + } + + 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/coinselection/src/types.rs b/crates/coin-selection/src/types.rs similarity index 62% rename from crates/coinselection/src/types.rs rename to crates/coin-selection/src/types.rs index 4412552..1dcf176 100644 --- a/crates/coinselection/src/types.rs +++ b/crates/coin-selection/src/types.rs @@ -8,10 +8,19 @@ 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_outpoint(&self, filter: GetTokenFilter) -> 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, @@ -24,39 +33,36 @@ pub trait CoinSelectionStorage: Send + Sync { } #[async_trait] -pub trait CoinSelector: Send + Sync + CoinSelectionStorage { - async fn add_taker_fund_order_outputs(&self, tx_id: Txid) -> Result<()> { - //todo: add inputs of transaction, mark the mas spent and - - //todo: add implementation +pub trait CoinSelector: Send + Sync + CoinSelectionStorage + DcdParamsStorage + EntropyStorage { + async fn add_taker_fund_order_outputs(&self, _tx_id: Txid) -> Result<()> { Ok(()) } - async fn add_taker_termination_early_outputs(&self, tx_id: Txid) -> Result<()> { + async fn add_taker_termination_early_outputs(&self, _tx_id: Txid) -> Result<()> { //todo: add inputs of transaction, mark the mas spent and //todo: add implementation Ok(()) } - async fn add_taker_settlement_outputs(&self, tx_id: Txid) -> Result<()> { + async fn add_taker_settlement_outputs(&self, _tx_id: Txid) -> Result<()> { //todo: add inputs of transaction, mark the mas spent and //todo: add implementation Ok(()) } - async fn add_maker_fund_outputs(&self, tx_id: Txid) -> Result<()> { + async fn add_maker_fund_outputs(&self, _tx_id: Txid) -> Result<()> { //todo: add inputs of transaction, mark the mas spent and //todo: add implementation Ok(()) } - async fn add_maker_termination_collateral_outputs(&self, tx_id: Txid) -> Result<()> { + async fn add_maker_termination_collateral_outputs(&self, _tx_id: Txid) -> Result<()> { //todo: add inputs of transaction, mark the mas spent and //todo: add implementation Ok(()) } - async fn add_maker_termination_settlement_outputs(&self, tx_id: Txid) -> Result<()> { + async fn add_maker_termination_settlement_outputs(&self, _tx_id: Txid) -> Result<()> { //todo: add inputs of transaction, mark the mas spent and //todo: add implementation Ok(()) } - async fn add_maker_settlement_outputs(&self, tx_id: Txid) -> Result<()> { + async fn add_maker_settlement_outputs(&self, _tx_id: Txid) -> Result<()> { //todo: add inputs of transaction, mark the mas spent and //todo: add implementation Ok(()) @@ -75,7 +81,7 @@ pub trait CoinSelector: Send + Sync + CoinSelectionStorage { collateral_token: None, }) } - async fn get_taker_settlement_inputs(&self, filter: GetSettlementFilter) -> Result { + async fn get_taker_settlement_inputs(&self, _filter: GetSettlementFilter) -> Result { //todo: add implementation Ok(TakerSettlementInputs { filler_token: None, @@ -105,7 +111,7 @@ pub trait CoinSelector: Send + Sync + CoinSelectionStorage { grantor_settlement_token_utxo: None, }) } - async fn get_maker_settlement_inputs(&self, filter: GetSettlementFilter) -> Result { + async fn get_maker_settlement_inputs(&self, _filter: GetSettlementFilter) -> Result { //todo: add implementation Ok(MakerSettlementInputs { asset_utxo: None, @@ -115,49 +121,72 @@ pub trait CoinSelector: Send + Sync + CoinSelectionStorage { } } -impl CoinSelector for T {} +impl CoinSelector for T {} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode, PartialEq)] pub struct DcdContractTokenEntropies { - filler_token_entropy: AssetEntropyHex, - grantor_collateral_token_entropy: AssetEntropyHex, - grantor_settlement_token_entropy: AssetEntropyHex, + 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_addr: String, + pub asset_id: String, + pub spent: bool, } +#[derive(Debug, Clone)] +pub struct OutPointInfoRaw { + pub id: u64, + pub outpoint: OutPoint, + pub owner_address: String, + pub asset_id: String, + pub spent: bool, +} + +#[derive(Debug, Clone)] pub struct GetTokenFilter { /// Token asset id to look for - asset_id: Option, + pub asset_id: Option, /// Whether transaction is spent or not according to db or not - spent: Option, + pub spent: Option, /// Owner of - owner: Option, + 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, @@ -165,16 +194,47 @@ pub struct MakerFundInputs { 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_address = ?"); + } + + 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/tests/fixtures/default_outpoints.sql b/crates/coin-selection/tests/fixtures/default_outpoints.sql new file mode 100644 index 0000000..b09e22d --- /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_address, 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_address, 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_address, 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_address, 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_address, 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..9f5cee1 --- /dev/null +++ b/crates/coin-selection/tests/testing_sqlite_db.rs @@ -0,0 +1,502 @@ +mod utils; + +#[cfg(test)] +mod tests { + use coin_selection::sqlite_db::SqliteRepo; + use coin_selection::types::{CoinSelectionStorage, GetTokenFilter, OutPointInfo}; + use simplicity::bitcoin::{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_address == 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_address == 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_address, 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_address == 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_address, 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_addr: 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_address, 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_addr: 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_address, 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_address, 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_address, 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_addr: 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_address == 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_address, 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/coinselection/migrations/20251204084835_init.sql b/crates/coinselection/migrations/20251204084835_init.sql deleted file mode 100644 index 8ddc1d3..0000000 --- a/crates/coinselection/migrations/20251204084835_init.sql +++ /dev/null @@ -1 +0,0 @@ --- Add migration script here diff --git a/crates/coinselection/src/sqlite_db.rs b/crates/coinselection/src/sqlite_db.rs deleted file mode 100644 index 4a96d93..0000000 --- a/crates/coinselection/src/sqlite_db.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::types::{CoinSelectionStorage, DcdContractTokenEntropies, GetTokenFilter, OutPointInfo}; -use async_trait::async_trait; -use contracts::DCDArguments; -use simplicity::bitcoin::OutPoint; - -pub struct SqliteDb {} - -#[async_trait] -impl CoinSelectionStorage for SqliteDb { - async fn add_outpoint(&self, info: OutPointInfo) -> crate::types::Result<()> { - todo!() - } - - async fn get_token_outpoint(&self, filter: GetTokenFilter) -> crate::types::Result> { - todo!() - } - - async fn add_dcd_params(&self, taproot_pubkey_gen: &str, dcd_args: &DCDArguments) -> crate::types::Result<()> { - todo!() - } - - async fn get_dcd_params(&self, taproot_pubkey_gen: &str) -> crate::types::Result> { - todo!() - } - - async fn add_dcd_contract_token_entropies( - &self, - taproot_pubkey_gen: &str, - token_entropies: DcdContractTokenEntropies, - ) -> crate::types::Result<()> { - todo!() - } - - async fn get_dcd_contract_token_entropies( - &self, - taproot_pubkey_gen: &str, - ) -> crate::types::Result> { - todo!() - } -} 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(), ); From f7fcee25e4c69c12eb8750fe73f56219b300b57a Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Fri, 5 Dec 2025 12:00:00 +0200 Subject: [PATCH 3/6] add enum representation of coinselector interface --- crates/coin-selection/src/types.rs | 173 +++++++++++++++-------------- 1 file changed, 90 insertions(+), 83 deletions(-) diff --git a/crates/coin-selection/src/types.rs b/crates/coin-selection/src/types.rs index 1dcf176..ef12c15 100644 --- a/crates/coin-selection/src/types.rs +++ b/crates/coin-selection/src/types.rs @@ -32,92 +32,99 @@ pub trait EntropyStorage: Send + Sync { ) -> 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 { + TakerFundOrder, + TakerTerminationEarly, + TakerSettlement(GetSettlementFilter), + MakerFund, + MakerTerminationCollateral, + MakerTerminationSettlement, + MakerSettlement(GetSettlementFilter), +} + #[async_trait] pub trait CoinSelector: Send + Sync + CoinSelectionStorage + DcdParamsStorage + EntropyStorage { - async fn add_taker_fund_order_outputs(&self, _tx_id: Txid) -> Result<()> { - Ok(()) - } - async fn add_taker_termination_early_outputs(&self, _tx_id: Txid) -> Result<()> { - //todo: add inputs of transaction, mark the mas spent and - //todo: add implementation - Ok(()) - } - async fn add_taker_settlement_outputs(&self, _tx_id: Txid) -> Result<()> { - //todo: add inputs of transaction, mark the mas spent and - //todo: add implementation - Ok(()) - } - async fn add_maker_fund_outputs(&self, _tx_id: Txid) -> Result<()> { - //todo: add inputs of transaction, mark the mas spent and - //todo: add implementation - Ok(()) - } - async fn add_maker_termination_collateral_outputs(&self, _tx_id: Txid) -> Result<()> { - //todo: add inputs of transaction, mark the mas spent and - //todo: add implementation - Ok(()) - } - async fn add_maker_termination_settlement_outputs(&self, _tx_id: Txid) -> Result<()> { - //todo: add inputs of transaction, mark the mas spent and - //todo: add implementation - Ok(()) - } - async fn add_maker_settlement_outputs(&self, _tx_id: Txid) -> Result<()> { - //todo: add inputs of transaction, mark the mas spent and - //todo: add implementation + async fn add_outputs(&self, option: TransactionOption) -> Result<()> { + match option { + TransactionOption::TakerFundOrder(_) => {} + TransactionOption::TakerTerminationEarly(_) => {} + TransactionOption::TakerSettlement(_) => {} + TransactionOption::MakerFund(_) => {} + TransactionOption::MakerTerminationCollateral(_) => {} + TransactionOption::MakerTerminationSettlement(_) => {} + TransactionOption::MakerSettlement(_) => {} + } Ok(()) } - async fn get_taker_fund_order_inputs(&self) -> Result { - //todo: add implementation - Ok(TakerFundInputs { - filler_token: None, - collateral_token: None, - }) - } - async fn get_taker_termination_early_inputs(&self) -> Result { - //todo: add implementation - Ok(TakerTerminationEarlyInputs { - filler_token: None, - collateral_token: None, - }) - } - async fn get_taker_settlement_inputs(&self, _filter: GetSettlementFilter) -> Result { - //todo: add implementation - Ok(TakerSettlementInputs { - filler_token: None, - asset_token: None, - }) - } - async fn get_maker_fund_inputs(&self) -> Result { - //todo: add implementation - Ok(MakerFundInputs { - filler_reissuance_tx: None, - grantor_collateral_reissuance_tx: None, - grantor_settlement_reissuance_tx: None, - asset_settlement_tx: None, - }) - } - async fn get_maker_termination_collateral_inputs(&self) -> Result { - //todo: add implementation - Ok(MakerTerminationCollateralInputs { - collateral_token_utxo: None, - grantor_collateral_token_utxo: None, - }) - } - async fn get_maker_termination_settlement_inputs(&self) -> Result { - //todo: add implementation - Ok(MakerTerminationSettlmentInputs { - settlement_asset_utxo: None, - grantor_settlement_token_utxo: None, - }) - } - async fn get_maker_settlement_inputs(&self, _filter: GetSettlementFilter) -> Result { - //todo: add implementation - Ok(MakerSettlementInputs { - asset_utxo: None, - grantor_collateral_token_utxo: None, - grantor_settlement_token_utxo: None, - }) + + async fn get_inputs(&self, option: TransactionInputs) -> Result { + let res = match option { + TransactionInputs::TakerFundOrder => TransactionInputsOption::TakerFundOrder(TakerFundInputs { + filler_token: None, + collateral_token: None, + }), + TransactionInputs::TakerTerminationEarly => { + TransactionInputsOption::TakerTerminationEarly(TakerTerminationEarlyInputs { + filler_token: None, + collateral_token: None, + }) + } + TransactionInputs::TakerSettlement(_filter) => { + TransactionInputsOption::TakerSettlement(TakerSettlementInputs { + filler_token: None, + asset_token: None, + }) + } + TransactionInputs::MakerFund => TransactionInputsOption::MakerFund(MakerFundInputs { + filler_reissuance_tx: None, + grantor_collateral_reissuance_tx: None, + grantor_settlement_reissuance_tx: None, + asset_settlement_tx: 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(_filter) => { + TransactionInputsOption::MakerSettlement(MakerSettlementInputs { + asset_utxo: None, + grantor_collateral_token_utxo: None, + grantor_settlement_token_utxo: None, + }) + } + }; + Ok(res) } } @@ -160,7 +167,7 @@ pub struct GetTokenFilter { pub asset_id: Option, /// Whether transaction is spent or not according to db or not pub spent: Option, - /// Owner of + /// Owner of token pub owner: Option, } From 7998ade9ad1a9a2513383fc87ac74bfaffe8466d Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Fri, 5 Dec 2025 16:41:08 +0200 Subject: [PATCH 4/6] coinselection db correction --- Cargo.toml | 5 +- crates/coin-selection/Cargo.toml | 2 + .../migrations/20251204084835_init.sql | 14 +-- crates/coin-selection/src/lib.rs | 48 +------- crates/coin-selection/src/sqlite_db.rs | 14 +-- crates/coin-selection/src/types.rs | 109 +++++++++++++----- crates/coin-selection/src/utils.rs | 43 +++++++ .../tests/fixtures/default_outpoints.sql | 10 +- .../coin-selection/tests/testing_sqlite_db.rs | 42 +++---- 9 files changed, 167 insertions(+), 120 deletions(-) create mode 100644 crates/coin-selection/src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 143df6a..12e453f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,10 +18,10 @@ 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" } -coin-selection = { path = "./crate/coin-selection" } dex-nostr-relay = { path = "./crates/dex-nostr-relay" } dirs = { version = "6.0.0" } dotenvy = { version = "0.15.7" } @@ -33,13 +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"]} +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 index 6cf55e0..cb5c735 100644 --- a/crates/coin-selection/Cargo.toml +++ b/crates/coin-selection/Cargo.toml @@ -19,6 +19,8 @@ simplicityhl = { workspace = true } simplicityhl-core = { workspace = true } sqlx = { workspace = true } tokio = { workspace = true } +reqwest = { workspace = true } +hex = { 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 index c9ff41f..a2b6c7f 100644 --- a/crates/coin-selection/migrations/20251204084835_init.sql +++ b/crates/coin-selection/migrations/20251204084835_init.sql @@ -1,12 +1,12 @@ CREATE TABLE IF NOT EXISTS outpoints ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tx_id VARYING CHARACTER(64) NOT NULL, - vout INTEGER NOT NULL, - owner_address TEXT NOT NULL, - asset_id TEXT NOT NULL, - spent BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + 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) ); diff --git a/crates/coin-selection/src/lib.rs b/crates/coin-selection/src/lib.rs index efaccb7..488b69c 100644 --- a/crates/coin-selection/src/lib.rs +++ b/crates/coin-selection/src/lib.rs @@ -1,50 +1,4 @@ pub mod common; pub mod sqlite_db; pub mod types; - -#[cfg(test)] -mod tests { - use sqlx::{Row, Sqlite, SqlitePool, migrate::MigrateDatabase}; - const DB_URL: &str = "sqlite://sqlite.db"; - - #[tokio::test] - async fn it_works() { - if !Sqlite::database_exists(DB_URL).await.unwrap_or(false) { - println!("Creating database {}", DB_URL); - match Sqlite::create_database(DB_URL).await { - Ok(_) => println!("Create db success"), - Err(error) => panic!("error: {}", error), - } - } else { - println!("Database already exists"); - } - let db = SqlitePool::connect(DB_URL).await.unwrap(); - let result = sqlx::query( - "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY NOT NULL, name VARCHAR(250) NOT NULL);", - ) - .execute(&db) - .await - .unwrap(); - println!("Create user table result: {:?}", result); - let result = sqlx::query( - "SELECT name - FROM sqlite_schema - WHERE type ='table' - AND name NOT LIKE 'sqlite_%';", - ) - .fetch_all(&db) - .await - .unwrap(); - for (idx, row) in result.iter().enumerate() { - println!("[{}]: {:?}", idx, row.get::("name")); - } - } -} - -// todo: create interface with functions -// add_maker_fund_tx -// add_taker_fund_tx -// get_utxos_maker_fund -// Tokens has to be returned -> Result -// }> +pub mod utils; diff --git a/crates/coin-selection/src/sqlite_db.rs b/crates/coin-selection/src/sqlite_db.rs index cdc2e9d..91cdc3e 100644 --- a/crates/coin-selection/src/sqlite_db.rs +++ b/crates/coin-selection/src/sqlite_db.rs @@ -3,7 +3,7 @@ use crate::types::{DcdParamsStorage, EntropyStorage, Result}; use anyhow::{Context, anyhow}; use async_trait::async_trait; use contracts::DCDArguments; -use simplicity::bitcoin::OutPoint; +use simplicityhl::elements::OutPoint; use sqlx::{Connection, Sqlite, SqlitePool, migrate::MigrateDatabase}; use std::path::PathBuf; @@ -99,17 +99,17 @@ impl CoinSelectionStorage for SqliteRepo { async fn add_outpoint(&self, info: OutPointInfo) -> Result<()> { sqlx::query( r#" - INSERT INTO outpoints (tx_id, vout, owner_address, asset_id, spent) + INSERT INTO outpoints (tx_id, vout, owner_script_pubkey, asset_id, spent) VALUES (?, ?, ?, ?, ?) ON CONFLICT(tx_id, vout) DO UPDATE - SET owner_address = excluded.owner_address, + 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_addr) + .bind(info.owner_script_pubkey) .bind(info.asset_id) .bind(info.spent) .execute(&self.pool) @@ -119,7 +119,7 @@ impl CoinSelectionStorage for SqliteRepo { } async fn get_token_outpoints(&self, filter: GetTokenFilter) -> Result> { - let base = "SELECT id, tx_id, vout, owner_address, asset_id, spent FROM outpoints"; + 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}"); @@ -139,13 +139,13 @@ impl CoinSelectionStorage for SqliteRepo { let outpoints = rows .into_iter() - .filter_map(|(id, tx_id, vout, owner_address, asset_id, spent)| { + .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_address, + owner_script_pubkey, asset_id, spent, }) diff --git a/crates/coin-selection/src/types.rs b/crates/coin-selection/src/types.rs index ef12c15..941159b 100644 --- a/crates/coin-selection/src/types.rs +++ b/crates/coin-selection/src/types.rs @@ -1,7 +1,7 @@ +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 simplicity::bitcoin::Txid; -use simplicityhl::elements::bitcoin::OutPoint; +use simplicityhl::elements::{OutPoint, Txid}; use simplicityhl_core::AssetEntropyHex; pub type Result = anyhow::Result; @@ -56,13 +56,34 @@ pub enum TransactionInputsOption { #[derive(Clone, Debug)] pub enum TransactionInputs { - TakerFundOrder, - TakerTerminationEarly, - TakerSettlement(GetSettlementFilter), - MakerFund, - MakerTerminationCollateral, - MakerTerminationSettlement, - MakerSettlement(GetSettlementFilter), + 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] @@ -72,7 +93,18 @@ pub trait CoinSelector: Send + Sync + CoinSelectionStorage + DcdParamsStorage + TransactionOption::TakerFundOrder(_) => {} TransactionOption::TakerTerminationEarly(_) => {} TransactionOption::TakerSettlement(_) => {} - TransactionOption::MakerFund(_) => {} + TransactionOption::MakerFund(order) => { + 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?); + } + } TransactionOption::MakerTerminationCollateral(_) => {} TransactionOption::MakerTerminationSettlement(_) => {} TransactionOption::MakerSettlement(_) => {} @@ -82,41 +114,60 @@ pub trait CoinSelector: Send + Sync + CoinSelectionStorage + DcdParamsStorage + async fn get_inputs(&self, option: TransactionInputs) -> Result { let res = match option { - TransactionInputs::TakerFundOrder => TransactionInputsOption::TakerFundOrder(TakerFundInputs { + 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 => { + TransactionInputs::TakerTerminationEarly { .. } => { TransactionInputsOption::TakerTerminationEarly(TakerTerminationEarlyInputs { filler_token: None, collateral_token: None, }) } - TransactionInputs::TakerSettlement(_filter) => { + TransactionInputs::TakerSettlement { .. } => { TransactionInputsOption::TakerSettlement(TakerSettlementInputs { filler_token: None, asset_token: None, }) } - TransactionInputs::MakerFund => TransactionInputsOption::MakerFund(MakerFundInputs { - filler_reissuance_tx: None, - grantor_collateral_reissuance_tx: None, - grantor_settlement_reissuance_tx: None, - asset_settlement_tx: None, - }), - TransactionInputs::MakerTerminationCollateral => { + TransactionInputs::MakerTerminationCollateral { .. } => { TransactionInputsOption::MakerTerminationCollateral(MakerTerminationCollateralInputs { collateral_token_utxo: None, grantor_collateral_token_utxo: None, }) } - TransactionInputs::MakerTerminationSettlement => { + TransactionInputs::MakerTerminationSettlement { .. } => { TransactionInputsOption::MakerTerminationSettlement(MakerTerminationSettlmentInputs { settlement_asset_utxo: None, grantor_settlement_token_utxo: None, }) } - TransactionInputs::MakerSettlement(_filter) => { + TransactionInputs::MakerSettlement { .. } => { TransactionInputsOption::MakerSettlement(MakerSettlementInputs { asset_utxo: None, grantor_collateral_token_utxo: None, @@ -147,7 +198,7 @@ pub struct GetSettlementFilter { #[derive(Debug, Clone)] pub struct OutPointInfo { pub outpoint: OutPoint, - pub owner_addr: String, + pub owner_script_pubkey: String, pub asset_id: String, pub spent: bool, } @@ -156,7 +207,7 @@ pub struct OutPointInfo { pub struct OutPointInfoRaw { pub id: u64, pub outpoint: OutPoint, - pub owner_address: String, + pub owner_script_pubkey: String, pub asset_id: String, pub spent: bool, } @@ -195,10 +246,10 @@ pub struct TakerSettlementInputs { #[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, + filler_reissuance_tx: Option, + grantor_collateral_reissuance_tx: Option, + grantor_settlement_reissuance_tx: Option, + asset_settlement_tx: Option, } #[expect(dead_code)] @@ -234,7 +285,7 @@ impl GetTokenFilter { query.push_str(" AND spent = ?"); } if self.owner.is_some() { - query.push_str(" AND owner_address = ?"); + query.push_str(" AND owner_script_pubkey = ?"); } if !query.is_empty() { 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 index b09e22d..7e4fd57 100644 --- a/crates/coin-selection/tests/fixtures/default_outpoints.sql +++ b/crates/coin-selection/tests/fixtures/default_outpoints.sql @@ -3,7 +3,7 @@ -- tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n dcd -- FILLER_ASSET_ID -insert into outpoints (vout, owner_address, asset_id, spent, tx_id) +insert into outpoints (vout, owner_script_pubkey, asset_id, spent, tx_id) values (1, 'tex1p3tzxsj4cs64a6qwpcc68aev4xx38mcqmrya9r3587jy49sk40z3qk6d9el', '2c3aa8ae0e199f9609e2e4b60a97a1f4b52c5d76d916b0a51e18ecded3d057b1', true, '1b993898b41c31cd88781e68ab9b2f6856c1c7d68921c74ef347412feac8ad6c'), @@ -19,7 +19,7 @@ values (1, 'tex1p3tzxsj4cs64a6qwpcc68aev4xx38mcqmrya9r3587jy49sk40z3qk6d9el', on conflict DO NOTHING; -- GRANTOR_COLLATERAL_ASSET_ID -insert into outpoints (vout, owner_address, asset_id, spent, tx_id) +insert into outpoints (vout, owner_script_pubkey, asset_id, spent, tx_id) values (6, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', 'ba817efa46ffb5dd5b985d2c6657376ceaf748eedfda3f88e273260c18538d73', true, 'cd1e4aa43251fa1ebbf32e8f9b2e66358b746a32d6bcc0420a0a2e24e0393f4e'), @@ -38,7 +38,7 @@ values (6, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', on conflict DO NOTHING; -- GRANTOR_SETTLEMENT_ASSET_ID -insert into outpoints (vout, owner_address, asset_id, spent, tx_id) +insert into outpoints (vout, owner_script_pubkey, asset_id, spent, tx_id) values (7, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', '82b7bba397cafbf1918cc8fee11aa636eba97ee4c88a6efe954b90e8a85806ea', false, 'cd1e4aa43251fa1ebbf32e8f9b2e66358b746a32d6bcc0420a0a2e24e0393f4e'), @@ -57,7 +57,7 @@ values (7, 'tex1pyzkfajdprt6gl6288z54c6m4lrg3vp32cajmqrh5kfaegydyrv0qtcg6lm', on conflict DO NOTHING; -- SETTLEMENT_ASSET_ID -insert into outpoints (vout, owner_address, asset_id, spent, tx_id) +insert into outpoints (vout, owner_script_pubkey, asset_id, spent, tx_id) values (4, 'tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n', '420561859e4217f0def578911bbf68d7d3f75d664b978de39083269994eecd4b', false, '403c9bca043cbfb692bfad8ff7ea09634a838ae833f9a62aa043d2ffa4458387'), @@ -70,7 +70,7 @@ values (4, 'tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n', on conflict DO NOTHING; -- COLLATERAL_ASSET_ID -insert into outpoints (vout, owner_address, asset_id, spent, tx_id) +insert into outpoints (vout, owner_script_pubkey, asset_id, spent, tx_id) values (0, 'tex1p9988q8kfq33m0y6wlsra683rur32k9vx58kqc6cceeks7tccu5yqhkjv7n', '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49', false, '1eb9bed5e3954d0556de572ea12c73d6b4d7f62a4d11646cf1a07d943c2cb50e'), diff --git a/crates/coin-selection/tests/testing_sqlite_db.rs b/crates/coin-selection/tests/testing_sqlite_db.rs index 9f5cee1..74f0103 100644 --- a/crates/coin-selection/tests/testing_sqlite_db.rs +++ b/crates/coin-selection/tests/testing_sqlite_db.rs @@ -4,7 +4,7 @@ mod utils; mod tests { use coin_selection::sqlite_db::SqliteRepo; use coin_selection::types::{CoinSelectionStorage, GetTokenFilter, OutPointInfo}; - use simplicity::bitcoin::{OutPoint, Txid}; + use simplicityhl::elements::{OutPoint, Txid}; use sqlx::SqlitePool; use std::str::FromStr; @@ -79,7 +79,7 @@ mod tests { assert!( outpoints .iter() - .all(|op| op.owner_address == TAKER_TOKEN_ADDRESS && op.asset_id == FILLER_ASSET_ID) + .all(|op| op.owner_script_pubkey == TAKER_TOKEN_ADDRESS && op.asset_id == FILLER_ASSET_ID) ); Ok(()) @@ -100,11 +100,9 @@ mod tests { let outpoints = repo.get_token_outpoints(filter).await?; println!("outpoints: {:#?}", outpoints); assert_eq!(outpoints.len(), 5); - assert!( - outpoints.iter().all(|op| { - op.owner_address == MAKER_TOKEN_ADDRESS && op.asset_id == GRANTOR_COLLATERAL_ASSET_ID - }) - ); + assert!(outpoints.iter().all(|op| { + op.owner_script_pubkey == MAKER_TOKEN_ADDRESS && op.asset_id == GRANTOR_COLLATERAL_ASSET_ID + })); Ok(()) } @@ -125,7 +123,7 @@ mod tests { 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_address, DCD_TOKEN_ADDRESS); + assert_eq!(outpoints[0].owner_script_pubkey, DCD_TOKEN_ADDRESS); assert_eq!(outpoints[0].asset_id, COLLATERAL_ASSET_ID); Ok(()) @@ -145,11 +143,9 @@ mod tests { }; let before = repo.get_token_outpoints(filter.clone()).await?; assert!(!before.is_empty()); - assert!( - before.iter().all(|op| { - op.owner_address == MAKER_TOKEN_ADDRESS && op.asset_id == GRANTOR_COLLATERAL_ASSET_ID - }) - ); + 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?; @@ -176,7 +172,7 @@ mod tests { let outpoints = repo.get_token_outpoints(filter).await?; assert_eq!(outpoints.len(), 1); // assert_eq!(outpoints[0].vout, 3); - assert_eq!(outpoints[0].owner_address, TAKER_TOKEN_ADDRESS); + assert_eq!(outpoints[0].owner_script_pubkey, TAKER_TOKEN_ADDRESS); assert_eq!(outpoints[0].asset_id, SETTLEMENT_ASSET_ID); Ok(()) @@ -215,7 +211,7 @@ mod tests { let info = OutPointInfo { outpoint: op, - owner_addr: MAKER_TOKEN_ADDRESS.to_string(), + owner_script_pubkey: MAKER_TOKEN_ADDRESS.to_string(), asset_id: FILLER_ASSET_ID.to_string(), spent: false, }; @@ -231,7 +227,7 @@ mod tests { assert_eq!(outpoints.len(), 1); assert_eq!(outpoints[0].outpoint, op); - assert_eq!(outpoints[0].owner_address, MAKER_TOKEN_ADDRESS); + assert_eq!(outpoints[0].owner_script_pubkey, MAKER_TOKEN_ADDRESS); assert_eq!(outpoints[0].asset_id, FILLER_ASSET_ID); Ok(()) @@ -268,7 +264,7 @@ mod tests { for (tx, vout, owner, asset) in ops { let info = OutPointInfo { outpoint: OutPoint { txid: txid(tx), vout }, - owner_addr: owner.to_string(), + owner_script_pubkey: owner.to_string(), asset_id: asset.to_string(), spent: false, }; @@ -282,7 +278,7 @@ mod tests { }; let r1 = repo.get_token_outpoints(filter1).await?; assert_eq!(r1.len(), 1); - assert_eq!(r1[0].owner_address, MAKER_TOKEN_ADDRESS); + assert_eq!(r1[0].owner_script_pubkey, MAKER_TOKEN_ADDRESS); assert_eq!(r1[0].asset_id, SETTLEMENT_ASSET_ID); let filter2 = GetTokenFilter { @@ -292,7 +288,7 @@ mod tests { }; let r2 = repo.get_token_outpoints(filter2).await?; assert_eq!(r2.len(), 1); - assert_eq!(r2[0].owner_address, TAKER_TOKEN_ADDRESS); + assert_eq!(r2[0].owner_script_pubkey, TAKER_TOKEN_ADDRESS); assert_eq!(r2[0].asset_id, COLLATERAL_ASSET_ID); let filter3 = GetTokenFilter { @@ -302,7 +298,7 @@ mod tests { }; let r3 = repo.get_token_outpoints(filter3).await?; assert_eq!(r3.len(), 1); - assert_eq!(r3[0].owner_address, DCD_TOKEN_ADDRESS); + assert_eq!(r3[0].owner_script_pubkey, DCD_TOKEN_ADDRESS); assert_eq!(r3[0].asset_id, GRANTOR_SETTLEMENT_ASSET_ID); Ok(()) @@ -322,7 +318,7 @@ mod tests { let info = OutPointInfo { outpoint: op, - owner_addr: TAKER_TOKEN_ADDRESS.to_string(), + owner_script_pubkey: TAKER_TOKEN_ADDRESS.to_string(), asset_id: FILLER_ASSET_ID.to_string(), spent: false, }; @@ -338,7 +334,7 @@ mod tests { assert!( unspent_before .iter() - .all(|p| { p.owner_address == TAKER_TOKEN_ADDRESS && p.asset_id == FILLER_ASSET_ID }) + .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::>()) @@ -354,7 +350,7 @@ mod tests { }; let spent_after = repo.get_token_outpoints(filter_spent).await?; assert_eq!(spent_after.len(), 1); - assert_eq!(spent_after[0].owner_address, TAKER_TOKEN_ADDRESS); + assert_eq!(spent_after[0].owner_script_pubkey, TAKER_TOKEN_ADDRESS); assert_eq!(spent_after[0].asset_id, FILLER_ASSET_ID); Ok(()) From 63dfb509a837e5374af4e028a34850be72acd7b0 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Fri, 5 Dec 2025 18:21:08 +0200 Subject: [PATCH 5/6] add debug logs for db --- crates/coin-selection/Cargo.toml | 5 +++-- crates/coin-selection/src/sqlite_db.rs | 16 ++++++++++++++++ crates/coin-selection/src/types.rs | 5 +++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/coin-selection/Cargo.toml b/crates/coin-selection/Cargo.toml index cb5c735..f4312cf 100644 --- a/crates/coin-selection/Cargo.toml +++ b/crates/coin-selection/Cargo.toml @@ -13,14 +13,15 @@ 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 } -reqwest = { workspace = true } -hex = { workspace = true } +tracing = { workspace = true } [dev-dependencies] dotenvy = { workspace = true } \ No newline at end of file diff --git a/crates/coin-selection/src/sqlite_db.rs b/crates/coin-selection/src/sqlite_db.rs index 91cdc3e..b3d079e 100644 --- a/crates/coin-selection/src/sqlite_db.rs +++ b/crates/coin-selection/src/sqlite_db.rs @@ -6,6 +6,7 @@ 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"); @@ -76,7 +77,10 @@ impl SqliteRepo { #[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 { @@ -96,7 +100,10 @@ impl CoinSelectionStorage for SqliteRepo { 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) @@ -118,7 +125,10 @@ impl CoinSelectionStorage for SqliteRepo { 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}"); @@ -158,7 +168,9 @@ impl CoinSelectionStorage for SqliteRepo { #[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( @@ -174,7 +186,9 @@ impl DcdParamsStorage for SqliteRepo { 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) @@ -192,6 +206,7 @@ impl DcdParamsStorage for SqliteRepo { #[async_trait] impl EntropyStorage for SqliteRepo { + #[instrument(level = "debug", skip_all, err)] async fn add_dcd_contract_token_entropies( &self, taproot_pubkey_gen: &str, @@ -212,6 +227,7 @@ impl EntropyStorage for SqliteRepo { Ok(()) } + #[instrument(level = "debug", skip_all, err)] async fn get_dcd_contract_token_entropies( &self, taproot_pubkey_gen: &str, diff --git a/crates/coin-selection/src/types.rs b/crates/coin-selection/src/types.rs index 941159b..f13ec86 100644 --- a/crates/coin-selection/src/types.rs +++ b/crates/coin-selection/src/types.rs @@ -94,6 +94,7 @@ pub trait CoinSelector: Send + Sync + CoinSelectionStorage + DcdParamsStorage + 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()); @@ -104,6 +105,10 @@ pub trait CoinSelector: Send + Sync + CoinSelectionStorage + DcdParamsStorage + 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(_) => {} From 34959f7aafddabdf5092023f0eda9658125c9614 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Fri, 5 Dec 2025 18:33:50 +0200 Subject: [PATCH 6/6] add dex implementation for taker fund command chore: * data is saved by now only on maker fund execution or in * add usage of sqlite --- crates/dex-cli/Cargo.toml | 1 + crates/dex-cli/src/cli/helper.rs | 6 ++ crates/dex-cli/src/cli/processor.rs | 79 +++++++++++++--- crates/dex-cli/src/cli/taker.rs | 4 +- crates/dex-cli/src/common/config.rs | 9 ++ .../src/contract_handlers/maker_funding.rs | 4 +- .../src/contract_handlers/taker_funding.rs | 92 +++++++++++++++++-- crates/dex-cli/src/error.rs | 2 + 8 files changed, 175 insertions(+), 22 deletions(-) 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), }