Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ logs/*
/.simplicity-dex.config.toml
/.cache
taker/
simplicity-dex
simplicity-dex

# DB
**/dcd_cache.db
/db
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ readme = "Readme.md"

[workspace.dependencies]
anyhow = { version = "1.0.100" }
async-trait = { version = "0.1.89" }
bincode = { version = "2.0.1" }
chrono = { version = "0.4.42" }
clap = { version = "4.5.49", features = ["derive"] }
coin-selection = { path = "./crates/coin-selection" }
config = { version = "0.15.18" }
contracts = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "baa8ab7", package = "contracts" }
contracts-adapter = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "baa8ab7", package = "contracts-adapter" }
Expand All @@ -31,12 +33,14 @@ humantime = { version = "2.3.0" }
nostr = { version = "0.43.1", features = ["std"] }
nostr-sdk = { version = "0.43.0" }
proptest = { version = "1.9.0" }
reqwest = { version = "0.12.24" }
serde = { version = "1.0.228" }
serde_json = { version = "1.0.145" }
simplicity-lang = { version = "0.6.0" }
simplicityhl = { version = "0.2.0" }
simplicityhl-core = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "baa8ab7", package = "simplicityhl-core", features = ["encoding"] }
sled = { version = "0.34.7" }
sqlx = { version = "0.8.6", features = ["runtime-tokio-native-tls", "sqlite"] }
thiserror = { version = "2.0.17" }
tokio = { version = "1.48.0", features = ["macros", "test-util", "rt", "rt-multi-thread", "tracing" ] }
tracing = { version = "0.1.41" }
Expand Down
27 changes: 27 additions & 0 deletions crates/coin-selection/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "coin-selection"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
readme.workspace = true

[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
bincode = { workspace = true }
contracts = { workspace = true }
contracts-adapter = { workspace = true }
global-utils = { workspace = true }
hex = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
simplicity-lang = { workspace = true }
simplicityhl = { workspace = true }
simplicityhl-core = { workspace = true }
sqlx = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }

[dev-dependencies]
dotenvy = { workspace = true }
29 changes: 29 additions & 0 deletions crates/coin-selection/migrations/20251204084835_init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
CREATE TABLE IF NOT EXISTS outpoints
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
tx_id VARYING CHARACTER(64) NOT NULL,
vout INTEGER NOT NULL,
owner_script_pubkey TEXT NOT NULL,
asset_id TEXT NOT NULL,
spent BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (tx_id, vout)
);

CREATE TABLE IF NOT EXISTS dcd_params
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
taproot_pubkey_gen TEXT NOT NULL UNIQUE,
dcd_args_blob BLOB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS dcd_token_entropies
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
taproot_pubkey_gen TEXT NOT NULL UNIQUE,
token_entropies_blob BLOB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- todo: create indexes
1 change: 1 addition & 0 deletions crates/coin-selection/src/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

4 changes: 4 additions & 0 deletions crates/coin-selection/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod common;
pub mod sqlite_db;
pub mod types;
pub mod utils;
250 changes: 250 additions & 0 deletions crates/coin-selection/src/sqlite_db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
use crate::types::{CoinSelectionStorage, DcdContractTokenEntropies, GetTokenFilter, OutPointInfo, OutPointInfoRaw};
use crate::types::{DcdParamsStorage, EntropyStorage, Result};
use anyhow::{Context, anyhow};
use async_trait::async_trait;
use contracts::DCDArguments;
use simplicityhl::elements::OutPoint;
use sqlx::{Connection, Sqlite, SqlitePool, migrate::MigrateDatabase};
use std::path::PathBuf;
use tracing::instrument;

const CARGO_MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR");

#[derive(Clone)]
pub struct SqliteRepo {
pool: SqlitePool,
}

const SQLITE_DB_NAME: &str = "dcd_cache.db";

