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 b7a835c
Show file tree
Hide file tree
Showing 12 changed files with 686 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
29 changes: 29 additions & 0 deletions include/cpp/joinstr.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include "joinstr.h"

auto errorToString(JoinstrError e) -> std::string {
switch (e) {
case JoinstrError::None:
return "None";
case JoinstrError::Tokio:
return "Tokio";
case JoinstrError::CastString:
return "CastString";
case JoinstrError::JsonError:
return "Json";
case JoinstrError::CString:
return "CString";
case JoinstrError::ListPools:
return "ListPools";
case ListCoins:
return "ListCoins";
case InitiateConjoin:
return "InitiateCoinjoin";
case SerdeJson:
return "SerdeJson";
case PoolConfig:
return "PoolConfig";
case PeerConfig:
return "PeerConfig";
}
return "Unknown";
}
90 changes: 90 additions & 0 deletions include/cpp/joinstr.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#pragma once

#include <cstdint>
#include <stdint.h>
#include <string>
using f64_t = double;

extern "C" {

enum Network { // NOLINT(performance-enum-size)
Bitcoin = 0,
Testnet,
Signet,
Regtest,
};

enum JoinstrError { // NOLINT(performance-enum-size)
None = 0,
Tokio,
CastString,
JsonError,
CString,
ListPools,
ListCoins,
InitiateConjoin,
SerdeJson,
PoolConfig,
PeerConfig,
};

struct PoolConfig {
f64_t denomination;
uint32_t fee;
uint64_t max_duration;
uint8_t peers;
Network network;
};

struct PeerConfig {
const char* electrum_address;
uint16_t electrum_port;
const char* mnemonics;
const char* input;
const char* output;
const char* relay;
};

struct Pools {
const char* pools;
JoinstrError error;
};

struct Coins {
const char* coins;
JoinstrError error;
};

struct Txid {
const char* txid;
JoinstrError error;
};

auto list_pools( // NOLINT(readability-identifier-naming)
uint64_t back,
uint64_t timeout,
const char* relay
) -> Pools;

auto list_coins( // NOLINT(readability-identifier-naming)
const char* mnemonics,
const char* addr,
uint16_t port,
Network network,
uint32_t index_min,
uint32_t index_max
) -> Coins;

auto initiate_coinjoin( // NOLINT(readability-identifier-naming)
struct PoolConfig config,
struct PeerConfig peer
) -> Txid;

auto join_coinjoin( // NOLINT(readability-identifier-naming)
const char* pool,
struct PeerConfig peer
) -> Txid;

}

auto errorToString(JoinstrError e) -> std::string;
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
Expand Up @@ -39,6 +39,8 @@ pub enum Error {
RelaysMissing,
FeeMissing,
TimelineDuration,
AlreadyHaveInput,
AlreadyHaveOutput,
}

impl From<crate::coinjoin::Error> for Error {
Expand Down
Loading

0 comments on commit b7a835c

Please sign in to comment.