Skip to content

Commit 6bf12dd

Browse files
authored
Merge branch 'master' into taproot-test
2 parents 8c21e19 + 97492f8 commit 6bf12dd

36 files changed

+1609
-840
lines changed

.github/workflows/lint.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ jobs:
1616

1717
- name: Set profile
1818
run: rustup set profile minimal
19+
20+
- name: Install protobuf compiler
21+
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
1922

2023
- name: Install nightly toolchain
2124
run: rustup toolchain install nightly

.github/workflows/test.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ jobs:
1414
- name: Checkout
1515
uses: actions/checkout@v4
1616

17+
- name: Install protobuf compiler
18+
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
19+
1720
- name: Install Rust toolchain
1821
uses: dtolnay/rust-toolchain@stable
1922
with:

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@ sha2 = "0.10.9"
3535
rust-coinselect = "0.1.6"
3636
crossterm = "0.29.0"
3737
zmq = "0.10.0"
38+
tungstenite = { version = "0.28.0", features = ["native-tls"]}
39+
nostr = "0.44.2"
3840

3941
[dev-dependencies]
4042
flate2 = {version = "1.0.35"}
4143
tar = {version = "0.4.43"}
44+
nostr-rs-relay = "0.8.12"
4245

4346
#Empty default feature set, (helpful to generalise in github actions)
4447
[features]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ The crate compiles into following binaries.
7777
Following dependencies are needed to compile the crate.
7878

7979
```shell
80-
sudo apt install build-essential automake libtool
80+
sudo apt install build-essential automake libtool protobuf-compiler
8181
```
8282
To run all the coinswap apps the two more systems are needed.
8383

docker/Dockerfile

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ FROM rust:1.90-alpine3.20 AS builder
66

77
RUN apk add --no-cache \
88
build-base \
9+
protobuf \
910
cmake \
1011
git \
1112
curl \
1213
pkgconfig \
1314
openssl-dev \
1415
sqlite-dev \
1516
sqlite-static \
16-
zeromq-dev
17+
zeromq-dev \
18+
ca-certificates \
19+
openssl-libs-static \
20+
libgcc \
21+
musl-dev
1722

1823
WORKDIR /usr/src/coinswap
1924
COPY . .
@@ -26,11 +31,20 @@ FROM alpine:3.20
2631

2732
RUN --mount=type=cache,target=/var/cache/apk \
2833
apk add --no-cache \
34+
build-base \
35+
protobuf \
36+
cmake \
37+
git \
38+
curl \
39+
pkgconfig \
40+
openssl-dev \
41+
sqlite-dev \
42+
sqlite-static \
43+
zeromq-dev \
2944
ca-certificates \
30-
openssl \
45+
openssl-libs-static \
3146
libgcc \
32-
sqlite \
33-
zeromq
47+
musl-dev
3448

3549
RUN adduser -D -u 1001 coinswap
3650

