From eba7f9500045d1930ad12a34c27315cb14582372 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Fri, 3 Jan 2025 07:32:30 +0100 Subject: [PATCH] bindings:implement a generic interface --- Cargo.toml | 8 +- joinstr.h | 28 ++++++ src/interface.rs | 214 +++++++++++++++++++++++++++++++++++++++++++ src/joinstr/error.rs | 2 + src/joinstr/mod.rs | 36 +++++++- src/lib.rs | 128 ++++++++++++++++++++++++++ src/nostr/mod.rs | 1 + src/signer/mod.rs | 5 +- tests/coinjoin.rs | 2 +- tests/joinstr.rs | 6 +- tests/utils.rs | 2 +- 11 files changed, 422 insertions(+), 10 deletions(-) create mode 100644 joinstr.h create mode 100644 src/interface.rs diff --git a/Cargo.toml b/Cargo.toml index c59b983..3fb4697 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,18 @@ [package] -name = "rust-joinstr" +name = "joinstr" version = "0.0.1" edition = "2021" +[lib] +crate-type = ["rlib", "cdylib", "staticlib"] + [dependencies] home = "=0.5.9" bitcoin = "=0.32.2" bip39 = { version = "2.0.0", features = ["rand"] } hex-conservative = "0.2.1" miniscript = {version = "12.2.0", features = ["base64", "serde"]} -simple_electrum_client = { git = "https://github.com/pythcoiner/simple_electrum_client.git", branch = "master"} +simple_electrum_client = { git = "https://github.com/pythcoiner/simple_electrum_client.git", branch = "openssl_vendored"} nostr-sdk = "0.35.0" serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" @@ -17,6 +20,7 @@ tokio = "1.40.0" log = "0.4.22" env_logger = "=0.10.2" rand = "0.8.5" +lazy_static = "1.5.0" [dev-dependencies] electrsd = { git = "https://github.com/pythcoiner/electrsd.git", branch = "buffered_logs"} diff --git a/joinstr.h b/joinstr.h new file mode 100644 index 0000000..4d661e4 --- /dev/null +++ b/joinstr.h @@ -0,0 +1,28 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef enum { + None = 0, + Tokio, + CastString, + Json, + CString, + ListPools +} Error; + +typedef struct Pools { + const char* pools; + Error error; +} Pools; + +Pools list_pools(uint64_t back, uint64_t timeout, const char* relay); + +#ifdef __cplusplus +} +#endif + diff --git a/src/interface.rs b/src/interface.rs new file mode 100644 index 0000000..1658206 --- /dev/null +++ b/src/interface.rs @@ -0,0 +1,214 @@ +use std::{fmt::Display, time::Duration}; + +use bitcoin::{address::NetworkUnchecked, Address, Network}; +use nostr_sdk::Keys; +use tokio::time::sleep; + +use crate::{ + electrum::Client, + joinstr::Joinstr, + nostr::{client::NostrClient, Pool}, + signer::{Coin, WpkhHotSigner}, + utils::now, +}; + +pub enum Error { + Unknown, + NostrClient(crate::nostr::client::Error), + SerdeJson(serde_json::Error), + Joinstr(crate::joinstr::Error), + Signer(crate::signer::Error), + Electrum(crate::electrum::Error), +} + +impl From for Error { + fn from(value: crate::nostr::client::Error) -> Self { + Self::NostrClient(value) + } +} + +impl From for Error { + fn from(value: crate::joinstr::Error) -> Self { + Self::Joinstr(value) + } +} + +impl From for Error { + fn from(value: crate::signer::Error) -> Self { + Self::Signer(value) + } +} + +impl From for Error { + fn from(value: crate::electrum::Error) -> Self { + Self::Electrum(value) + } +} + +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Self::SerdeJson(value) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Unknown => write!(f, "Unknown error!"), + Error::NostrClient(e) => write!(f, "NostrClient error: {:?}", e), + Error::SerdeJson(e) => write!(f, "serde_json error: {:?}", e), + Error::Joinstr(e) => write!(f, "Joinstr error: {:?}", e), + Error::Signer(e) => write!(f, "Signer error: {:?}", e), + Error::Electrum(e) => write!(f, "Electrum error: {:?}", e), + } + } +} + +pub struct PoolConfig { + pub denomination: f64, + pub fee: u32, + pub max_duration: u64, + pub peers: usize, + pub network: Network, +} + +pub struct PeerConfig { + pub mnemonics: String, + pub electrum_address: String, + pub electrum_port: u16, + pub input: String, + pub output: String, + pub relay: String, +} + +/// List available coins +pub async fn list_coins( + _mnemonics: String, + _electrum_address: String, + _electrum_port: u16, +) -> Result, Error> { + let coins = Vec::new(); + + // TODO: fetch coins + // + Ok(coins) +} + +/// Initiate and participate to a coinjoin +/// +/// # Arguments +/// * `config` - configuration of the pool to initiate +/// * `peer` - information about the peer +/// +pub async fn initiate_coinjoin( + config: PoolConfig, + peer: PeerConfig, +) -> Result { + let relays = vec![peer.relay.clone()]; + let (url, port) = (peer.electrum_address, peer.electrum_port); + let mut initiator = Joinstr::new_initiator( + Keys::generate(), + &relays, + (&url, port), + config.network, + "initiator", + ) + .await? + .denomination(config.denomination)? + .fee(config.fee)? + .simple_timeout(now() + config.max_duration)? + .min_peers(config.peers)?; + + let mut signer = WpkhHotSigner::new_from_mnemonics(config.network, &peer.mnemonics)?; + let client = Client::new(&url, port)?; + signer.set_client(client); + + let addr: Address = serde_json::from_str(&peer.output)?; + let coin: Coin = serde_json::from_str(&peer.input)?; + + initiator.set_coin(coin)?; + initiator.set_address(addr)?; + + initiator.start_coinjoin(None, Some(&signer)).await?; + + let txid = initiator + .final_tx() + .expect("coinjoin success") + .compute_txid() + .to_string(); + + Ok(txid) +} + +/// List available pools +/// +/// # Arguments +/// * `back` - how many second back look in the past +/// * `timeout` - how many microseconds we will wait before fetching relay notifications +/// * `relay` - the relay url, must start w/ `wss://` or `ws://` +/// +/// # Returns a [`Vec`] of [`String`] containing a json serialization of a [`Pool`] +pub async fn list_pools( + back: u64, + timeout: u64, + relay: String, +) -> Result, Error> { + let mut pools = Vec::new(); + let relays = vec![relay]; + let mut pool_listener = NostrClient::new("pool_listener") + .relays(&relays)? + .keys(Keys::generate())?; + pool_listener.connect_nostr().await.unwrap(); + // subscribe to 2020 event up to 1 day back in time + pool_listener.subscribe_pools(back).await.unwrap(); + + sleep(Duration::from_micros(timeout)).await; + + while let Some(pool) = pool_listener.receive_pool_notification()? { + let str = serde_json::to_string(&pool)?; + pools.push(str) + } + + Ok(pools) +} + +/// Try to join an already initiated coinjoin +/// +/// # Arguments +/// * `pool` - [`String`] containing a json serialization of a [`Pool`] +/// * `peer` - information about the peer +/// +pub async fn join_coinjoin( + pool: String, /* Pool */ + peer: PeerConfig, +) -> Result { + let pool: Pool = serde_json::from_str(&pool)?; + let relays = vec![peer.relay.clone()]; + let (url, port) = (peer.electrum_address, peer.electrum_port); + let addr: Address = serde_json::from_str(&peer.output)?; + let coin: Coin = serde_json::from_str(&peer.input)?; + let mut joinstr_peer = Joinstr::new_peer_with_electrum( + &relays, + &pool, + (&url, port), + coin, + addr, + pool.network, + "peer", + ) + .await?; + + let mut signer = WpkhHotSigner::new_from_mnemonics(pool.network, &peer.mnemonics)?; + let client = Client::new(&url, port)?; + signer.set_client(client); + + joinstr_peer.start_coinjoin(None, Some(&signer)).await?; + + let txid = joinstr_peer + .final_tx() + .expect("coinjoin success") + .compute_txid() + .to_string(); + + Ok(txid) +} diff --git a/src/joinstr/error.rs b/src/joinstr/error.rs index 5588647..fae693b 100644 --- a/src/joinstr/error.rs +++ b/src/joinstr/error.rs @@ -39,6 +39,8 @@ pub enum Error { RelaysMissing, FeeMissing, TimelineDuration, + AlreadyHaveInput, + AlreadyHaveOutput, } impl From for Error { diff --git a/src/joinstr/mod.rs b/src/joinstr/mod.rs index d79bae9..ee4d1f9 100644 --- a/src/joinstr/mod.rs +++ b/src/joinstr/mod.rs @@ -45,7 +45,7 @@ pub struct Joinstr<'a> { final_tx: Option, } -impl<'a> Default for Joinstr<'a> { +impl Default for Joinstr<'_> { fn default() -> Self { Self { initiator: false, @@ -948,6 +948,40 @@ impl<'a> Joinstr<'a> { Ok(()) } + /// Set the coin to coinjoin + /// + /// # Errors + /// + /// This function will return an error if the coin is already set + pub fn set_coin(&mut self, coin: Coin) -> Result<(), Error> { + if self.input.is_none() { + self.input = Some(coin); + Ok(()) + } else { + Err(Error::AlreadyHaveInput) + } + } + + /// Set the address the coin must be sent to + /// + /// # Errors + /// + /// This function will return an error if the address is already set + /// or if address is for wrong network + pub fn set_address(&mut self, addr: Address) -> Result<(), Error> { + let addr = if addr.is_valid_for_network(self.network) { + addr.assume_checked() + } else { + return Err(Error::WrongAddressNetwork); + }; + if self.output.is_none() { + self.output = Some(addr); + Ok(()) + } else { + Err(Error::AlreadyHaveOutput) + } + } + /// Strart a coinjoin process, followings steps will be processed: /// - if no `pool` arg is passed, a new pool will be initiated. /// - if a `pool` arg is passed, it will join the pool diff --git a/src/lib.rs b/src/lib.rs index 8467d13..a42b2fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,135 @@ #![allow(dead_code)] pub mod coinjoin; pub mod electrum; +pub mod interface; pub mod joinstr; pub mod nostr; pub mod signer; pub mod utils; + +use lazy_static::lazy_static; +use serde::Serialize; +use std::{ + ffi::{c_char, CStr, CString}, + ptr::null, + sync::Mutex, +}; +use tokio::runtime::Runtime; + +lazy_static! { + static ref RT: Mutex = Mutex::new(Runtime::new().unwrap()); +} + +fn cast_to_cstring(value: T) -> Result +where + T: Serialize, +{ + match serde_json::to_string(&value) { + Ok(v) => match CString::new(v) { + Ok(s) => Ok(s), + Err(_) => { + log::error!("fail to convert json string to C string!"); + Err(Error::Json) + } + }, + Err(_) => { + log::error!("fail to convert pool list to it json string representation!"); + Err(Error::CString) + } + } +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub enum Network { + /// Mainnet Bitcoin. + Bitcoin, + /// Bitcoin's testnet network. + Testnet, + /// Bitcoin's signet network. + Signet, + /// Bitcoin's regtest network. + Regtest, +} + +#[repr(C)] +pub struct PoolConfig { + pub denomination: f64, + pub fee: u32, + pub max_duration: u32, + pub peers: u8, + pub network: Network, +} + +#[repr(C)] +pub struct PeerConfig { + pub outpoint: *mut c_char, + pub electrum: *mut c_char, + pub mnemonics: *mut c_char, + pub address: *mut c_char, + pub relay: *mut c_char, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub enum Error { + None, + Tokio, + CastString, + Json, + CString, + ListPools, +} + + +impl Pools { + pub fn ok(pools: CString) -> Self { + Pools { + pools: pools.into_raw(), + error: Error::None, + } + } + pub fn error(e: Error) -> Self { + Pools { + pools: null(), + error: e, + } + } +} + +#[repr(C)] +pub struct Pools { + pools: *const c_char, + error: Error, +} + +#[no_mangle] +#[allow(clippy::not_unsafe_ptr_arg_deref)] +pub extern "C" fn list_pools(back: u64, timeout: u64, relay: *const c_char) -> Pools { + let relay = unsafe { CStr::from_ptr(relay) }; + let relay = if let Ok(relay) = relay.to_str() { + relay.to_owned() + } else { + log::error!("list_pool(): fail to cast `relay` arg!"); + return Pools::error(Error::CastString); + }; + let future = async { + match interface::list_pools(back, timeout, relay).await { + Ok(p) => cast_to_cstring(p), + Err(e) => { + log::error!("list_pools() fail: {}", e); + Err(Error::ListPools) + } + } + }; + + if let Ok(runtime) = RT.lock() { + match runtime.block_on(future) { + Ok(p) => Pools::ok(p), + Err(e) => Pools::error(e), + } + } else { + log::error!("list_pools(): fail to get a lock on tokio runtime!"); + Pools::error(Error::Tokio) + } +} diff --git a/src/nostr/mod.rs b/src/nostr/mod.rs index 13e7a61..685051c 100644 --- a/src/nostr/mod.rs +++ b/src/nostr/mod.rs @@ -19,6 +19,7 @@ pub struct InputDataSigned { pub amount: Option, } +#[derive(Debug, Clone, PartialEq)] pub enum Error { NoInput, TooMuchInputs, diff --git a/src/signer/mod.rs b/src/signer/mod.rs index 920a819..2b3f69e 100644 --- a/src/signer/mod.rs +++ b/src/signer/mod.rs @@ -1,5 +1,6 @@ mod error; pub use error::Error; +use serde::{Deserialize, Serialize}; use crate::{electrum::Client, nostr::InputDataSigned}; use bip39::Mnemonic; @@ -43,7 +44,7 @@ impl Debug for WpkhHotSigner { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Coin { pub txout: TxOut, pub outpoint: OutPoint, @@ -51,7 +52,7 @@ pub struct Coin { pub coin_path: CoinPath, } -#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy)] +#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize())] pub struct CoinPath { pub depth: u32, pub index: Option, diff --git a/tests/coinjoin.rs b/tests/coinjoin.rs index d74ad29..f6c379d 100644 --- a/tests/coinjoin.rs +++ b/tests/coinjoin.rs @@ -2,8 +2,8 @@ pub mod utils; use crate::utils::{funded_wallet, generate}; use electrsd::bitcoind::bitcoincore_rpc::RpcApi; +use joinstr::{coinjoin::CoinJoin, electrum::Client, signer::CoinPath}; use miniscript::bitcoin::Amount; -use rust_joinstr::{coinjoin::CoinJoin, electrum::Client, signer::CoinPath}; #[test] fn simple_tx() { diff --git a/tests/joinstr.rs b/tests/joinstr.rs index fba61fa..39fcb2f 100644 --- a/tests/joinstr.rs +++ b/tests/joinstr.rs @@ -3,16 +3,16 @@ use std::{sync::Once, time::Duration}; use crate::utils::{bootstrap_electrs, funded_wallet_with_bitcoind}; use electrsd::bitcoind::bitcoincore_rpc::RpcApi; -use miniscript::bitcoin::Network; -use rust_joinstr::{ +use joinstr::{ electrum::Client, signer::{CoinPath, WpkhHotSigner}, utils::now, }; +use miniscript::bitcoin::Network; +use joinstr::{joinstr::Joinstr, nostr::client::NostrClient}; use nostr_sdk::{Event, Keys, Kind}; use nostrd::NostrD; -use rust_joinstr::{joinstr::Joinstr, nostr::client::NostrClient}; use tokio::time::sleep; static INIT: Once = Once::new(); diff --git a/tests/utils.rs b/tests/utils.rs index 018ae9c..2361f82 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -7,8 +7,8 @@ use electrsd::{ }, ElectrsD, }; +use joinstr::{electrum::Client, signer::WpkhHotSigner}; use miniscript::bitcoin::{Address, Amount, Network}; -use rust_joinstr::{electrum::Client, signer::WpkhHotSigner}; pub fn bootstrap_electrs() -> ( String, /* url */