Skip to content

Commit

Permalink
bindings:implement a generic interface
Browse files Browse the repository at this point in the history
  • Loading branch information
pythcoiner committed Jan 19, 2025
1 parent 761ece3 commit eba7f95
Show file tree
Hide file tree
Showing 11 changed files with 422 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"}
Expand Down
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

214 changes: 214 additions & 0 deletions src/interface.rs
Original file line number Diff line number Diff line change
@@ -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<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
pub async fn list_coins(
_mnemonics: String,
_electrum_address: String,
_electrum_port: u16,
) -> Result<Vec<Coin>, 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<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
Expand Up @@ -39,6 +39,8 @@ pub enum Error {
RelaysMissing,
FeeMissing,
TimelineDuration,
AlreadyHaveInput,
AlreadyHaveOutput,
}

impl From<crate::coinjoin::Error> for Error {
Expand Down
36 changes: 35 additions & 1 deletion src/joinstr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit eba7f95

Please sign in to comment.