src/bin/taker.rs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use bitcoin::{Address, Amount};
22
use bitcoind::bitcoincore_rpc::Auth;
33
use clap::Parser;
44
use coinswap::{
5-
taker::{error::TakerError, SwapParams, Taker, TaprootTaker},
5+
taker::{error::TakerError, offers::MakerState, SwapParams, Taker, TaprootTaker},
66
utill::{parse_proxy_auth, setup_taker_logger, MIN_FEE_RATE, UTXO},
77
wallet::{Destination, RPCConfig, Wallet},
88
};
@@ -180,7 +180,7 @@ fn main() -> Result<(), TakerError> {
180180
let rpc_config = RPCConfig {
181181
url: args.rpc,
182182
auth: Auth::UserPass(args.auth.0, args.auth.1),
183-
wallet_name: "random".to_string(), // we can put anything here as it will get updated in the init.
183+
wallet_name: "random_1".to_string(), // we can put anything here as it will get updated in the init.
184184
};
185185

186186
match &args.command {
@@ -339,24 +339,49 @@ fn main() -> Result<(), TakerError> {
339339
taker.get_wallet_mut().sync_and_save()?;
340340
}
341341
Commands::FetchOffers => {
342-
let all_offers = {
343-
let offerbook = taker.fetch_offers()?;
344-
offerbook
345-
.all_makers()
346-
.iter()
347-
.cloned()
348-
.cloned()
349-
.collect::<Vec<_>>()
350-
};
351-
if all_offers.is_empty() {
352-
println!("NO LIVE OFFERS FOUND!! You should run a maker!!");
342+
use std::time::{Duration, Instant};
343+
344+
println!("Waiting for offerbook synchronization to complete…");
345+
let sync_start = Instant::now();
346+
347+
while taker.is_offerbook_syncing() {
348+
println!("Offerbook sync in progress...");
349+
std::thread::sleep(Duration::from_secs(2));
350+
}
351+
352+
println!("Offerbook synchronized in {:.2?}", sync_start.elapsed());
353+
354+
let offerbook = taker.fetch_offers()?;
355+
let makers = offerbook.all_makers();
356+
357+
if makers.is_empty() {
358+
println!("No makers found in offerbook");
353359
return Ok(());
354-
} else {
355-
all_offers.iter().try_for_each(|offer| {
356-
println!("{}", taker.display_offer(offer)?);
357-
Ok::<_, TakerError>(())
358-
})?;
359360
}
361+
362+
let mut good = 0;
363+
let mut bad = 0;
364+
let mut unresponsive = 0;
365+
366+
println!("\nDiscovered {} makers\n", makers.len());
367+
368+
for maker in &makers {
369+
match maker.state {
370+
MakerState::Good => good += 1,
371+
MakerState::Bad => bad += 1,
372+
MakerState::Unresponsive { .. } => unresponsive += 1,
373+
}
374+
375+
println!("{}", taker.display_offer(maker)?);
376+
}
377+
378+
println!(
379+
"\nOfferbook summary → good: {}, bad: {}, unresponsive: {} (total: {})",
380+
good,
381+
bad,
382+
unresponsive,
383+
makers.len()
384+
);
360385
}
361386
Commands::Coinswap { makers, amount } => {
362387
// Note: taproot coinswap is handled at the top level to avoid

src/maker/api.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ impl Maker {
297297
config.network_port = port;
298298
}
299299

300+
// ## TODO: Encapsulate these initialization inside the watcher and
301+
// pollute the client declaration.
300302
let backend = ZmqBackend::new(&zmq_addr);
301303
let rpc_backend = BitcoinRpc::new(rpc_config.clone())?;
302304
let blockchain_info = rpc_backend.get_blockchain_info()?;
@@ -306,11 +308,12 @@ impl Maker {
306308
let registry = FileRegistry::load(file_registry);
307309
let (tx_requests, rx_requests) = mpsc::channel();
308310
let (tx_events, rx_responses) = mpsc::channel();
311+
let rpc_config_watcher = rpc_config.clone();
309312

310313
let mut watcher = Watcher::<Maker>::new(backend, registry, rx_requests, tx_events);
311314
_ = thread::Builder::new()
312315
.name("Watcher thread".to_string())
313-
.spawn(move || watcher.run(rpc_backend));
316+
.spawn(move || watcher.run(rpc_config_watcher));
314317

315318
let watch_service = WatchService::new(tx_requests, rx_responses);
316319

src/maker/api2.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@ impl Maker {
280280
config.network_port = port;
281281
}
282282

283+
// ## TODO: Encapsulate these initialization inside the watcher and
284+
// pollute the client declaration.
283285
let backend = ZmqBackend::new(&zmq_addr);
284286
let rpc_backend = BitcoinRpc::new(rpc_config.clone())?;
285287
let blockchain_info = rpc_backend.get_blockchain_info()?;
@@ -289,11 +291,12 @@ impl Maker {
289291
let registry = FileRegistry::load(file_registry);
290292
let (tx_requests, rx_requests) = mpsc::channel();
291293
let (tx_events, rx_responses) = mpsc::channel();
294+
let rpc_config_watcher = rpc_config.clone();
292295

293296
let mut watcher = Watcher::<Maker>::new(backend, registry, rx_requests, tx_events);
294297
_ = thread::Builder::new()
295298
.name("Watcher thread".to_string())
296-
.spawn(move || watcher.run(rpc_backend));
299+
.spawn(move || watcher.run(rpc_config_watcher));
297300

298301
let watch_service = WatchService::new(tx_requests, rx_responses);
299302

src/maker/server.rs

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,19 @@
44
//! The server maintains the thread pool for P2P Connection, Watchtower, Bitcoin Backend, and RPC Client Request.
55
//! The server listens at two ports: 6102 for P2P, and 6103 for RPC Client requests.
66
7-
use crate::protocol::messages::FidelityProof;
7+
use crate::{
8+
protocol::messages::FidelityProof,
9+
utill::{COINSWAP_KIND, NOSTR_RELAYS},
10+
};
811
use bitcoin::{absolute::LockTime, Amount};
912
use bitcoind::bitcoincore_rpc::RpcApi;
13+
use nostr::{
14+
event::{EventBuilder, Kind},
15+
key::{Keys, SecretKey},
16+
message::{ClientMessage, RelayMessage},
17+
util::JsonUtil,
18+
};
19+
use tungstenite::Message;
1020

1121
use std::{
1222
io::ErrorKind,
@@ -75,10 +85,102 @@ fn network_bootstrap(maker: Arc<Maker>) -> Result<String, MakerError> {
7585
/// 2. Creates a new fidelity bond if no valid bonds remain after redemption.
7686
fn manage_fidelity_bonds(maker: &Maker, maker_addr: &str) -> Result<(), MakerError> {
7787
maker.wallet.write()?.redeem_expired_fidelity_bonds()?;
78-
setup_fidelity_bond(maker, maker_addr)?;
88+
let fidelity = setup_fidelity_bond(maker, maker_addr)?;
89+
broadcast_bond_on_nostr(fidelity)?;
7990
Ok(())
8091
}
8192

93+
// ##TODO: Make this part of nostr module and improve error handing
94+
// ##TODO: Try retry in case relay doesn't accept the event
95+
fn broadcast_bond_on_nostr(fidelity: FidelityProof) -> Result<(), MakerError> {
96+
let outpoint = fidelity.bond.outpoint;
97+
let content = format!("{}:{}", outpoint.txid, outpoint.vout);
98+
99+
// ##TODO: Don't use ephemeral keys
100+
let secret_key = SecretKey::generate();
101+
let keys = Keys::new(secret_key);
102+
103+
let event = EventBuilder::new(Kind::Custom(COINSWAP_KIND), content)
104+
.build(keys.public_key)
105+
.sign_with_keys(&keys)
106+
.expect("Event should be signed");
107+
108+
let msg = ClientMessage::Event(std::borrow::Cow::Owned(event));
109+
110+
log::debug!("nostr wire msg: {}", msg.as_json());
111+
112+
let mut success = false;
113+
114+
for relay in NOSTR_RELAYS {
115+
match broadcast_to_relay(relay, &msg) {
116+
Ok(()) => {
117+
success = true;
118+
}
119+
Err(e) => {
120+
log::warn!("failed to broadcast to {}: {:?}", relay, e);
121+
}
122+
}
123+
}
124+
125+
if !success {
126+
log::warn!("nostr event was not accepted by any relay");
127+
}
128+
129+
Ok(())
130+
}
131+
132+
fn broadcast_to_relay(relay: &str, msg: &ClientMessage) -> Result<(), MakerError> {
133+
let (mut socket, _) = tungstenite::connect(relay).map_err(|e| {
134+
log::warn!("failed to connect to nostr relay {}: {}", relay, e);
135+
MakerError::General("failed to connect to nostr relay")
136+
})?;
137+
138+
socket
139+
.write(Message::Text(msg.as_json().into()))
140+
.map_err(|e| {
141+
log::warn!("nostr relay write failed: {}", e);
142+
MakerError::General("failed to write to nostr relay")
143+
})?;
144+
socket.flush().ok();
145+
146+
match socket.read() {
147+
Ok(Message::Text(text)) => {
148+
if let Ok(relay_msg) = RelayMessage::from_json(&text) {
149+
match relay_msg {
150+
RelayMessage::Ok {
151+
event_id,
152+
status: true,
153+
..
154+
} => {
155+
log::info!("nostr relay {} accepted event {}", relay, event_id);
156+
return Ok(());
157+
}
158+
RelayMessage::Ok {
159+
event_id,
160+
status: false,
161+
message,
162+
} => {
163+
log::warn!(
164+
"nostr relay {} rejected event {}: {}",
165+
relay,
166+
event_id,
167+
message
168+
);
169+
}
170+
_ => {}
171+
}
172+
}
173+
}
174+
Ok(_) => {}
175+
Err(e) => {
176+
log::warn!("nostr relay {} read error: {}", relay, e);
177+
}
178+
}
179+
log::warn!("nostr relay {} did not confirm event", relay);
180+
181+
Err(MakerError::General("nostr relay did not confirm event"))
182+
}
183+
82184
/// Ensures the wallet has a valid fidelity bond. If no active bond exists, it creates a new one.
83185
///
84186
/// ### NOTE ON VALID FIDELITY BOND:
@@ -152,7 +254,7 @@ fn setup_fidelity_bond(maker: &Maker, maker_address: &str) -> Result<FidelityPro
152254
let fidelity_result = maker.get_wallet().write()?.create_fidelity(
153255
amount,
154256
locktime,
155-
Some(maker_address.as_bytes()),
257+
Some(maker_address),
156258
MIN_FEE_RATE,
157259
);
158260

0 commit comments

Comments
 (0)