Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
bindings:implement a generic interface
Browse files Browse the repository at this point in the history
pythcoiner committed Jan 19, 2025
1 parent 761ece3 commit d8be6a6
Showing 11 changed files with 509 additions and 10 deletions.
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
[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"
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"}
28 changes: 28 additions & 0 deletions joinstr.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

#ifdef __cplusplus
extern "C" {
#endif

#include <stdint.h>

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

228 changes: 228 additions & 0 deletions src/interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
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, CoinPath, 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<crate::nostr::client::Error> for Error {
fn from(value: crate::nostr::client::Error) -> Self {
Self::NostrClient(value)
}
}

impl From<crate::joinstr::Error> for Error {
fn from(value: crate::joinstr::Error) -> Self {
Self::Joinstr(value)
}
}

impl From<crate::signer::Error> for Error {
fn from(value: crate::signer::Error) -> Self {
Self::Signer(value)
}
}

impl From<crate::electrum::Error> for Error {
fn from(value: crate::electrum::Error) -> Self {
Self::Electrum(value)
}
}

impl From<serde_json::Error> 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
// FIXME: this function is a ugly+ineficient hack, we should use
// the electrum notification mechanism and let consumer poll our
// static/cached state
pub async fn list_coins(
mnemonics: String,
electrum_address: String,
electrum_port: u16,
range: (u32, u32),
network: Network,
) -> Result<Vec<Coin>, Error> {
let mut signer = WpkhHotSigner::new_from_mnemonics(network, &mnemonics)?;
let client = Client::new(&electrum_address, electrum_port)?;
signer.set_client(client);

for i in range.0..range.1 {
let recv = CoinPath::new(0, i);
let change = CoinPath::new(1, i);
let _ = signer.get_coins_at(recv);
let _ = signer.get_coins_at(change);
}

let coins = signer.list_coins().into_iter().map(|c| c.1).collect();

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<String /* Txid */, Error> {
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<NetworkUnchecked> = 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<Vec<String /* Pool */>, 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<String /* Txid */, Error> {
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<NetworkUnchecked> = 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)
}
2 changes: 2 additions & 0 deletions src/joinstr/error.rs
Original file line number Diff line number Diff line change
@@ -39,6 +39,8 @@ pub enum Error {
RelaysMissing,
FeeMissing,
TimelineDuration,
AlreadyHaveInput,
AlreadyHaveOutput,
}

impl From<crate::coinjoin::Error> for Error {
36 changes: 35 additions & 1 deletion src/joinstr/mod.rs
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ pub struct Joinstr<'a> {
final_tx: Option<miniscript::bitcoin::Transaction>,
}

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<NetworkUnchecked>) -> 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
192 changes: 192 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,199 @@
#![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<Runtime> = Mutex::new(Runtime::new().unwrap());
}

fn serialize_to_cstring<T>(value: T) -> Result<CString, Error>
where
T: Serialize,
{
match serde_json::to_string(&value) {
Ok(v) => match CString::new(v) {
Ok(s) => Ok(s),
Err(_) => Err(Error::Json),
},
Err(_) => Err(Error::CString),
}
}

macro_rules! to_string {
($value:expr, $t:ty) => {{
let cstr = unsafe { CStr::from_ptr($value) };
match cstr.to_str() {
Ok(str_slice) => str_slice.to_owned(),
Err(_) => return <$t>::error(Error::CString),
}
}};
}

macro_rules! runtime {
($future:expr, $t:ty) => {{
if let Ok(runtime) = RT.lock() {
match runtime.block_on($future) {
Ok(p) => <$t>::ok(p),
Err(e) => <$t>::error(e),
}
} else {
<$t>::error(Error::Tokio)
}
}};
}

#[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,
}

impl Network {
pub fn to_rust_bitcoin(self) -> bitcoin::Network {
match self {
Network::Bitcoin => bitcoin::Network::Bitcoin,
Network::Testnet => bitcoin::Network::Testnet,
Network::Signet => bitcoin::Network::Signet,
Network::Regtest => bitcoin::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,
ListCoins,
SerdeJson,
}

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 = to_string!(relay, Pools);
let future = async {
match interface::list_pools(back, timeout, relay).await {
Ok(p) => serialize_to_cstring(p),
Err(_) => Err(Error::ListPools),
}
};

runtime!(future, Pools)
}

impl Coins {
pub fn ok(pools: CString) -> Self {
Coins {
pools: pools.into_raw(),
error: Error::None,
}
}
pub fn error(e: Error) -> Self {
Coins {
pools: null(),
error: e,
}
}
}

#[repr(C)]
pub struct Coins {
pools: *const c_char,
error: Error,
}

#[no_mangle]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn list_coins(
mnemonics: *const c_char,
addr: *const c_char,
port: u16,
network: Network,
index_min: u32,
index_max: u32,
) -> Coins {
let mnemonics = to_string!(mnemonics, Coins);
let addr = to_string!(addr, Coins);

let future = async {
match interface::list_coins(
mnemonics,
addr,
port,
(index_min, index_max),
network.to_rust_bitcoin(),
)
.await
{
Ok(c) => serialize_to_cstring(c),
Err(_) => Err(Error::ListCoins),
}
};

runtime!(future, Coins)
}
1 change: 1 addition & 0 deletions src/nostr/mod.rs
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ pub struct InputDataSigned {
pub amount: Option<Amount>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Error {
NoInput,
TooMuchInputs,
14 changes: 12 additions & 2 deletions src/signer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod error;
pub use error::Error;
use serde::{Deserialize, Serialize};

use crate::{electrum::Client, nostr::InputDataSigned};
use bip39::Mnemonic;
@@ -43,20 +44,29 @@ impl Debug for WpkhHotSigner {
}
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Coin {
pub txout: TxOut,
pub outpoint: OutPoint,
pub sequence: Sequence,
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<u32>,
}

impl CoinPath {
pub fn new(depth: u32, index: u32) -> Self {
CoinPath {
depth,
index: Some(index),
}
}
}

pub fn descriptor(
xpub: &Xpub,
fg: &Fingerprint,
2 changes: 1 addition & 1 deletion tests/coinjoin.rs
Original file line number Diff line number Diff line change
@@ -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() {
6 changes: 3 additions & 3 deletions tests/joinstr.rs
Original file line number Diff line number Diff line change
@@ -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();
2 changes: 1 addition & 1 deletion tests/utils.rs
Original file line number Diff line number Diff line change
@@ -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 */

0 comments on commit d8be6a6

Please sign in to comment.