From f88b3039fd5a0ecd7b65173c2583662e4bea33ec Mon Sep 17 00:00:00 2001 From: bennyhodl Date: Wed, 26 Mar 2025 16:27:52 -0400 Subject: [PATCH 1/5] postgres init --- Cargo.lock | 36 +++++++++ ddk/Cargo.toml | 8 +- ddk/examples/postgres.rs | 38 ++++++++++ .../migrations/0002_bdk_wallet.down.sql | 7 ++ .../migrations/0002_bdk_wallet.up.sql | 73 +++++++++++++++++++ ddk/src/storage/postgres/mod.rs | 56 +++++++++++--- 6 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 ddk/examples/postgres.rs create mode 100644 ddk/src/storage/postgres/migrations/0002_bdk_wallet.down.sql create mode 100644 ddk/src/storage/postgres/migrations/0002_bdk_wallet.up.sql diff --git a/Cargo.lock b/Cargo.lock index 976156d3..e8429588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,6 +385,20 @@ dependencies = [ "miniscript", ] +[[package]] +name = "bdk_sqlx" +version = "0.1.0" +dependencies = [ + "bdk_wallet", + "serde", + "serde_json", + "sqlx", + "thiserror 1.0.65", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "bdk_wallet" version = "1.0.0" @@ -980,6 +994,7 @@ dependencies = [ "base64 0.13.1", "bdk_chain", "bdk_esplora", + "bdk_sqlx", "bdk_wallet", "bitcoin", "bitcoincore-rpc", @@ -2066,6 +2081,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -3542,6 +3558,8 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", + "rustls 0.23.16", + "rustls-pemfile 2.2.0", "serde", "serde_json", "sha2", @@ -3552,6 +3570,8 @@ dependencies = [ "tokio-stream", "tracing", "url", + "uuid", + "webpki-roots 0.26.6", ] [[package]] @@ -3635,6 +3655,7 @@ dependencies = [ "thiserror 2.0.11", "time", "tracing", + "uuid", "whoami", ] @@ -3676,6 +3697,7 @@ dependencies = [ "thiserror 2.0.11", "time", "tracing", + "uuid", "whoami", ] @@ -3702,6 +3724,7 @@ dependencies = [ "time", "tracing", "url", + "uuid", ] [[package]] @@ -4211,6 +4234,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -4221,12 +4254,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/ddk/Cargo.toml b/ddk/Cargo.toml index d23dc4cc..55d7c566 100644 --- a/ddk/Cargo.toml +++ b/ddk/Cargo.toml @@ -23,7 +23,7 @@ nostr-oracle = ["dep:nostr-database", "nostr", "kormir", "kormir/nostr"] # storage features sled = ["dep:sled"] -postgres = ["dep:sqlx", "sqlx/postgres"] +postgres = ["dep:sqlx", "sqlx/postgres", "dep:bdk_sqlx"] [dependencies] dlc = { version = "0.7.1", features = ["use-serde"] } @@ -72,6 +72,7 @@ hmac = "0.12.1" sha2 = "0.10" nostr-database = { version = "0.39.0", optional = true } sqlx = { version = "0.8.3", optional = true, features = ["runtime-tokio", "time", "bigdecimal", "chrono"] } +bdk_sqlx = { version = "0.1.0", path = "../../bdk-sqlx", optional = true } [dev-dependencies] test-log = { version = "0.2.16", features = ["trace"] } @@ -88,3 +89,8 @@ required-features = ["lightning", "kormir", "sled"] name = "nostr" path = "examples/nostr.rs" required-features = ["nostr"] + +[[example]] +name = "postgres" +path = "examples/postgres.rs" +required-features = ["postgres", "lightning", "kormir"] diff --git a/ddk/examples/postgres.rs b/ddk/examples/postgres.rs new file mode 100644 index 00000000..d6b8a0b9 --- /dev/null +++ b/ddk/examples/postgres.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use bitcoin::key::rand::Fill; +use ddk::builder::Builder; +use ddk::oracle::kormir::KormirOracleClient; +use ddk::storage::postgres::PostgresStore; +use ddk::transport::lightning::LightningTransport; +use std::sync::Arc; + +type ApplicationDdk = ddk::DlcDevKit; + +#[tokio::main] +async fn main() -> Result<()> { + let transport = Arc::new(LightningTransport::new(&[0u8; 32], 1776)?); + let storage = Arc::new( + PostgresStore::new( + "postgres://loco:loco@localhost:5432/sons-of-liberty_development", + false, + ) + .await?, + ); + let oracle_client = + Arc::new(KormirOracleClient::new("https://kormir.dlcdevkit.com", None).await?); + + let mut seed_bytes = [0u8; 32]; + seed_bytes.try_fill(&mut bitcoin::key::rand::thread_rng())?; + + let mut builder = Builder::new(); + builder.set_seed_bytes(seed_bytes); + builder.set_transport(transport.clone()); + builder.set_storage(storage.clone()); + builder.set_oracle(oracle_client.clone()); + + let ddk: ApplicationDdk = builder.finish().await?; + + ddk.start().expect("couldn't start ddk"); + + Ok(()) +} diff --git a/ddk/src/storage/postgres/migrations/0002_bdk_wallet.down.sql b/ddk/src/storage/postgres/migrations/0002_bdk_wallet.down.sql new file mode 100644 index 00000000..6e1a10c0 --- /dev/null +++ b/ddk/src/storage/postgres/migrations/0002_bdk_wallet.down.sql @@ -0,0 +1,7 @@ +DROP TABLE IF EXISTS anchor_tx; +DROP TABLE IF EXISTS txout; +DROP TABLE IF EXISTS tx; +DROP TABLE IF EXISTS block; +DROP TABLE IF EXISTS keychain; +DROP TABLE IF EXISTS network; +DROP TABLE IF EXISTS version; \ No newline at end of file diff --git a/ddk/src/storage/postgres/migrations/0002_bdk_wallet.up.sql b/ddk/src/storage/postgres/migrations/0002_bdk_wallet.up.sql new file mode 100644 index 00000000..d7d806a9 --- /dev/null +++ b/ddk/src/storage/postgres/migrations/0002_bdk_wallet.up.sql @@ -0,0 +1,73 @@ +-- Schema version control +CREATE TABLE IF NOT EXISTS version ( + version INTEGER PRIMARY KEY +); + +-- Network is the valid network for all other table data +CREATE TABLE IF NOT EXISTS network ( + wallet_name TEXT PRIMARY KEY, + name TEXT NOT NULL +); + +-- Keychain is the json serialized keychain structure as JSONB, +-- descriptor is the complete descriptor string, +-- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum, +-- last revealed index is a u32 +CREATE TABLE IF NOT EXISTS keychain ( + wallet_name TEXT NOT NULL, + keychainkind TEXT NOT NULL, + descriptor TEXT NOT NULL, + descriptor_id BYTEA NOT NULL, + last_revealed INTEGER DEFAULT 0, + PRIMARY KEY (wallet_name, keychainkind) + +); + +-- Hash is block hash hex string, +-- Block height is a u32 +CREATE TABLE IF NOT EXISTS block ( + wallet_name TEXT NOT NULL, + hash TEXT NOT NULL, + height INTEGER NOT NULL, + PRIMARY KEY (wallet_name, hash) +); +CREATE INDEX idx_block_height ON block (height); + +-- Txid is transaction hash hex string (reversed) +-- Whole_tx is a consensus encoded transaction, +-- Last seen is a u64 unix epoch seconds +CREATE TABLE IF NOT EXISTS tx ( + wallet_name TEXT NOT NULL, + txid TEXT NOT NULL, + whole_tx BYTEA, + last_seen BIGINT, + PRIMARY KEY (wallet_name, txid) +); + +-- Outpoint txid hash hex string (reversed) +-- Outpoint vout +-- TxOut value as SATs +-- TxOut script consensus encoded +CREATE TABLE IF NOT EXISTS txout ( + wallet_name TEXT NOT NULL, + txid TEXT NOT NULL, + vout INTEGER NOT NULL, + value BIGINT NOT NULL, + script BYTEA NOT NULL, + PRIMARY KEY (wallet_name, txid, vout) +); + +-- Join table between anchor and tx +-- Block hash hex string +-- Anchor is a json serialized Anchor structure as JSONB, +-- Txid is transaction hash hex string (reversed) +CREATE TABLE IF NOT EXISTS anchor_tx ( + wallet_name TEXT NOT NULL, + block_hash TEXT NOT NULL, + anchor JSONB NOT NULL, + txid TEXT NOT NULL, + PRIMARY KEY (wallet_name, block_hash, txid), + FOREIGN KEY (wallet_name, block_hash) REFERENCES block(wallet_name, hash), + FOREIGN KEY (wallet_name, txid) REFERENCES tx(wallet_name, txid) +); +CREATE INDEX idx_anchor_tx_txid ON anchor_tx (txid); \ No newline at end of file diff --git a/ddk/src/storage/postgres/mod.rs b/ddk/src/storage/postgres/mod.rs index fde529b6..95953230 100644 --- a/ddk/src/storage/postgres/mod.rs +++ b/ddk/src/storage/postgres/mod.rs @@ -1,25 +1,32 @@ use super::sqlx::SqlxError; +use crate::error::WalletError; +use crate::transport::PeerInformation; +use crate::Storage; use crate::{ error::to_storage_error, storage::sqlx::ContractRow, util::ser::{deserialize_contract, serialize_contract, ContractPrefix}, }; +use bdk_sqlx::Store; +use bdk_wallet::ChangeSet; use ddk_manager::{ contract::{ offered_contract::OfferedContract, ser::Serializable, signed_contract::SignedContract, Contract, PreClosedContract, }, - Storage, + Storage as ManagerStorage, }; -use sqlx::{Database, Pool, Postgres}; +use dlc_messages::oracle_msgs::OracleAnnouncement; +use sqlx::{Pool, Postgres}; /// Manages a pool of database connections. -#[derive(Debug, Clone)] -pub struct Store { - pub(crate) pool: Pool, +#[derive(Debug)] +pub struct PostgresStore { + pub(crate) pool: Pool, + pub(crate) bdk_pool: Store, } -impl Store { +impl PostgresStore { pub async fn new(url: &str, migrations: bool) -> Result { let pool = Pool::::connect(url).await?; if migrations { @@ -28,12 +35,43 @@ impl Store { .run(&pool) .await?; } - Ok(Self { pool }) + + let bdk_pool = Store::::new(pool.clone(), "bdk_store".to_string(), false) + .await + .unwrap(); + + Ok(Self { pool, bdk_pool }) + } +} + +impl Storage for PostgresStore { + fn initialize_bdk(&self) -> Result { + Ok(ChangeSet::default()) + } + + fn persist_bdk(&self, _changeset: &ChangeSet) -> Result<(), WalletError> { + Ok(()) + } + + fn list_peers(&self) -> anyhow::Result> { + unimplemented!() + } + + fn save_peer(&self, _peer: PeerInformation) -> anyhow::Result<()> { + unimplemented!() + } + + fn save_announcement(&self, _announcement: OracleAnnouncement) -> anyhow::Result<()> { + unimplemented!() + } + + fn get_marketplace_announcements(&self) -> anyhow::Result> { + unimplemented!() } } #[async_trait::async_trait] -impl Storage for Store { +impl ManagerStorage for PostgresStore { async fn get_contract( &self, id: &ddk_manager::ContractId, @@ -284,7 +322,7 @@ mod tests { #[tokio::test] async fn postgres() { - let store = Store::new( + let store = PostgresStore::new( "postgres://loco:loco@localhost:5432/sons-of-liberty_development", true, ) From 5ba37a942ee7ff3b002aed2583387cf510f5ae7e Mon Sep 17 00:00:00 2001 From: bennyhodl Date: Thu, 27 Mar 2025 09:42:06 -0400 Subject: [PATCH 2/5] update nostr & convert pubkey --- Cargo.lock | 29 ++++++++++++------ ddk/Cargo.toml | 8 ++--- ddk/src/nostr/mod.rs | 29 +++++++++++++++--- ddk/src/oracle/nostr.rs | 2 +- ddk/src/transport/nostr/mod.rs | 39 ++---------------------- ddk/src/transport/nostr/relay_handler.rs | 20 ++---------- 6 files changed, 53 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8429588..3f12f543 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2038,9 +2038,9 @@ dependencies = [ [[package]] name = "kormir" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49390cd20115267c6b9d60dffb2f68a9d0e83f6aca9ae336e3892f11a7b8ff6" +checksum = "5a8aea195e8ee01710359c7b45f438eddec83c5e2fba72cf43c42dbaf5327215" dependencies = [ "base64 0.13.1", "bitcoin", @@ -2153,6 +2153,12 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lru" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" + [[package]] name = "matchers" version = "0.1.0" @@ -2308,9 +2314,9 @@ dependencies = [ [[package]] name = "nostr" -version = "0.39.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d90b55eff1f0747d9e423972179672e1aacac3d3ccee4c1281147eaa90d6491e" +checksum = "2f900ddcdc28395759fcd44b18a03255e7deee8858551bfe5d5d5a07311d82ea" dependencies = [ "aes", "base64 0.22.1", @@ -2322,6 +2328,7 @@ dependencies = [ "chacha20poly1305", "getrandom", "instant", + "regex", "scrypt", "secp256k1", "serde", @@ -2332,23 +2339,25 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.39.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce07b47c77b8e5a856727885fe0ae47b9aa53d8d853a2190dd479b5a0d6e4f52" +checksum = "714512e4653f4e7c4f4abb50a0ac82257541b22087dee780b9e3d787276e88d4" dependencies = [ + "lru", "nostr", "tokio", ] [[package]] name = "nostr-relay-pool" -version = "0.39.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "211ac5bbdda1a8eec0c21814a838da832038767a5d354fe2fcc1ca438cae56fd" +checksum = "5bde07a729e0a1b306c9a07da81a0d1d55d0487316017090906f3b6660741b8d" dependencies = [ "async-utility", "async-wsocket", "atomic-destructor", + "lru", "negentropy 0.3.1", "negentropy 0.5.0", "nostr", @@ -2359,9 +2368,9 @@ dependencies = [ [[package]] name = "nostr-sdk" -version = "0.39.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baca581deb810a88bb51c54d1d7980f4506a64a3e9a19270829b406e47adf31" +checksum = "26238eee805d7dc3abcc8d570068c81cb4285b08e9db4d7999e54e20748c472e" dependencies = [ "async-utility", "nostr", diff --git a/ddk/Cargo.toml b/ddk/Cargo.toml index 55d7c566..7d4894d3 100644 --- a/ddk/Cargo.toml +++ b/ddk/Cargo.toml @@ -58,19 +58,19 @@ sled = { version = "0.34.7", optional = true } # Nostr transport dependencies base64 = { version = "0.13.0" , optional = true } -nostr-rs = { package = "nostr", version = "0.39.0", features = ["std", "nip04"], optional = true } -nostr-sdk = { version = "0.39.0", optional = true } +nostr-rs = { package = "nostr", version = "0.40.0", features = ["std", "nip04"], optional = true } +nostr-sdk = { version = "0.40.0", optional = true } # lightning transport lightning-net-tokio = { version = "0.0.125", optional = true } # oracle feature reqwest = { version = "0.12.9", features = ["json"], optional = true } -kormir = "0.4.3" +kormir = "0.4.4" # kormir = { path = "../../kormir/kormir" } hmac = "0.12.1" sha2 = "0.10" -nostr-database = { version = "0.39.0", optional = true } +nostr-database = { version = "0.40.0", optional = true } sqlx = { version = "0.8.3", optional = true, features = ["runtime-tokio", "time", "bigdecimal", "chrono"] } bdk_sqlx = { version = "0.1.0", path = "../../bdk-sqlx", optional = true } diff --git a/ddk/src/nostr/mod.rs b/ddk/src/nostr/mod.rs index 6d07d00c..aa3fd937 100644 --- a/ddk/src/nostr/mod.rs +++ b/ddk/src/nostr/mod.rs @@ -1,9 +1,9 @@ +use bitcoin::key::Parity; use bitcoin::secp256k1::PublicKey as BitcoinPublicKey; use dlc_messages::oracle_msgs::{OracleAnnouncement, OracleAttestation}; use lightning::io::Cursor; use lightning::util::ser::Readable; -use nostr_rs::key::PublicKey; -use nostr_rs::{Filter, Kind, PublicKey as NostrPublicKey, Timestamp}; +use nostr_rs::{Filter, Kind, PublicKey, Timestamp}; /// Nostr [dlc_messages::oracle_msgs::OracleAnnouncement] marketplace. #[cfg(feature = "marketplace")] @@ -24,11 +24,11 @@ pub fn bitcoin_to_nostr_pubkey(bitcoin_pk: &BitcoinPublicKey) -> PublicKey { } pub fn nostr_to_bitcoin_pubkey(nostr_pk: &PublicKey) -> BitcoinPublicKey { - BitcoinPublicKey::from_slice(nostr_pk.as_bytes()) - .expect("Should not fail converting nostr key to bitcoin key.") + let xonly = nostr_pk.xonly().expect("Could not get xonly public key."); + BitcoinPublicKey::from_x_only_public_key(xonly, Parity::Even) } -pub fn create_dlc_message_filter(since: Timestamp, public_key: NostrPublicKey) -> Filter { +pub fn create_dlc_message_filter(since: Timestamp, public_key: PublicKey) -> Filter { Filter::new() .kind(DLC_MESSAGE_KIND) .since(since) @@ -54,3 +54,22 @@ pub fn oracle_attestation_from_str(content: &str) -> anyhow::Result BitcoinPublicKey { - nostr::nostr_to_bitcoin_pubkey(&self.keys.public_key) + nostr::nostr_to_bitcoin_pubkey(&self.keys.public_key()) } /// Get messages that have not been processed yet. @@ -47,7 +45,7 @@ impl Transport for NostrDlc { let event = nostr::messages::create_dlc_msg_event(nostr_counterparty, None, message, &self.keys) .unwrap(); - match self.client.send_event(event).await { + match self.client.send_event(&event).await { Err(e) => tracing::error!(error = e.to_string(), "Failed to send nostr event."), Ok(e) => tracing::info!(event_id = e.val.to_string(), "Sent DLC message event."), } @@ -60,36 +58,3 @@ impl Transport for NostrDlc { } } } - -fn bitcoin_to_nostr_pubkey(bitcoin_pk: &BitcoinPublicKey) -> PublicKey { - // Convert to XOnlyPublicKey first - let (xonly, _parity) = bitcoin_pk.x_only_public_key(); - - // Create nostr public key from the x-only bytes - PublicKey::from_slice(xonly.serialize().as_slice()) - .expect("Could not convert Bitcoin key to nostr key.") -} - -fn nostr_to_bitcoin_pubkey(nostr_pk: &PublicKey) -> BitcoinPublicKey { - let xonly = nostr_pk.xonly().expect("Could not get xonly public key."); - BitcoinPublicKey::from_x_only_public_key(xonly, Parity::Even) -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::*; - - #[test] - fn test_nostr_to_bitcoin_pubkey() { - let nostr_pk = "7622b0ca2b5ad4d7441784a97bfc50c69d09853a640ad793a4fb9d238c7e0b15"; - let bitcoin_pk = "027622b0ca2b5ad4d7441784a97bfc50c69d09853a640ad793a4fb9d238c7e0b15"; - let nostr_pk_2 = bitcoin_to_nostr_pubkey(&BitcoinPublicKey::from_str(bitcoin_pk).unwrap()); - assert_eq!(nostr_pk_2.to_string(), nostr_pk); - - let nostr = PublicKey::from_str(nostr_pk).unwrap(); - let btc_pk = nostr_to_bitcoin_pubkey(&nostr); - assert_eq!(btc_pk.to_string(), bitcoin_pk); - } -} diff --git a/ddk/src/transport/nostr/relay_handler.rs b/ddk/src/transport/nostr/relay_handler.rs index d8b66a20..47471115 100644 --- a/ddk/src/transport/nostr/relay_handler.rs +++ b/ddk/src/transport/nostr/relay_handler.rs @@ -1,11 +1,10 @@ use std::sync::Arc; -use crate::nostr; use crate::nostr::messages::{create_dlc_msg_event, handle_dlc_msg_event}; use crate::DlcDevKitDlcManager; +use crate::{nostr, Transport}; use crate::{Oracle, Storage}; use bitcoin::bip32::Xpriv; -use bitcoin::secp256k1::PublicKey as BitcoinPublicKey; use bitcoin::Network; use nostr_rs::{secp256k1::Secp256k1, Keys, Timestamp, Url}; use nostr_sdk::{Client, RelayPoolNotification}; @@ -41,19 +40,6 @@ impl NostrDlc { }) } - pub fn transport_public_key(&self) -> BitcoinPublicKey { - // Get the bytes from nostr public key - let pk_bytes = self.keys.public_key().to_bytes(); - - // Convert to XOnlyPublicKey first - let xonly = bitcoin::secp256k1::XOnlyPublicKey::from_slice(&pk_bytes) - .expect("Converting from nostr public key to bitcoin public key should not fail."); - - // Convert to full PublicKey (this will assume even y coordinate) - - BitcoinPublicKey::from_x_only_public_key(xonly, bitcoin::key::Parity::Even) - } - pub fn start( &self, mut stop_signal: watch::Receiver, @@ -61,7 +47,7 @@ impl NostrDlc { ) -> JoinHandle> { tracing::info!( pubkey = self.keys.public_key().to_string(), - transport_public_key = self.transport_public_key().to_string(), + transport_public_key = self.public_key().to_string(), "Starting Nostr DLC listener." ); let nostr_client = self.client.clone(); @@ -115,7 +101,7 @@ impl NostrDlc { ) .expect("no message"); nostr_client - .send_event(event) + .send_event(&event) .await .expect("Break out into functions."); } From a0538a30895ac6d931ba9e9cf3945f88eec998bc Mon Sep 17 00:00:00 2001 From: bennyhodl Date: Thu, 27 Mar 2025 10:10:26 -0400 Subject: [PATCH 3/5] async wallet storage --- ddk-manager/tests/manager_tests.rs | 1 + ddk-manager/tests/test_utils.rs | 3 +- ddk/examples/postgres.rs | 3 +- ddk/src/builder.rs | 17 +++--- ddk/src/lib.rs | 6 +- ddk/src/storage/memory.rs | 5 +- ddk/src/storage/sled/mod.rs | 5 +- ddk/src/wallet.rs | 89 +++++++++++++++++++----------- ddk/tests/balance.rs | 2 +- ddk/tests/nostr.rs | 8 +-- ddk/tests/test_util.rs | 16 +++++- 11 files changed, 99 insertions(+), 56 deletions(-) diff --git a/ddk-manager/tests/manager_tests.rs b/ddk-manager/tests/manager_tests.rs index 384defa1..538f848b 100644 --- a/ddk-manager/tests/manager_tests.rs +++ b/ddk-manager/tests/manager_tests.rs @@ -39,6 +39,7 @@ async fn get_manager() -> TestManager { Network::Regtest, store.clone(), ) + .await .unwrap(), ); diff --git a/ddk-manager/tests/test_utils.rs b/ddk-manager/tests/test_utils.rs index f91dc4bf..498cbf52 100644 --- a/ddk-manager/tests/test_utils.rs +++ b/ddk-manager/tests/test_utils.rs @@ -649,9 +649,10 @@ pub async fn create_and_fund_wallet(name: &str) -> (DlcDevKitWallet, Arc Result<()> { let transport = Arc::new(LightningTransport::new(&[0u8; 32], 1776)?); let storage = Arc::new( PostgresStore::new( - "postgres://loco:loco@localhost:5432/sons-of-liberty_development", + &std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"), false, + "test".to_string(), ) .await?, ); diff --git a/ddk/src/builder.rs b/ddk/src/builder.rs index 58a028dc..97dff168 100644 --- a/ddk/src/builder.rs +++ b/ddk/src/builder.rs @@ -138,13 +138,16 @@ impl Builder { .clone() .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); - let wallet = Arc::new(DlcDevKitWallet::new( - &name, - &self.seed_bytes, - &self.esplora_host, - self.network, - storage.clone(), - )?); + let wallet = Arc::new( + DlcDevKitWallet::new( + &name, + &self.seed_bytes, + &self.esplora_host, + self.network, + storage.clone(), + ) + .await?, + ); let mut oracles = HashMap::new(); oracles.insert(oracle.get_public_key(), oracle.clone()); diff --git a/ddk/src/lib.rs b/ddk/src/lib.rs index 45e5501b..4ae0ffee 100644 --- a/ddk/src/lib.rs +++ b/ddk/src/lib.rs @@ -67,10 +67,10 @@ pub trait Transport: Send + Sync + 'static { #[async_trait] /// Storage for DLC contracts. pub trait Storage: ddk_manager::Storage + Send + Sync + 'static { - ///// Instantiate the storage for the BDK wallet. - fn initialize_bdk(&self) -> Result; + /// Instantiate the storage for the BDK wallet. + async fn initialize_bdk(&self) -> Result; /// Save changeset to the wallet storage. - fn persist_bdk(&self, changeset: &ChangeSet) -> Result<(), WalletError>; + async fn persist_bdk(&self, changeset: &ChangeSet) -> Result<(), WalletError>; /// Connected counterparties. fn list_peers(&self) -> anyhow::Result>; /// Persis counterparty. diff --git a/ddk/src/storage/memory.rs b/ddk/src/storage/memory.rs index eb253af4..d8cf611a 100644 --- a/ddk/src/storage/memory.rs +++ b/ddk/src/storage/memory.rs @@ -29,6 +29,7 @@ impl MemoryStorage { } } +#[async_trait::async_trait] impl Storage for MemoryStorage { fn save_peer(&self, _peer: PeerInformation) -> anyhow::Result<()> { // self.peers.write().unwrap().insert(peer.id.clone(), peer); @@ -43,7 +44,7 @@ impl Storage for MemoryStorage { }]) } - fn persist_bdk( + async fn persist_bdk( &self, changeset: &bdk_wallet::ChangeSet, ) -> Result<(), crate::error::WalletError> { @@ -53,7 +54,7 @@ impl Storage for MemoryStorage { Ok(()) } - fn initialize_bdk(&self) -> Result { + async fn initialize_bdk(&self) -> Result { Ok(self.bdk_data.read().unwrap().clone().unwrap_or_default()) } diff --git a/ddk/src/storage/sled/mod.rs b/ddk/src/storage/sled/mod.rs index a65747bd..d2657678 100644 --- a/ddk/src/storage/sled/mod.rs +++ b/ddk/src/storage/sled/mod.rs @@ -95,8 +95,9 @@ impl SledStorage { } } +#[async_trait::async_trait] impl Storage for SledStorage { - fn persist_bdk(&self, changeset: &ChangeSet) -> Result<(), WalletError> { + async fn persist_bdk(&self, changeset: &ChangeSet) -> Result<(), WalletError> { let wallet_tree = self.wallet_tree().map_err(sled_to_wallet_error)?; let new_changeset = match wallet_tree .get(CHANGESET_KEY) @@ -116,7 +117,7 @@ impl Storage for SledStorage { Ok(()) } - fn initialize_bdk(&self) -> Result { + async fn initialize_bdk(&self) -> Result { tracing::info!("Initializing wallet persistance."); let changeset = match self .wallet_tree() diff --git a/ddk/src/wallet.rs b/ddk/src/wallet.rs index 62cb6d63..cfdd6b3a 100644 --- a/ddk/src/wallet.rs +++ b/ddk/src/wallet.rs @@ -6,8 +6,8 @@ use bdk_wallet::coin_selection::{ BranchAndBoundCoinSelection, CoinSelectionAlgorithm, SingleRandomDraw, }; use bdk_wallet::descriptor::IntoWalletDescriptor; +use bdk_wallet::AsyncWalletPersister; pub use bdk_wallet::LocalOutput; -use bdk_wallet::WalletPersister; use bdk_wallet::{ bitcoin::{ bip32::Xpriv, @@ -26,7 +26,9 @@ use bitcoin::{secp256k1::SecretKey, Amount, FeeRate, ScriptBuf, Transaction}; use ddk_manager::{error::Error as ManagerError, SimpleSigner}; use lightning::chain::chaininterface::{ConfirmationTarget, FeeEstimator}; use std::collections::HashMap; +use std::future::Future; use std::io::Write; +use std::pin::Pin; use std::sync::atomic::AtomicU32; // use std::sync::RwLock; use std::{ @@ -35,19 +37,34 @@ use std::{ }; use tokio::sync::Mutex; +type FutureResult<'a, T, E> = Pin> + Send + 'a>>; + /// Wrapper type to pass `crate::Storage` to a BDK wallet. #[derive(Clone)] pub struct WalletStorage(Arc); -impl WalletPersister for WalletStorage { +impl AsyncWalletPersister for WalletStorage { type Error = WalletError; - fn persist(persister: &mut Self, changeset: &bdk_wallet::ChangeSet) -> Result<(), Self::Error> { - persister.0.as_ref().persist_bdk(changeset) + fn initialize<'a>( + persister: &'a mut Self, + ) -> FutureResult<'a, bdk_wallet::ChangeSet, Self::Error> + where + Self: 'a, + { + tracing::info!("initialize store"); + Box::pin(persister.0.initialize_bdk()) } - fn initialize(persister: &mut Self) -> Result { - persister.0.as_ref().initialize_bdk() + fn persist<'a>( + persister: &'a mut Self, + changeset: &'a bdk_wallet::ChangeSet, + ) -> FutureResult<'a, (), Self::Error> + where + Self: 'a, + { + tracing::info!("persist store"); + Box::pin(persister.0.persist_bdk(changeset)) } } @@ -67,7 +84,7 @@ pub struct DlcDevKitWallet { const MIN_FEERATE: u32 = 253; impl DlcDevKitWallet { - pub fn new( + pub async fn new( name: &str, seed_bytes: &[u8; 32], esplora_url: &str, @@ -90,14 +107,16 @@ impl DlcDevKitWallet { .descriptor(KeychainKind::Internal, Some(internal_descriptor.clone())) .extract_keys() .check_network(network) - .load_wallet(&mut storage) + .load_wallet_async(&mut storage) + .await .map_err(|_| WalletError::WalletPersistanceError)?; let internal_wallet = match load_wallet { Some(w) => w, None => Wallet::create(external_descriptor, internal_descriptor) .network(network) - .create_wallet(&mut storage) + .create_wallet_async(&mut storage) + .await .map_err(|_| WalletError::WalletPersistanceError)?, }; @@ -201,7 +220,8 @@ impl DlcDevKitWallet { }; wallet.apply_update(sync_result)?; wallet - .persist(&mut storage) + .persist_async(&mut storage) + .await .map_err(|_| WalletError::WalletPersistanceError)?; Ok(()) } @@ -219,27 +239,25 @@ impl DlcDevKitWallet { Ok(wallet.balance()) } - pub fn new_external_address(&self) -> Result { + pub async fn new_external_address(&self) -> Result { let Ok(mut wallet) = self.wallet.try_lock() else { tracing::error!("Could not get lock to sync wallet."); return Err(WalletError::Lock); }; let mut storage = self.storage.clone(); let address = wallet.next_unused_address(KeychainKind::External); - let _ = wallet.persist(&mut storage); + let _ = wallet.persist_async(&mut storage).await; Ok(address) } - pub fn new_change_address(&self) -> Result { + pub async fn new_change_address(&self) -> Result { let Ok(mut wallet) = self.wallet.try_lock() else { tracing::error!("Could not get lock to sync wallet."); return Err(WalletError::Lock); }; let mut storage = self.storage.clone(); let address = wallet.next_unused_address(KeychainKind::Internal); - wallet - .persist(&mut storage) - .map_err(|_| WalletError::WalletPersistanceError)?; + let _ = wallet.persist_async(&mut storage).await; Ok(address) } @@ -378,9 +396,11 @@ impl ddk_manager::ContractSignerProvider for DlcDevKitWallet { impl ddk_manager::Wallet for DlcDevKitWallet { fn get_new_address(&self) -> Result { - let address = self - .new_external_address() - .map_err(wallet_err_to_manager_err)?; + let address = tokio::task::block_in_place(|| { + // This runs in a blocking context but yields to the runtime + tokio::runtime::Handle::current().block_on(self.new_external_address()) + }) + .map_err(wallet_err_to_manager_err)?; tracing::info!( address = address.address.to_string(), "Revealed new address for contract." @@ -389,9 +409,11 @@ impl ddk_manager::Wallet for DlcDevKitWallet { } fn get_new_change_address(&self) -> Result { - let address = self - .new_change_address() - .map_err(wallet_err_to_manager_err)?; + let address = tokio::task::block_in_place(|| { + // This runs in a blocking context but yields to the runtime + tokio::runtime::Handle::current().block_on(self.new_change_address()) + }) + .map_err(wallet_err_to_manager_err)?; tracing::info!( address = address.address.to_string(), "Revealed new change address for contract." @@ -502,7 +524,7 @@ mod tests { use super::DlcDevKitWallet; - fn create_wallet() -> DlcDevKitWallet { + async fn create_wallet() -> DlcDevKitWallet { let storage = Arc::new(MemoryStorage::new()); let mut entropy = [0u8; 64]; entropy @@ -516,6 +538,7 @@ mod tests { Network::Regtest, storage.clone(), ) + .await .unwrap() } @@ -552,16 +575,16 @@ mod tests { generate_blocks(5) } - #[test] - fn address_is_p2wpkh() { - let test = create_wallet(); - let address = test.new_external_address().unwrap(); + #[tokio::test] + async fn address_is_p2wpkh() { + let test = create_wallet().await; + let address = test.new_external_address().await.unwrap(); assert_eq!(address.address.address_type().unwrap(), AddressType::P2wpkh) } - #[test] - fn derive_contract_signer() { - let test = create_wallet(); + #[tokio::test] + async fn derive_contract_signer() { + let test = create_wallet().await; let mut temp_key_id = [0u8; 32]; temp_key_id .try_fill(&mut bitcoin::key::rand::thread_rng()) @@ -573,15 +596,15 @@ mod tests { #[tokio::test] async fn send_all() { - let wallet = create_wallet(); + let wallet = create_wallet().await; let network = wallet.blockchain.get_network().unwrap(); let address = match network { Network::Regtest => "bcrt1qt0yrvs7qx8guvpqsx8u9mypz6t4zr3pxthsjkm", Network::Signet => "bcrt1q7h9uzwvyw29vrpujp69l7kce7e5w98mpn8kwsp", _ => "bcrt1qt0yrvs7qx8guvpqsx8u9mypz6t4zr3pxthsjkm", }; - let addr_one = wallet.new_external_address().unwrap().address; - let addr_two = wallet.new_external_address().unwrap().address; + let addr_one = wallet.new_external_address().await.unwrap().address; + let addr_two = wallet.new_external_address().await.unwrap().address; fund_address(&addr_one); fund_address(&addr_two); wallet.sync().await.unwrap(); diff --git a/ddk/tests/balance.rs b/ddk/tests/balance.rs index dea82215..205642c8 100644 --- a/ddk/tests/balance.rs +++ b/ddk/tests/balance.rs @@ -27,7 +27,7 @@ async fn contract_balance() { .await .unwrap(); - let address = bob.ddk.wallet.new_external_address().unwrap().address; + let address = bob.ddk.wallet.new_external_address().await.unwrap().address; let auth = bitcoincore_rpc::Auth::UserPass("ddk".to_string(), "ddk".to_string()); let client = bitcoincore_rpc::Client::new("http://127.0.0.1:18443", auth).unwrap(); diff --git a/ddk/tests/nostr.rs b/ddk/tests/nostr.rs index f978bb23..1fb17ea4 100644 --- a/ddk/tests/nostr.rs +++ b/ddk/tests/nostr.rs @@ -5,11 +5,11 @@ mod nostr_test { use super::*; use bitcoin::{key::rand::Fill, Network}; use chrono::{Local, TimeDelta}; - use ddk::builder::Builder; use ddk::oracle::memory::MemoryOracle; use ddk::storage::memory::MemoryStorage; use ddk::transport::nostr::NostrDlc; use ddk::DlcDevKit; + use ddk::{builder::Builder, Transport}; use dlc::{EnumerationPayout, Payout}; use std::sync::Arc; @@ -53,8 +53,8 @@ mod nostr_test { alice.start().unwrap(); bob.start().unwrap(); - let alice_address = alice.wallet.new_external_address().unwrap().address; - let bob_address = bob.wallet.new_external_address().unwrap().address; + let alice_address = alice.wallet.new_external_address().await.unwrap().address; + let bob_address = bob.wallet.new_external_address().await.unwrap().address; test_util::fund_addresses(&alice_address, &bob_address); let expiry = TimeDelta::seconds(15); @@ -98,7 +98,7 @@ mod nostr_test { oracle.oracle.public_key().to_string(), EVENT_ID.to_string(), ); - let alice_pubkey = alice.transport.transport_public_key(); + let alice_pubkey = alice.transport.public_key(); let _offer = bob .send_dlc_offer(&contract_input, alice_pubkey, vec![announcement]) .await diff --git a/ddk/tests/test_util.rs b/ddk/tests/test_util.rs index 1bfe67cf..5a229899 100644 --- a/ddk/tests/test_util.rs +++ b/ddk/tests/test_util.rs @@ -32,8 +32,20 @@ pub async fn test_ddk() -> (TestSuite, TestSuite, Arc) { let test = TestSuite::new(&secp, "send_offer", oracle.clone()).await; let test_two = TestSuite::new(&secp, "sender_offer_two", oracle.clone()).await; - let node_one_address = test.ddk.wallet.new_external_address().unwrap().address; - let node_two_address = test_two.ddk.wallet.new_external_address().unwrap().address; + let node_one_address = test + .ddk + .wallet + .new_external_address() + .await + .unwrap() + .address; + let node_two_address = test_two + .ddk + .wallet + .new_external_address() + .await + .unwrap() + .address; fund_addresses(&node_one_address, &node_two_address); From da9c4c88af68b4d2322dc62a781ba62ec43bba99 Mon Sep 17 00:00:00 2001 From: bennyhodl Date: Thu, 27 Mar 2025 10:10:43 -0400 Subject: [PATCH 4/5] postgres module with sqlx --- ...8ce569a84a63b7206d46633c58bd58f59e468.json | 86 +++ ...820ce6e88479b87ccd2f96493e088f085aa0e.json | 86 +++ ...bdbaddd092066514180f13a76be6a0468ed2f.json | 88 +++ ...5c1a2669d7fde1d96a96d3f0bce4f18aa32ba.json | 14 + ...768ef3a51433545853b1a9e0c574bfbb84a84.json | 86 +++ ...5b3005212d70e3a528858e620b52ff0ea7d09.json | 80 +++ ...e73c0fce4c3d5f70404aec161d04975c7b6dd.json | 25 + ...56df7a924b1b1c25c3d7154b7ed64497424e9.json | 0 ...556d7a8afecdd3807f863f8c7453e1ded1d7e.json | 80 +++ ...f1420bf2749cc771a4aaf7ee1af447a60a925.json | 86 +++ ...e91f205d02c2d549b5f8e3fa476e6fd140e98.json | 86 +++ Cargo.lock | 36 -- ddk-node/Cargo.toml | 2 +- ddk-node/src/lib.rs | 17 +- ddk-node/src/opts.rs | 8 + ddk/Cargo.toml | 5 +- ddk/src/storage/mod.rs | 2 +- ddk/src/storage/postgres/mod.rs | 606 +++++++++++++++++- ddk/src/storage/sqlx.rs | 18 + ddk/src/util/ser.rs | 27 + justfile | 8 +- 21 files changed, 1362 insertions(+), 84 deletions(-) create mode 100644 .sqlx/query-03e8e2e31dc2cd64e751b07d2108ce569a84a63b7206d46633c58bd58f59e468.json create mode 100644 .sqlx/query-196e6d0e07716dac64c02932819820ce6e88479b87ccd2f96493e088f085aa0e.json create mode 100644 .sqlx/query-1ddd4edc610d16ad75c1b7006dfbdbaddd092066514180f13a76be6a0468ed2f.json create mode 100644 .sqlx/query-52ed4c0dcbb5634574a75c11bb95c1a2669d7fde1d96a96d3f0bce4f18aa32ba.json create mode 100644 .sqlx/query-563ff97595e5312c7f0233394f3768ef3a51433545853b1a9e0c574bfbb84a84.json create mode 100644 .sqlx/query-6d3b30b69d37e751f617578e8285b3005212d70e3a528858e620b52ff0ea7d09.json create mode 100644 .sqlx/query-904700b1c4cb51b0b2620d30acbe73c0fce4c3d5f70404aec161d04975c7b6dd.json rename {ddk/.sqlx => .sqlx}/query-97cd141835dda147b093709959356df7a924b1b1c25c3d7154b7ed64497424e9.json (100%) create mode 100644 .sqlx/query-a41467ffcd6821838eda59888f4556d7a8afecdd3807f863f8c7453e1ded1d7e.json create mode 100644 .sqlx/query-c76c564743999400c5349536528f1420bf2749cc771a4aaf7ee1af447a60a925.json create mode 100644 .sqlx/query-fa6cae992997a789b9362bdfdc7e91f205d02c2d549b5f8e3fa476e6fd140e98.json diff --git a/.sqlx/query-03e8e2e31dc2cd64e751b07d2108ce569a84a63b7206d46633c58bd58f59e468.json b/.sqlx/query-03e8e2e31dc2cd64e751b07d2108ce569a84a63b7206d46633c58bd58f59e468.json new file mode 100644 index 00000000..ccf02adc --- /dev/null +++ b/.sqlx/query-03e8e2e31dc2cd64e751b07d2108ce569a84a63b7206d46633c58bd58f59e468.json @@ -0,0 +1,86 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM contracts WHERE state = 5", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "state", + "type_info": "Int2" + }, + { + "ordinal": 2, + "name": "is_offer_party", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "counter_party", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "offer_collateral", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "accept_collateral", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_collateral", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "fee_rate_per_vb", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "cet_locktime", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "refund_locktime", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "pnl", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "contract_data", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false + ] + }, + "hash": "03e8e2e31dc2cd64e751b07d2108ce569a84a63b7206d46633c58bd58f59e468" +} diff --git a/.sqlx/query-196e6d0e07716dac64c02932819820ce6e88479b87ccd2f96493e088f085aa0e.json b/.sqlx/query-196e6d0e07716dac64c02932819820ce6e88479b87ccd2f96493e088f085aa0e.json new file mode 100644 index 00000000..ae1a8689 --- /dev/null +++ b/.sqlx/query-196e6d0e07716dac64c02932819820ce6e88479b87ccd2f96493e088f085aa0e.json @@ -0,0 +1,86 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM contracts", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "state", + "type_info": "Int2" + }, + { + "ordinal": 2, + "name": "is_offer_party", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "counter_party", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "offer_collateral", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "accept_collateral", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_collateral", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "fee_rate_per_vb", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "cet_locktime", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "refund_locktime", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "pnl", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "contract_data", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false + ] + }, + "hash": "196e6d0e07716dac64c02932819820ce6e88479b87ccd2f96493e088f085aa0e" +} diff --git a/.sqlx/query-1ddd4edc610d16ad75c1b7006dfbdbaddd092066514180f13a76be6a0468ed2f.json b/.sqlx/query-1ddd4edc610d16ad75c1b7006dfbdbaddd092066514180f13a76be6a0468ed2f.json new file mode 100644 index 00000000..f466162b --- /dev/null +++ b/.sqlx/query-1ddd4edc610d16ad75c1b7006dfbdbaddd092066514180f13a76be6a0468ed2f.json @@ -0,0 +1,88 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM contracts WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "state", + "type_info": "Int2" + }, + { + "ordinal": 2, + "name": "is_offer_party", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "counter_party", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "offer_collateral", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "accept_collateral", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_collateral", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "fee_rate_per_vb", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "cet_locktime", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "refund_locktime", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "pnl", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "contract_data", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false + ] + }, + "hash": "1ddd4edc610d16ad75c1b7006dfbdbaddd092066514180f13a76be6a0468ed2f" +} diff --git a/.sqlx/query-52ed4c0dcbb5634574a75c11bb95c1a2669d7fde1d96a96d3f0bce4f18aa32ba.json b/.sqlx/query-52ed4c0dcbb5634574a75c11bb95c1a2669d7fde1d96a96d3f0bce4f18aa32ba.json new file mode 100644 index 00000000..92e51e40 --- /dev/null +++ b/.sqlx/query-52ed4c0dcbb5634574a75c11bb95c1a2669d7fde1d96a96d3f0bce4f18aa32ba.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM contracts WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "52ed4c0dcbb5634574a75c11bb95c1a2669d7fde1d96a96d3f0bce4f18aa32ba" +} diff --git a/.sqlx/query-563ff97595e5312c7f0233394f3768ef3a51433545853b1a9e0c574bfbb84a84.json b/.sqlx/query-563ff97595e5312c7f0233394f3768ef3a51433545853b1a9e0c574bfbb84a84.json new file mode 100644 index 00000000..addd7c02 --- /dev/null +++ b/.sqlx/query-563ff97595e5312c7f0233394f3768ef3a51433545853b1a9e0c574bfbb84a84.json @@ -0,0 +1,86 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM contracts WHERE state = 4", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "state", + "type_info": "Int2" + }, + { + "ordinal": 2, + "name": "is_offer_party", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "counter_party", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "offer_collateral", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "accept_collateral", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_collateral", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "fee_rate_per_vb", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "cet_locktime", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "refund_locktime", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "pnl", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "contract_data", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false + ] + }, + "hash": "563ff97595e5312c7f0233394f3768ef3a51433545853b1a9e0c574bfbb84a84" +} diff --git a/.sqlx/query-6d3b30b69d37e751f617578e8285b3005212d70e3a528858e620b52ff0ea7d09.json b/.sqlx/query-6d3b30b69d37e751f617578e8285b3005212d70e3a528858e620b52ff0ea7d09.json new file mode 100644 index 00000000..69216266 --- /dev/null +++ b/.sqlx/query-6d3b30b69d37e751f617578e8285b3005212d70e3a528858e620b52ff0ea7d09.json @@ -0,0 +1,80 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, state, is_offer_party, counter_party, offer_collateral, accept_collateral, total_collateral, fee_rate_per_vb, cet_locktime, refund_locktime, pnl FROM contracts", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "state", + "type_info": "Int2" + }, + { + "ordinal": 2, + "name": "is_offer_party", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "counter_party", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "offer_collateral", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "accept_collateral", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_collateral", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "fee_rate_per_vb", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "cet_locktime", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "refund_locktime", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "pnl", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true + ] + }, + "hash": "6d3b30b69d37e751f617578e8285b3005212d70e3a528858e620b52ff0ea7d09" +} diff --git a/.sqlx/query-904700b1c4cb51b0b2620d30acbe73c0fce4c3d5f70404aec161d04975c7b6dd.json b/.sqlx/query-904700b1c4cb51b0b2620d30acbe73c0fce4c3d5f70404aec161d04975c7b6dd.json new file mode 100644 index 00000000..decb6857 --- /dev/null +++ b/.sqlx/query-904700b1c4cb51b0b2620d30acbe73c0fce4c3d5f70404aec161d04975c7b6dd.json @@ -0,0 +1,25 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO contracts (\n id, state, is_offer_party, counter_party,\n offer_collateral, accept_collateral, total_collateral, fee_rate_per_vb, \n cet_locktime, refund_locktime, pnl, contract_data\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)\n ON CONFLICT (id)\n DO UPDATE SET\n id = EXCLUDED.id,\n state = EXCLUDED.state,\n contract_data = EXCLUDED.contract_data,\n pnl = EXCLUDED.pnl\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int2", + "Bool", + "Text", + "Int8", + "Int8", + "Int8", + "Int8", + "Int4", + "Int4", + "Int8", + "Bytea" + ] + }, + "nullable": [] + }, + "hash": "904700b1c4cb51b0b2620d30acbe73c0fce4c3d5f70404aec161d04975c7b6dd" +} diff --git a/ddk/.sqlx/query-97cd141835dda147b093709959356df7a924b1b1c25c3d7154b7ed64497424e9.json b/.sqlx/query-97cd141835dda147b093709959356df7a924b1b1c25c3d7154b7ed64497424e9.json similarity index 100% rename from ddk/.sqlx/query-97cd141835dda147b093709959356df7a924b1b1c25c3d7154b7ed64497424e9.json rename to .sqlx/query-97cd141835dda147b093709959356df7a924b1b1c25c3d7154b7ed64497424e9.json diff --git a/.sqlx/query-a41467ffcd6821838eda59888f4556d7a8afecdd3807f863f8c7453e1ded1d7e.json b/.sqlx/query-a41467ffcd6821838eda59888f4556d7a8afecdd3807f863f8c7453e1ded1d7e.json new file mode 100644 index 00000000..7a89e10e --- /dev/null +++ b/.sqlx/query-a41467ffcd6821838eda59888f4556d7a8afecdd3807f863f8c7453e1ded1d7e.json @@ -0,0 +1,80 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, state, is_offer_party, counter_party, offer_collateral, accept_collateral, total_collateral, fee_rate_per_vb, cet_locktime, refund_locktime, pnl FROM contracts WHERE state = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "state", + "type_info": "Int2" + }, + { + "ordinal": 2, + "name": "is_offer_party", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "counter_party", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "offer_collateral", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "accept_collateral", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_collateral", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "fee_rate_per_vb", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "cet_locktime", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "refund_locktime", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "pnl", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true + ] + }, + "hash": "a41467ffcd6821838eda59888f4556d7a8afecdd3807f863f8c7453e1ded1d7e" +} diff --git a/.sqlx/query-c76c564743999400c5349536528f1420bf2749cc771a4aaf7ee1af447a60a925.json b/.sqlx/query-c76c564743999400c5349536528f1420bf2749cc771a4aaf7ee1af447a60a925.json new file mode 100644 index 00000000..5461418a --- /dev/null +++ b/.sqlx/query-c76c564743999400c5349536528f1420bf2749cc771a4aaf7ee1af447a60a925.json @@ -0,0 +1,86 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM contracts WHERE state = 3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "state", + "type_info": "Int2" + }, + { + "ordinal": 2, + "name": "is_offer_party", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "counter_party", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "offer_collateral", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "accept_collateral", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_collateral", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "fee_rate_per_vb", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "cet_locktime", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "refund_locktime", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "pnl", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "contract_data", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false + ] + }, + "hash": "c76c564743999400c5349536528f1420bf2749cc771a4aaf7ee1af447a60a925" +} diff --git a/.sqlx/query-fa6cae992997a789b9362bdfdc7e91f205d02c2d549b5f8e3fa476e6fd140e98.json b/.sqlx/query-fa6cae992997a789b9362bdfdc7e91f205d02c2d549b5f8e3fa476e6fd140e98.json new file mode 100644 index 00000000..72c5ce6a --- /dev/null +++ b/.sqlx/query-fa6cae992997a789b9362bdfdc7e91f205d02c2d549b5f8e3fa476e6fd140e98.json @@ -0,0 +1,86 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM contracts WHERE state = 1 AND is_offer_party = false", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "state", + "type_info": "Int2" + }, + { + "ordinal": 2, + "name": "is_offer_party", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "counter_party", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "offer_collateral", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "accept_collateral", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_collateral", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "fee_rate_per_vb", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "cet_locktime", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "refund_locktime", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "pnl", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "contract_data", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false + ] + }, + "hash": "fa6cae992997a789b9362bdfdc7e91f205d02c2d549b5f8e3fa476e6fd140e98" +} diff --git a/Cargo.lock b/Cargo.lock index 3f12f543..5972f336 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,20 +385,6 @@ dependencies = [ "miniscript", ] -[[package]] -name = "bdk_sqlx" -version = "0.1.0" -dependencies = [ - "bdk_wallet", - "serde", - "serde_json", - "sqlx", - "thiserror 1.0.65", - "tokio", - "tracing", - "tracing-subscriber", -] - [[package]] name = "bdk_wallet" version = "1.0.0" @@ -994,7 +980,6 @@ dependencies = [ "base64 0.13.1", "bdk_chain", "bdk_esplora", - "bdk_sqlx", "bdk_wallet", "bitcoin", "bitcoincore-rpc", @@ -2081,7 +2066,6 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ - "cc", "pkg-config", "vcpkg", ] @@ -3567,8 +3551,6 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls 0.23.16", - "rustls-pemfile 2.2.0", "serde", "serde_json", "sha2", @@ -3579,8 +3561,6 @@ dependencies = [ "tokio-stream", "tracing", "url", - "uuid", - "webpki-roots 0.26.6", ] [[package]] @@ -3664,7 +3644,6 @@ dependencies = [ "thiserror 2.0.11", "time", "tracing", - "uuid", "whoami", ] @@ -3706,7 +3685,6 @@ dependencies = [ "thiserror 2.0.11", "time", "tracing", - "uuid", "whoami", ] @@ -3733,7 +3711,6 @@ dependencies = [ "time", "tracing", "url", - "uuid", ] [[package]] @@ -4243,16 +4220,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-serde" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" -dependencies = [ - "serde", - "tracing-core", -] - [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -4263,15 +4230,12 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", - "serde", - "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", - "tracing-serde", ] [[package]] diff --git a/ddk-node/Cargo.toml b/ddk-node/Cargo.toml index 596a7760..680f4c37 100644 --- a/ddk-node/Cargo.toml +++ b/ddk-node/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/bennyhodl/dlcdevkit" edition = "2021" [dependencies] -ddk = { version = "0.0.15", path = "../ddk", features = ["marketplace", "sled", "kormir", "nostr"] } +ddk = { version = "0.0.15", path = "../ddk", features = ["marketplace", "postgres", "kormir", "nostr"] } ddk-manager = { version = "0.7.3", path = "../ddk-manager", features = ["use-serde"] } ddk-payouts = { version = "0.0.15", path = "../payouts" } diff --git a/ddk-node/src/lib.rs b/ddk-node/src/lib.rs index c64d05e3..5fc71605 100644 --- a/ddk-node/src/lib.rs +++ b/ddk-node/src/lib.rs @@ -8,7 +8,7 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::{Address, Amount, FeeRate, Network}; use ddk::builder::Builder; use ddk::oracle::kormir::KormirOracleClient; -use ddk::storage::sled::SledStorage; +use ddk::storage::postgres::PostgresStore; use ddk::transport::nostr::NostrDlc; use ddk::util::ser::serialize_contract; use ddk::DlcDevKit; @@ -36,7 +36,7 @@ use tonic::Response; use tonic::Status; use tonic::{async_trait, Code}; -type Ddk = DlcDevKit; +type Ddk = DlcDevKit; #[derive(Clone)] pub struct DdkNode { @@ -75,9 +75,7 @@ impl DdkNode { .await?, ); - let storage = Arc::new(SledStorage::new( - storage_path.join("sled_db").to_str().unwrap(), - )?); + let storage = Arc::new(PostgresStore::new(&opts.postgres_url, true, opts.name).await?); // let oracle = Arc::new(P2PDOracleClient::new(&oracle_host).await?); let oracle = Arc::new(KormirOracleClient::new(&opts.oracle_host, None).await?); @@ -201,7 +199,13 @@ impl DdkRpc for DdkNode { _request: Request, ) -> Result, Status> { tracing::info!("Request for new wallet address"); - let address = self.node.wallet.new_external_address().unwrap().to_string(); + let address = self + .node + .wallet + .new_external_address() + .await + .unwrap() + .to_string(); let response = NewAddressResponse { address }; Ok(Response::new(response)) } @@ -213,6 +217,7 @@ impl DdkRpc for DdkNode { ) -> Result, Status> { tracing::info!("Request for offers to the node."); let offers = self.node.storage.get_contract_offers().await.unwrap(); + tracing::info!("Offers: {:?}", offers); let offers: Vec> = offers .iter() .map(|offer| serde_json::to_vec(offer).unwrap()) diff --git a/ddk-node/src/opts.rs b/ddk-node/src/opts.rs index db928251..248cbb20 100644 --- a/ddk-node/src/opts.rs +++ b/ddk-node/src/opts.rs @@ -46,4 +46,12 @@ pub struct NodeOpts { #[arg(default_value = "file")] #[arg(value_parser = ["file", "bytes"])] pub seed: String, + #[arg(long)] + #[arg(help = "Name for the wallet.")] + #[arg(default_value = "ddk-node")] + pub name: String, + #[arg(long)] + #[arg(help = "Url for the postgres database connection.")] + #[arg(default_value = "postgres://dlcdevkit:dlcdevkit@localhost:5433/ddk_one")] + pub postgres_url: String, } diff --git a/ddk/Cargo.toml b/ddk/Cargo.toml index 7d4894d3..8e3786d5 100644 --- a/ddk/Cargo.toml +++ b/ddk/Cargo.toml @@ -23,7 +23,7 @@ nostr-oracle = ["dep:nostr-database", "nostr", "kormir", "kormir/nostr"] # storage features sled = ["dep:sled"] -postgres = ["dep:sqlx", "sqlx/postgres", "dep:bdk_sqlx"] +postgres = ["dep:sqlx", "sqlx/postgres"] [dependencies] dlc = { version = "0.7.1", features = ["use-serde"] } @@ -55,6 +55,7 @@ crossbeam = "0.8.4" # storage features sled = { version = "0.34.7", optional = true } +sqlx = { version = "0.8.3", optional = true, features = ["runtime-tokio", "time", "bigdecimal", "chrono"] } # Nostr transport dependencies base64 = { version = "0.13.0" , optional = true } @@ -71,8 +72,6 @@ kormir = "0.4.4" hmac = "0.12.1" sha2 = "0.10" nostr-database = { version = "0.40.0", optional = true } -sqlx = { version = "0.8.3", optional = true, features = ["runtime-tokio", "time", "bigdecimal", "chrono"] } -bdk_sqlx = { version = "0.1.0", path = "../../bdk-sqlx", optional = true } [dev-dependencies] test-log = { version = "0.2.16", features = ["trace"] } diff --git a/ddk/src/storage/mod.rs b/ddk/src/storage/mod.rs index e99940cf..c4b5b41f 100644 --- a/ddk/src/storage/mod.rs +++ b/ddk/src/storage/mod.rs @@ -5,4 +5,4 @@ pub mod postgres; pub mod sled; #[cfg(feature = "postgres")] -mod sqlx; +pub mod sqlx; diff --git a/ddk/src/storage/postgres/mod.rs b/ddk/src/storage/postgres/mod.rs index 95953230..d25c44c5 100644 --- a/ddk/src/storage/postgres/mod.rs +++ b/ddk/src/storage/postgres/mod.rs @@ -1,4 +1,6 @@ -use super::sqlx::SqlxError; +use std::str::FromStr; + +use super::sqlx::{ContractRowNoBytes, SqlxError}; use crate::error::WalletError; use crate::transport::PeerInformation; use crate::Storage; @@ -7,8 +9,21 @@ use crate::{ storage::sqlx::ContractRow, util::ser::{deserialize_contract, serialize_contract, ContractPrefix}, }; -use bdk_sqlx::Store; +use bdk_chain::{ + local_chain, tx_graph, Anchor, ConfirmationBlockTime, DescriptorExt, DescriptorId, Merge, +}; +use bdk_wallet::bitcoin::{ + self, + consensus::{self, Decodable}, + hashes::Hash, + Amount, BlockHash, Network, OutPoint, ScriptBuf, TxOut, Txid, +}; +use bdk_wallet::chain as bdk_chain; +use bdk_wallet::descriptor::{Descriptor, ExtendedDescriptor}; +use bdk_wallet::keys::DescriptorPublicKey; use bdk_wallet::ChangeSet; +use bdk_wallet::KeychainKind; +use bdk_wallet::KeychainKind::{External, Internal}; use ddk_manager::{ contract::{ offered_contract::OfferedContract, ser::Serializable, signed_contract::SignedContract, @@ -17,17 +32,21 @@ use ddk_manager::{ Storage as ManagerStorage, }; use dlc_messages::oracle_msgs::OracleAnnouncement; -use sqlx::{Pool, Postgres}; +use serde_json::json; +use sqlx::postgres::PgRow; +use sqlx::{FromRow, Pool, Postgres, Row, Transaction}; +use std::sync::Arc; +use tracing::info; /// Manages a pool of database connections. #[derive(Debug)] pub struct PostgresStore { pub(crate) pool: Pool, - pub(crate) bdk_pool: Store, + wallet_name: String, } impl PostgresStore { - pub async fn new(url: &str, migrations: bool) -> Result { + pub async fn new(url: &str, migrations: bool, wallet_name: String) -> Result { let pool = Pool::::connect(url).await?; if migrations { tracing::info!("Migrating postgres"); @@ -36,37 +55,179 @@ impl PostgresStore { .await?; } - let bdk_pool = Store::::new(pool.clone(), "bdk_store".to_string(), false) - .await - .unwrap(); + Ok(Self { pool, wallet_name }) + } + + pub async fn get_contract_rows( + &self, + states: Option>, + ) -> Result, SqlxError> { + let rows = if let Some(states) = states { + let placeholders = (1..=states.len()) + .map(|i| format!("${}", i)) + .collect::>() + .join(", "); + + let query = format!("SELECT id, state, is_offer_party, counter_party, offer_collateral, accept_collateral, total_collateral, fee_rate_per_vb, cet_locktime, refund_locktime, pnl FROM contracts WHERE state IN ({})", placeholders); + + let mut query = sqlx::query_as::<_, ContractRowNoBytes>(&query); + + for state in states { + query = query.bind(state as i16); + } + + query.fetch_all(&self.pool).await? + } else { + sqlx::query_as!(ContractRowNoBytes, "SELECT id, state, is_offer_party, counter_party, offer_collateral, accept_collateral, total_collateral, fee_rate_per_vb, cet_locktime, refund_locktime, pnl FROM contracts") + .fetch_all(&self.pool) + .await? + }; + Ok(rows) + } + + pub async fn get_offer_rows(&self) -> Result, SqlxError> { + let rows = sqlx::query_as!(ContractRowNoBytes, "SELECT id, state, is_offer_party, counter_party, offer_collateral, accept_collateral, total_collateral, fee_rate_per_vb, cet_locktime, refund_locktime, pnl FROM contracts WHERE state = 1") + .fetch_all(&self.pool) + .await?; + Ok(rows) + } + + #[tracing::instrument] + pub(crate) async fn read(&self) -> Result { + let mut tx = self.pool.begin().await?; + let mut changeset = ChangeSet::default(); + let sql = + "SELECT n.name as network, + k_int.descriptor as internal_descriptor, k_int.last_revealed as internal_last_revealed, + k_ext.descriptor as external_descriptor, k_ext.last_revealed as external_last_revealed + FROM network n + LEFT JOIN keychain k_int ON n.wallet_name = k_int.wallet_name AND k_int.keychainkind = 'Internal' + LEFT JOIN keychain k_ext ON n.wallet_name = k_ext.wallet_name AND k_ext.keychainkind = 'External' + WHERE n.wallet_name = $1"; + + // Fetch wallet data + let row = sqlx::query(sql) + .bind(&self.wallet_name) + .fetch_optional(&mut *tx) + .await?; + + if let Some(row) = row { + Self::changeset_from_row(&mut tx, &mut changeset, row, &self.wallet_name).await?; + } + + Ok(changeset) + } + + #[tracing::instrument] + pub(crate) async fn changeset_from_row( + tx: &mut Transaction<'_, Postgres>, + changeset: &mut ChangeSet, + row: PgRow, + wallet_name: &str, + ) -> Result<(), SqlxError> { + tracing::info!("changeset from row"); + + let network: String = row.get("network"); + let internal_last_revealed: Option = row.get("internal_last_revealed"); + let external_last_revealed: Option = row.get("external_last_revealed"); + let internal_desc_str: Option = row.get("internal_descriptor"); + let external_desc_str: Option = row.get("external_descriptor"); + + changeset.network = Some(Network::from_str(&network).expect("parse Network")); + + if let Some(desc_str) = external_desc_str { + let descriptor: Descriptor = desc_str.parse()?; + let did = descriptor.descriptor_id(); + changeset.descriptor = Some(descriptor); + if let Some(last_rev) = external_last_revealed { + changeset.indexer.last_revealed.insert(did, last_rev as u32); + } + } + + if let Some(desc_str) = internal_desc_str { + let descriptor: Descriptor = desc_str.parse()?; + let did = descriptor.descriptor_id(); + changeset.change_descriptor = Some(descriptor); + if let Some(last_rev) = internal_last_revealed { + changeset.indexer.last_revealed.insert(did, last_rev as u32); + } + } - Ok(Self { pool, bdk_pool }) + changeset.tx_graph = tx_graph_changeset_from_postgres(tx, wallet_name).await?; + changeset.local_chain = local_chain_changeset_from_postgres(tx, wallet_name).await?; + Ok(()) + } + + #[tracing::instrument] + pub(crate) async fn write(&self, changeset: &ChangeSet) -> Result<(), SqlxError> { + tracing::info!("changeset write"); + if changeset.is_empty() { + return Ok(()); + } + + let wallet_name = &self.wallet_name; + let mut tx = self.pool.begin().await?; + + if let Some(ref descriptor) = changeset.descriptor { + insert_descriptor(&mut tx, wallet_name, descriptor, External).await?; + } + + if let Some(ref change_descriptor) = changeset.change_descriptor { + insert_descriptor(&mut tx, wallet_name, change_descriptor, Internal).await?; + } + + if let Some(network) = changeset.network { + insert_network(&mut tx, wallet_name, network).await?; + } + + let last_revealed_indices = &changeset.indexer.last_revealed; + if !last_revealed_indices.is_empty() { + for (desc_id, index) in last_revealed_indices { + update_last_revealed(&mut tx, wallet_name, *desc_id, *index).await?; + } + } + + local_chain_changeset_persist_to_postgres(&mut tx, wallet_name, &changeset.local_chain) + .await?; + tx_graph_changeset_persist_to_postgres(&mut tx, wallet_name, &changeset.tx_graph).await?; + + tx.commit().await?; + + Ok(()) } } +#[async_trait::async_trait] impl Storage for PostgresStore { - fn initialize_bdk(&self) -> Result { - Ok(ChangeSet::default()) + async fn initialize_bdk(&self) -> Result { + tracing::info!("initialize store"); + self.read() + .await + .map_err(|_| WalletError::StorageError("Did not initialize bdk storage".to_string())) } - fn persist_bdk(&self, _changeset: &ChangeSet) -> Result<(), WalletError> { - Ok(()) + async fn persist_bdk(&self, changeset: &ChangeSet) -> Result<(), WalletError> { + tracing::info!("persist store"); + + self.write(changeset) + .await + .map_err(|_| WalletError::StorageError("Did not persist bdk storage".to_string())) } fn list_peers(&self) -> anyhow::Result> { - unimplemented!() + unimplemented!("Not implemented to list peers") } fn save_peer(&self, _peer: PeerInformation) -> anyhow::Result<()> { - unimplemented!() + unimplemented!("Not implemented to save peer") } fn save_announcement(&self, _announcement: OracleAnnouncement) -> anyhow::Result<()> { - unimplemented!() + unimplemented!("Not implemented to save announcement") } fn get_marketplace_announcements(&self) -> anyhow::Result> { - unimplemented!() + unimplemented!("Not implemented to get marketplace announcements") } } @@ -158,19 +319,70 @@ impl ManagerStorage for PostgresStore { &self, contract: &ddk_manager::contract::Contract, ) -> Result<(), ddk_manager::error::Error> { + tracing::info!("Updating contract. {:?}", contract.get_id()); let prefix = ContractPrefix::get_prefix(contract); + let serialized_contract = serialize_contract(contract)?; + let contract_id = hex::encode(contract.get_id()); + let (offer_collateral, accept_collateral, total_collateral) = contract.get_collateral(); + + // Start a transaction + let mut tx = self.pool.begin().await.map_err(to_storage_error)?; + + // Step 1: Remove by temp_id if Accepted or Signed + match contract { + a @ Contract::Accepted(_) | a @ Contract::Signed(_) => { + tracing::info!( + "Deleting contract by temp_id: {:?}", + hex::encode(a.get_temporary_id()) + ); + let temp_id = hex::encode(a.get_temporary_id()); + sqlx::query_as!(ContractRow, "DELETE FROM contracts WHERE id = $1", temp_id) + .execute(&mut *tx) + .await + .map_err(to_storage_error)?; + } + _ => {} + } + + // Step 2: Upsert the contract by id sqlx::query_as!( ContractRow, - "UPDATE contracts SET state = $1, contract_data = $2, pnl = $3 WHERE id = $4", + r#" + INSERT INTO contracts ( + id, state, is_offer_party, counter_party, + offer_collateral, accept_collateral, total_collateral, fee_rate_per_vb, + cet_locktime, refund_locktime, pnl, contract_data + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + ON CONFLICT (id) + DO UPDATE SET + id = EXCLUDED.id, + state = EXCLUDED.state, + contract_data = EXCLUDED.contract_data, + pnl = EXCLUDED.pnl + "#, + contract_id, prefix as i16, - serialize_contract(&contract)?, + // need to track if the contract is offer party + false, + hex::encode(contract.get_counter_party_id().serialize()), + offer_collateral as i64, + accept_collateral as i64, + total_collateral as i64, + // need to get fee rate per vb + 1 as i64, + contract.get_cet_locktime() as i32, + contract.get_refund_locktime() as i32, Some(contract.get_pnl()), - hex::encode(contract.get_id()), + serialized_contract ) - .execute(&self.pool) + .execute(&mut *tx) .await .map_err(to_storage_error)?; + // Commit the transaction + tx.commit().await.map_err(to_storage_error)?; + Ok(()) } @@ -304,6 +516,297 @@ impl ManagerStorage for PostgresStore { } } +/// Insert keychain descriptors. +#[tracing::instrument] +async fn insert_descriptor( + tx: &mut Transaction<'_, Postgres>, + wallet_name: &str, + descriptor: &ExtendedDescriptor, + keychain: KeychainKind, +) -> Result<(), SqlxError> { + info!("insert descriptor"); + let descriptor_str = descriptor.to_string(); + + let descriptor_id = descriptor.descriptor_id().to_byte_array(); + let keychain = match keychain { + External => "External", + Internal => "Internal", + }; + + sqlx::query( + "INSERT INTO keychain (wallet_name, keychainkind, descriptor, descriptor_id) VALUES ($1, $2, $3, $4)", + ) + .bind(wallet_name) + .bind(keychain) + .bind(descriptor_str) + .bind(descriptor_id.as_slice()) + .execute(&mut **tx) + .await?; + + Ok(()) +} + +/// Insert network. +#[tracing::instrument] +async fn insert_network( + tx: &mut Transaction<'_, Postgres>, + wallet_name: &str, + network: Network, +) -> Result<(), SqlxError> { + info!("insert network"); + sqlx::query("INSERT INTO network (wallet_name, name) VALUES ($1, $2)") + .bind(wallet_name) + .bind(network.to_string()) + .execute(&mut **tx) + .await?; + + Ok(()) +} + +/// Update keychain last revealed +#[tracing::instrument] +async fn update_last_revealed( + tx: &mut Transaction<'_, Postgres>, + wallet_name: &str, + descriptor_id: DescriptorId, + last_revealed: u32, +) -> Result<(), SqlxError> { + info!("update last revealed"); + + sqlx::query( + "UPDATE keychain SET last_revealed = $1 WHERE wallet_name = $2 AND descriptor_id = $3", + ) + .bind(last_revealed as i32) + .bind(wallet_name) + .bind(descriptor_id.to_byte_array()) + .execute(&mut **tx) + .await?; + + Ok(()) +} + +/// Select transactions, txouts, and anchors. +#[tracing::instrument] +async fn tx_graph_changeset_from_postgres( + db_tx: &mut Transaction<'_, Postgres>, + wallet_name: &str, +) -> Result, SqlxError> { + info!("tx graph changeset from postgres"); + let mut changeset = tx_graph::ChangeSet::default(); + + // Fetch transactions + let rows = sqlx::query("SELECT txid, whole_tx, last_seen FROM tx WHERE wallet_name = $1") + .bind(wallet_name) + .fetch_all(&mut **db_tx) + .await?; + + for row in rows { + let txid: String = row.get("txid"); + let txid = Txid::from_str(&txid)?; + let whole_tx: Option> = row.get("whole_tx"); + let last_seen: Option = row.get("last_seen"); + + if let Some(tx_bytes) = whole_tx { + if let Ok(tx) = bitcoin::Transaction::consensus_decode(&mut tx_bytes.as_slice()) { + changeset.txs.insert(Arc::new(tx)); + } + } + if let Some(last_seen) = last_seen { + changeset.last_seen.insert(txid, last_seen as u64); + } + } + + // Fetch txouts + let rows = sqlx::query("SELECT txid, vout, value, script FROM txout WHERE wallet_name = $1") + .bind(wallet_name) + .fetch_all(&mut **db_tx) + .await?; + + for row in rows { + let txid: String = row.get("txid"); + let txid = Txid::from_str(&txid)?; + let vout: i32 = row.get("vout"); + let value: i64 = row.get("value"); + let script: Vec = row.get("script"); + + changeset.txouts.insert( + OutPoint { + txid, + vout: vout as u32, + }, + TxOut { + value: Amount::from_sat(value as u64), + script_pubkey: ScriptBuf::from(script), + }, + ); + } + + // Fetch anchors + let rows = sqlx::query("SELECT anchor, txid FROM anchor_tx WHERE wallet_name = $1") + .bind(wallet_name) + .fetch_all(&mut **db_tx) + .await?; + + for row in rows { + let anchor: serde_json::Value = row.get("anchor"); + let txid: String = row.get("txid"); + let txid = Txid::from_str(&txid)?; + + if let Ok(anchor) = serde_json::from_value::(anchor) { + changeset.anchors.insert((anchor, txid)); + } + } + + Ok(changeset) +} + +/// Insert transactions, txouts, and anchors. +#[tracing::instrument] +async fn tx_graph_changeset_persist_to_postgres( + db_tx: &mut Transaction<'_, Postgres>, + wallet_name: &str, + changeset: &tx_graph::ChangeSet, +) -> Result<(), SqlxError> { + info!("tx graph changeset from postgres"); + for tx in &changeset.txs { + sqlx::query( + "INSERT INTO tx (wallet_name, txid, whole_tx) VALUES ($1, $2, $3) + ON CONFLICT (wallet_name, txid) DO UPDATE SET whole_tx = $3", + ) + .bind(wallet_name) + .bind(tx.compute_txid().to_string()) + .bind(consensus::serialize(tx.as_ref())) + .execute(&mut **db_tx) + .await?; + } + + for (&txid, &last_seen) in &changeset.last_seen { + sqlx::query("UPDATE tx SET last_seen = $1 WHERE wallet_name = $2 AND txid = $3") + .bind(last_seen as i64) + .bind(wallet_name) + .bind(txid.to_string()) + .execute(&mut **db_tx) + .await?; + } + + for (op, txo) in &changeset.txouts { + sqlx::query( + "INSERT INTO txout (wallet_name, txid, vout, value, script) VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (wallet_name, txid, vout) DO UPDATE SET value = $4, script = $5", + ) + .bind(wallet_name) + .bind(op.txid.to_string()) + .bind(op.vout as i32) + .bind(txo.value.to_sat() as i64) + .bind(txo.script_pubkey.as_bytes()) + .execute(&mut **db_tx) + .await?; + } + + for (anchor, txid) in &changeset.anchors { + let block_hash = anchor.anchor_block().hash; + let anchor = serde_json::to_value(anchor)?; + sqlx::query( + "INSERT INTO anchor_tx (wallet_name, block_hash, anchor, txid) VALUES ($1, $2, $3, $4) + ON CONFLICT (wallet_name, block_hash, txid) DO UPDATE SET anchor = $3", + ) + .bind(wallet_name) + .bind(block_hash.to_string()) + .bind(anchor) + .bind(txid.to_string()) + .execute(&mut **db_tx) + .await?; + } + + Ok(()) +} + +/// Select blocks. +#[tracing::instrument] +async fn local_chain_changeset_from_postgres( + db_tx: &mut Transaction<'_, Postgres>, + wallet_name: &str, +) -> Result { + info!("local chain changeset from postgres"); + let mut changeset = local_chain::ChangeSet::default(); + + let rows = sqlx::query("SELECT hash, height FROM block WHERE wallet_name = $1") + .bind(wallet_name) + .fetch_all(&mut **db_tx) + .await?; + + for row in rows { + let hash: String = row.get("hash"); + let height: i32 = row.get("height"); + let block_hash = BlockHash::from_str(&hash)?; + changeset.blocks.insert(height as u32, Some(block_hash)); + } + + Ok(changeset) +} + +/// Insert blocks. +#[tracing::instrument] +async fn local_chain_changeset_persist_to_postgres( + db_tx: &mut Transaction<'_, Postgres>, + wallet_name: &str, + changeset: &local_chain::ChangeSet, +) -> Result<(), SqlxError> { + info!("local chain changeset to postgres"); + for (&height, &hash) in &changeset.blocks { + match hash { + Some(hash) => { + sqlx::query( + "INSERT INTO block (wallet_name, hash, height) VALUES ($1, $2, $3) + ON CONFLICT (wallet_name, hash) DO UPDATE SET height = $3", + ) + .bind(wallet_name) + .bind(hash.to_string()) + .bind(height as i32) + .execute(&mut **db_tx) + .await?; + } + None => { + sqlx::query("DELETE FROM block WHERE wallet_name = $1 AND height = $2") + .bind(wallet_name) + .bind(height as i32) + .execute(&mut **db_tx) + .await?; + } + } + } + + Ok(()) +} + +/// Collects information on all the wallets in the database and dumps it to stdout. +#[tracing::instrument] +async fn easy_backup(db: Pool) -> Result<(), SqlxError> { + info!("Starting easy backup"); + + let statement = "SELECT * FROM keychain"; + + let results = sqlx::query_as::<_, KeychainEntry>(statement) + .fetch_all(&db) + .await?; + + let json_array = json!(results); + println!("{}", serde_json::to_string_pretty(&json_array)?); + + info!("Easy backup completed successfully"); + Ok(()) +} + +/// Represents a row in the keychain table. +#[derive(serde::Serialize, FromRow)] +struct KeychainEntry { + wallet_name: String, + keychainkind: String, + descriptor: String, + descriptor_id: Vec, + last_revealed: i32, +} + #[cfg(test)] mod tests { use ddk_manager::contract::{ @@ -320,28 +823,65 @@ mod tests { T::deserialize(&mut cursor).unwrap() } - #[tokio::test] - async fn postgres() { + async fn seed_db() -> PostgresStore { let store = PostgresStore::new( - "postgres://loco:loco@localhost:5432/sons-of-liberty_development", + &std::env::var("DATABASE_URL").unwrap(), true, + "test".to_string(), ) .await .unwrap(); + let accept = include_bytes!("../../../tests/data/dlc_storage/Accepted"); + let accepted_contract = deserialize_object::(&accept.to_vec()); + store + .update_contract(&Contract::Accepted(accepted_contract)) + .await + .expect("Failed to update accepted contract"); let signed = include_bytes!("../../../tests/data/dlc_storage/Signed"); + let signed_contract = deserialize_object::(&signed.to_vec()); + store + .update_contract(&Contract::Signed(signed_contract)) + .await + .expect("Failed to update signed contract"); let confirmed = include_bytes!("../../../tests/data/dlc_storage/Confirmed"); + let confirmed_contract = deserialize_object::(&confirmed.to_vec()); + store + .update_contract(&Contract::Confirmed(confirmed_contract)) + .await + .expect("Failed to update confirmed contract"); let preclosed = include_bytes!("../../../tests/data/dlc_storage/PreClosed"); + let preclosed_contract = deserialize_object::(&preclosed.to_vec()); + store + .update_contract(&Contract::PreClosed(preclosed_contract)) + .await + .expect("Failed to update preclosed contract"); let offered = include_bytes!("../../../tests/data/dlc_storage/Offered"); + let offered_contract = deserialize_object::(&offered.to_vec()); + store + .update_contract(&Contract::Offered(offered_contract)) + .await + .expect("Failed to update offered contract"); - let accepted_contract = deserialize_object::(&accept.to_vec()); - let serialized = include_bytes!("../../../tests/data/dlc_storage/Offered"); - let offered_contract = deserialize_object::(&serialized.to_vec()); - let result = store.create_contract(&offered_contract).await; - assert!(result.is_ok()); - - let contract = store.get_contract(&offered_contract.id).await; - assert!(contract.is_ok()); - assert!(matches!(contract.unwrap().unwrap(), Contract::Offered(_))); + store + } + + #[tokio::test] + async fn postgres() { + let db = seed_db().await; + + let offer_rows = db.get_offer_rows().await.unwrap(); + assert_eq!(offer_rows.len(), 1); + assert_eq!(offer_rows[0].state, ContractPrefix::Offered as i16); + + let signed_prefix: ContractPrefix = "signed".to_string().into(); + let confirmed_prefix: ContractPrefix = "confirmed".to_string().into(); + let confirmed_rows = db + .get_contract_rows(Some(vec![signed_prefix, confirmed_prefix])) + .await + .unwrap(); + assert_eq!(confirmed_rows.len(), 2); + assert_eq!(confirmed_rows[0].state, ContractPrefix::Signed as i16); + assert_eq!(confirmed_rows[1].state, ContractPrefix::Confirmed as i16); } } diff --git a/ddk/src/storage/sqlx.rs b/ddk/src/storage/sqlx.rs index 7d5173bd..af7c1834 100644 --- a/ddk/src/storage/sqlx.rs +++ b/ddk/src/storage/sqlx.rs @@ -21,6 +21,9 @@ pub enum SqlxError { SerializeContract(#[from] bitcoin::io::Error), #[error("deserialize contract error: {0}")] DeserializeContract(#[from] ddk_manager::error::Error), + /// miniscript error + #[error("miniscript error: {0}")] + Miniscript(#[from] bdk_chain::miniscript::Error), } #[derive(Debug, Clone, FromRow, Serialize, Deserialize)] @@ -38,3 +41,18 @@ pub struct ContractRow { pub pnl: Option, pub contract_data: Vec, } + +#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] +pub struct ContractRowNoBytes { + pub id: String, + pub state: i16, + pub is_offer_party: bool, + pub counter_party: String, + pub offer_collateral: i64, + pub total_collateral: i64, + pub accept_collateral: i64, + pub fee_rate_per_vb: i64, + pub cet_locktime: i32, + pub refund_locktime: i32, + pub pnl: Option, +} diff --git a/ddk/src/util/ser.rs b/ddk/src/util/ser.rs index 22691527..532f6405 100644 --- a/ddk/src/util/ser.rs +++ b/ddk/src/util/ser.rs @@ -60,19 +60,46 @@ macro_rules! convertible_enum { convertible_enum!( enum ContractPrefix { Offered = 1, + // 2 Accepted, + // 3 Signed, + // 4 Confirmed, + // 5 PreClosed, + // 6 Closed, + // 7 FailedAccept, + // 8 FailedSign, + // 9 Refunded, + // 10 Rejected,; }, Contract ); +impl From for ContractPrefix { + fn from(s: String) -> Self { + match s.as_str() { + "offered" => ContractPrefix::Offered, + "accepted" => ContractPrefix::Accepted, + "signed" => ContractPrefix::Signed, + "confirmed" => ContractPrefix::Confirmed, + "pre-closed" => ContractPrefix::PreClosed, + "closed" => ContractPrefix::Closed, + "failed-accept" => ContractPrefix::FailedAccept, + "failed-sign" => ContractPrefix::FailedSign, + "refunded" => ContractPrefix::Refunded, + "rejected" => ContractPrefix::Rejected, + _ => ContractPrefix::Offered, + } + } +} + convertible_enum!( enum ChannelPrefix { Offered = 100, diff --git a/justfile b/justfile index 15d3044b..4b9c3904 100644 --- a/justfile +++ b/justfile @@ -5,10 +5,10 @@ bc *args: - docker exec bitcoin bitcoin-cli --rpcport=18443 --rpcuser=ddk --rpcpassword=ddk -rpcwallet=ddk {{args}} node-one: - - cargo run --bin ddk-node -- --network regtest --esplora http://127.0.0.1:30000 + - cargo run --bin ddk-node -- --network regtest --esplora http://127.0.0.1:30000 --name node-one --postgres-url postgres://dlcdevkit:dlcdevkit@localhost:5433/ddk_one node-two: - - cargo run --bin ddk-node -- --network regtest --esplora http://127.0.0.1:30000 --port 1777 --grpc 0.0.0.0:3031 --storage-dir ~/.ddk/node-two + - cargo run --bin ddk-node -- --network regtest --esplora http://127.0.0.1:30000 --port 1777 --grpc 0.0.0.0:3031 --storage-dir ~/.ddk/node-two --name node-two --postgres-url postgres://dlcdevkit:dlcdevkit@localhost:5433/ddk_two cli-one *args: - cargo run --bin ddk-cli {{args}} @@ -17,7 +17,7 @@ cli-two *args: - cargo run --bin ddk-cli -- --server http://127.0.0.1:3031 {{args}} up: - - DATABASE_URL=postgres://loco:loco@localhost:5432/sons-of-liberty_development sqlx migrate run --source ddk/src/storage/postgres/migrations + - DATABASE_URL=postgres://dlcdevkit:dlcdevkit@localhost:5433/ddk_one sqlx migrate run --source ddk/src/storage/postgres/migrations down: - - DATABASE_URL=postgres://loco:loco@localhost:5432/sons-of-liberty_development sqlx migrate revert --source ddk/src/storage/postgres/migrations + - DATABASE_URL=postgres://dlcdevkit:dlcdevkit@localhost:5433/ddk_one sqlx migrate revert --source ddk/src/storage/postgres/migrations From 33b99856cff9f9bf4140b79589f93c164c333339 Mon Sep 17 00:00:00 2001 From: bennyhodl Date: Fri, 18 Apr 2025 20:21:15 -0400 Subject: [PATCH 5/5] async wallet trait --- Cargo.lock | 19 ++++---- ddk-manager/src/lib.rs | 5 +- ddk-manager/src/manager.rs | 4 +- ddk-manager/src/utils.rs | 4 +- ddk/src/wallet.rs | 24 ++++----- payouts/Cargo.toml | 1 + payouts/src/lib.rs | 1 + payouts/src/parlay.rs | 99 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 130 insertions(+), 27 deletions(-) create mode 100644 payouts/src/parlay.rs diff --git a/Cargo.lock b/Cargo.lock index 5972f336..35326007 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1076,6 +1076,7 @@ dependencies = [ "dlc-trie", "serde", "serde_json", + "thiserror 2.0.12", ] [[package]] @@ -3555,7 +3556,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", @@ -3641,7 +3642,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tracing", "whoami", @@ -3682,7 +3683,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tracing", "whoami", @@ -3865,11 +3866,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -3885,9 +3886,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -4260,7 +4261,7 @@ dependencies = [ "rustls 0.23.16", "rustls-pki-types", "sha1", - "thiserror 2.0.11", + "thiserror 2.0.12", "utf-8", ] diff --git a/ddk-manager/src/lib.rs b/ddk-manager/src/lib.rs index 2390eaa4..dcbe48ba 100644 --- a/ddk-manager/src/lib.rs +++ b/ddk-manager/src/lib.rs @@ -136,13 +136,14 @@ pub trait ContractSignerProvider { fn get_new_secret_key(&self) -> Result; } +#[async_trait::async_trait] /// Wallet trait to provide functionalities related to generating, storing and /// managing bitcoin addresses and UTXOs. pub trait Wallet { /// Returns a new (unused) address. - fn get_new_address(&self) -> Result; + async fn get_new_address(&self) -> Result; /// Returns a new (unused) change address. - fn get_new_change_address(&self) -> Result; + async fn get_new_change_address(&self) -> Result; /// Get a set of UTXOs to fund the given amount. fn get_utxos_for_amount( &self, diff --git a/ddk-manager/src/manager.rs b/ddk-manager/src/manager.rs index fe432a69..31858196 100644 --- a/ddk-manager/src/manager.rs +++ b/ddk-manager/src/manager.rs @@ -2414,7 +2414,7 @@ where &counter_sk, &counter_revocation_sk, &tx, - &self.wallet.get_new_address()?, + &self.wallet.get_new_address().await?, 0, fee_rate_per_vb, )? @@ -2428,7 +2428,7 @@ where &counter_sk, &counter_revocation_sk, &tx, - &self.wallet.get_new_address()?, + &self.wallet.get_new_address().await?, CET_NSEQUENCE, 0, fee_rate_per_vb, diff --git a/ddk-manager/src/utils.rs b/ddk-manager/src/utils.rs index 7f1a3fad..da6bb52e 100644 --- a/ddk-manager/src/utils.rs +++ b/ddk-manager/src/utils.rs @@ -103,10 +103,10 @@ where { let funding_pubkey = signer.get_public_key(secp)?; - let payout_addr = wallet.get_new_address()?; + let payout_addr = wallet.get_new_address().await?; let payout_spk = payout_addr.script_pubkey(); let payout_serial_id = get_new_serial_id(); - let change_addr = wallet.get_new_change_address()?; + let change_addr = wallet.get_new_change_address().await?; let change_spk = change_addr.script_pubkey(); let change_serial_id = get_new_serial_id(); diff --git a/ddk/src/wallet.rs b/ddk/src/wallet.rs index cfdd6b3a..56ec0680 100644 --- a/ddk/src/wallet.rs +++ b/ddk/src/wallet.rs @@ -394,13 +394,13 @@ impl ddk_manager::ContractSignerProvider for DlcDevKitWallet { } } +#[async_trait::async_trait] impl ddk_manager::Wallet for DlcDevKitWallet { - fn get_new_address(&self) -> Result { - let address = tokio::task::block_in_place(|| { - // This runs in a blocking context but yields to the runtime - tokio::runtime::Handle::current().block_on(self.new_external_address()) - }) - .map_err(wallet_err_to_manager_err)?; + async fn get_new_address(&self) -> Result { + let address = self + .new_external_address() + .await + .map_err(wallet_err_to_manager_err)?; tracing::info!( address = address.address.to_string(), "Revealed new address for contract." @@ -408,12 +408,12 @@ impl ddk_manager::Wallet for DlcDevKitWallet { Ok(address.address) } - fn get_new_change_address(&self) -> Result { - let address = tokio::task::block_in_place(|| { - // This runs in a blocking context but yields to the runtime - tokio::runtime::Handle::current().block_on(self.new_change_address()) - }) - .map_err(wallet_err_to_manager_err)?; + async fn get_new_change_address(&self) -> Result { + let address = self + .new_change_address() + .await + .map_err(wallet_err_to_manager_err)?; + tracing::info!( address = address.address.to_string(), "Revealed new change address for contract." diff --git a/payouts/Cargo.toml b/payouts/Cargo.toml index 027ce618..b43c696d 100644 --- a/payouts/Cargo.toml +++ b/payouts/Cargo.toml @@ -21,3 +21,4 @@ bitcoin = "0.32.2" serde = { version = "1.0.209", features = ["derive"] } serde_json = "1.0.127" anyhow = "1.0.86" +thiserror = "2.0.12" diff --git a/payouts/src/lib.rs b/payouts/src/lib.rs index 6f0016a4..12d0d5e6 100644 --- a/payouts/src/lib.rs +++ b/payouts/src/lib.rs @@ -1,6 +1,7 @@ pub mod enumeration; pub mod options; pub(crate) mod options_builder; +pub mod parlay; use std::str::FromStr; diff --git a/payouts/src/parlay.rs b/payouts/src/parlay.rs new file mode 100644 index 00000000..4d5da8c0 --- /dev/null +++ b/payouts/src/parlay.rs @@ -0,0 +1,99 @@ +use ddk_manager::{ + contract::{ + contract_input::{ContractInput, ContractInputInfo, OracleInput}, + numerical_descriptor::NumericalDescriptor, + ContractDescriptor, + }, + payout_curve::{ + PayoutFunction, PayoutFunctionPiece, PayoutPoint, PolynomialPayoutCurvePiece, + RoundingInterval, RoundingIntervals, + }, +}; +use dlc_trie::OracleNumericInfo; + +/// Create a complete DLC Contract using oracle-normalized scores +pub fn create_parlay_contract( + max_normalized_value: u64, + offer_collateral: u64, + accept_collateral: u64, + oracle_input: OracleInput, + fee_rate: u64, +) -> ContractInput { + // Create payout function + let payout_function = create_normalized_payout_function( + max_normalized_value, + accept_collateral + offer_collateral, + ); + + // Determine appropriate rounding intervals + // For efficiency, we might round to nearest 10 for larger values + let rounding_intervals = RoundingIntervals { + intervals: vec![ + RoundingInterval { + begin_interval: 0, + rounding_mod: 1, + }, + RoundingInterval { + begin_interval: 101, + rounding_mod: 10, + }, + ], + }; + // Calculate number of digits needed for the oracle + let digits_needed = (max_normalized_value as f64).log2().ceil() as u16; + + // Create numerical descriptor + let numerical_descriptor = NumericalDescriptor { + payout_function, + rounding_intervals, + difference_params: None, + oracle_numeric_infos: OracleNumericInfo { + base: 2, + nb_digits: vec![digits_needed as usize], + }, + }; + + // Create contract descriptor + let contract_descriptor = ContractDescriptor::Numerical(numerical_descriptor); + + // Create contract info + let contract_info = ContractInputInfo { + contract_descriptor, + oracles: oracle_input, + }; + + // Create final contract input + ContractInput { + offer_collateral, + accept_collateral, + fee_rate, + contract_infos: vec![contract_info], + } +} + +/// Create a PayoutFunction for an oracle-normalized score +fn create_normalized_payout_function( + max_normalized_value: u64, // Typically 1000 or 10000 for 3 or 4 decimal precision + max_payout: u64, // Maximum contract payout +) -> PayoutFunction { + // Create a simple linear polynomial with just two points + let payout_points = vec![ + PayoutPoint { + event_outcome: 0, + outcome_payout: 0, + extra_precision: 0, + }, + PayoutPoint { + event_outcome: max_normalized_value, + outcome_payout: max_payout, + extra_precision: 0, + }, + ]; + + // Create a single polynomial piece + let polynomial_piece = PolynomialPayoutCurvePiece::new(payout_points).unwrap(); + let payout_function_piece = PayoutFunctionPiece::PolynomialPayoutCurvePiece(polynomial_piece); + + // Create the payout function with this single piece + PayoutFunction::new(vec![payout_function_piece]).unwrap() +}