impl SqliteRepo {
pub async fn from_url(db_url: &str) -> Result<Self> {
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<Self> {
sqlx::migrate!().run(&pool).await?;
Ok(Self { pool })
}

pub async fn new() -> Result<Self> {
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<str>) -> String {
format!("sqlite://{}", path.as_ref())
}

fn default_db_path() -> Result<PathBuf> {
let manifest_dir = PathBuf::from(CARGO_MANIFEST_DIR);
let workspace_root = manifest_dir
.parent()
.and_then(|p| p.parent())
.ok_or_else(|| anyhow::anyhow!("Could not determine workspace root"))?;

let db_dir = workspace_root.join("db");

if !db_dir.exists() {
std::fs::create_dir_all(&db_dir)?;
}

Ok(db_dir.join(SQLITE_DB_NAME))
}

pub async fn healthcheck(&self) -> Result<()> {
self.pool.acquire().await?.ping().await?;
Ok(())
}
}

#[async_trait]
impl CoinSelectionStorage for SqliteRepo {
#[instrument(level = "debug", skip_all, err)]
async fn mark_outpoints_spent(&self, outpoints: &[OutPoint]) -> Result<()> {
tracing::debug!("Input params, outpoints: {outpoints:?}");

// mark given outpoints as spent in a single transaction
let mut tx = self.pool.begin().await?;
for op in outpoints {
sqlx::query(
r#"
UPDATE outpoints
SET spent = 1
WHERE tx_id = ? AND vout = ?
"#,
)
.bind(op.txid.to_string())
.bind(op.vout as i64)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(())
}

#[instrument(level = "debug", skip_all, err)]
async fn add_outpoint(&self, info: OutPointInfo) -> Result<()> {
tracing::debug!("Input params, info: {info:?}");

sqlx::query(
r#"
INSERT INTO outpoints (tx_id, vout, owner_script_pubkey, asset_id, spent)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(tx_id, vout) DO UPDATE
SET owner_script_pubkey = excluded.owner_script_pubkey,
asset_id = excluded.asset_id,
spent = excluded.spent
"#,
)
.bind(info.outpoint.txid.to_string())
.bind(info.outpoint.vout as i64)
.bind(info.owner_script_pubkey)
.bind(info.asset_id)
.bind(info.spent)
.execute(&self.pool)
.await?;

Ok(())
}

#[instrument(level = "debug", skip_all, err)]
async fn get_token_outpoints(&self, filter: GetTokenFilter) -> Result<Vec<OutPointInfoRaw>> {
tracing::debug!("Input params, filter: {filter:?}");

let base = "SELECT id, tx_id, vout, owner_script_pubkey, asset_id, spent FROM outpoints";
let where_clause = filter.get_sql_filter();
let query = format!("{base}{where_clause}");

let mut sql_query = sqlx::query_as::<_, (i64, String, i64, String, String, bool)>(&query);

if let Some(asset_id) = &filter.asset_id {
sql_query = sql_query.bind(asset_id);
}
if let Some(spent) = filter.spent {
sql_query = sql_query.bind(spent);
}
if let Some(owner) = &filter.owner {
sql_query = sql_query.bind(owner);
}

let rows = sql_query.fetch_all(&self.pool).await?;

let outpoints = rows
.into_iter()
.filter_map(|(id, tx_id, vout, owner_script_pubkey, asset_id, spent)| {
let tx_id = tx_id.parse().ok()?;
let vout = vout as u32;
Some(OutPointInfoRaw {
id: id as u64,
outpoint: OutPoint::new(tx_id, vout),
owner_script_pubkey,
asset_id,
spent,
})
})
.collect();

Ok(outpoints)
}
}

#[async_trait]
impl DcdParamsStorage for SqliteRepo {
#[instrument(level = "debug", skip_all, err)]
async fn add_dcd_params(&self, taproot_pubkey_gen: &str, dcd_args: &DCDArguments) -> Result<()> {
tracing::debug!("Input params, taproot: {taproot_pubkey_gen}, dcd_args: {dcd_args:?}");
let serialized = bincode::encode_to_vec(dcd_args, bincode::config::standard())?;

sqlx::query(
"INSERT INTO dcd_params (taproot_pubkey_gen, dcd_args_blob)
VALUES (?, ?)
ON CONFLICT(taproot_pubkey_gen) DO UPDATE SET dcd_args_blob = excluded.dcd_args_blob",
)
.bind(taproot_pubkey_gen)
.bind(serialized)
.execute(&self.pool)
.await?;

Ok(())
}

#[instrument(level = "debug", skip_all, err)]
async fn get_dcd_params(&self, taproot_pubkey_gen: &str) -> Result<Option<DCDArguments>> {
tracing::debug!("Input params, taproot: {taproot_pubkey_gen}");
let row = sqlx::query_as::<_, (Vec<u8>,)>("SELECT dcd_args_blob FROM dcd_params WHERE taproot_pubkey_gen = ?")
.bind(taproot_pubkey_gen)
.fetch_optional(&self.pool)
.await?;

match row {
Some((blob,)) => {
let (dcd_args, _) = bincode::decode_from_slice(&blob, bincode::config::standard())?;
Ok(Some(dcd_args))
}
None => Ok(None),
}
}
}

#[async_trait]
impl EntropyStorage for SqliteRepo {
#[instrument(level = "debug", skip_all, err)]
async fn add_dcd_contract_token_entropies(
&self,
taproot_pubkey_gen: &str,
token_entropies: DcdContractTokenEntropies,
) -> Result<()> {
let serialized = bincode::encode_to_vec(&token_entropies, bincode::config::standard())?;

sqlx::query(
"INSERT INTO dcd_token_entropies (taproot_pubkey_gen, token_entropies_blob)
VALUES (?, ?)
ON CONFLICT(taproot_pubkey_gen) DO UPDATE SET token_entropies_blob = excluded.token_entropies_blob",
)
.bind(taproot_pubkey_gen)
.bind(serialized)
.execute(&self.pool)
.await?;

Ok(())
}

#[instrument(level = "debug", skip_all, err)]
async fn get_dcd_contract_token_entropies(
&self,
taproot_pubkey_gen: &str,
) -> Result<Option<DcdContractTokenEntropies>> {
let row = sqlx::query_as::<_, (Vec<u8>,)>(
"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),
}
}
}
Loading
Loading