mirror of
https://github.com/logos-messaging/logos-chat-rs.git
synced 2026-05-18 20:59:40 +00:00
Initial commit
This commit is contained in:
commit
fc23911bd3
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/target
|
||||
/.DS_Store
|
||||
.DS_Store
|
||||
/libs
|
||||
1821
Cargo.lock
generated
Normal file
1821
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
Cargo.toml
Normal file
5
Cargo.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[workspace]
|
||||
members = ["chat"]
|
||||
|
||||
# optional but nice
|
||||
resolver = "2"
|
||||
61
Makefile
Normal file
61
Makefile
Normal file
@ -0,0 +1,61 @@
|
||||
# Config
|
||||
REPO_URL = https://github.com/logos-messaging/logos-messaging-nim
|
||||
REPO_DIR = logos-messaging-nim
|
||||
OUTPUT_DIR = libs
|
||||
LIBWAKU_NIM_PARAMS ?= --undef:metrics
|
||||
|
||||
# Platform-specific library name
|
||||
ifeq ($(shell uname),Darwin)
|
||||
LIB_NAME = libwaku.dylib
|
||||
# macOS needs SDKROOT for Clang to find <string.h> and friends.
|
||||
export SDKROOT ?= $(shell xcrun --show-sdk-path)
|
||||
else
|
||||
LIB_NAME = libwaku.so
|
||||
endif
|
||||
|
||||
.PHONY: all clean setup build copy
|
||||
|
||||
all: setup build copy
|
||||
|
||||
# 1. Setup: Clone and initialize submodules
|
||||
setup:
|
||||
@echo "--- [1/3] Checking dependencies ---"
|
||||
@if ! command -v nim > /dev/null; then \
|
||||
echo "Error: Nim is not installed. Please run: brew install nim"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "--- Checking repository ---"
|
||||
@if [ ! -d "$(REPO_DIR)" ]; then \
|
||||
echo "Cloning logos-messaging-nim..."; \
|
||||
git clone $(REPO_URL) $(REPO_DIR); \
|
||||
else \
|
||||
echo "Repository exists. Updating..."; \
|
||||
cd $(REPO_DIR) && git pull; \
|
||||
fi
|
||||
@echo "--- Initializing Submodules ---"
|
||||
cd $(REPO_DIR) && git submodule update --init --recursive
|
||||
|
||||
# 2. Build: Use the repo's internal 'make'
|
||||
build:
|
||||
@echo "--- [2/3] Building libwaku ---"
|
||||
@echo "Using SDKROOT: $(SDKROOT)"
|
||||
@# Update vendored deps
|
||||
cd $(REPO_DIR) && $(MAKE) update
|
||||
@# Compile
|
||||
cd $(REPO_DIR) && $(MAKE) libwaku BUILD_COMMAND="libwakuDynamic $(LIBWAKU_NIM_PARAMS)"
|
||||
|
||||
# 3. Retrieve: Copy the result
|
||||
copy:
|
||||
@echo "--- [3/3] Retrieving library ---"
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
@if [ -f "$(REPO_DIR)/build/$(LIB_NAME)" ]; then \
|
||||
cp "$(REPO_DIR)/build/$(LIB_NAME)" "$(OUTPUT_DIR)/$(LIB_NAME)"; \
|
||||
else \
|
||||
echo "Error: Could not find $(LIB_NAME) in $(REPO_DIR)/build/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Success! Library located at: ./$(OUTPUT_DIR)/$(LIB_NAME)"
|
||||
|
||||
clean:
|
||||
rm -rf $(OUTPUT_DIR)
|
||||
rm -rf $(REPO_DIR)
|
||||
18
build.rs
Normal file
18
build.rs
Normal file
@ -0,0 +1,18 @@
|
||||
fn main() -> Result<(), std::io::Error> {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let libs_dir = std::path::Path::new(&manifest_dir).join("libs");
|
||||
println!("cargo:rustc-link-search=native={}", libs_dir.display());
|
||||
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
match target_os.as_str() {
|
||||
"macos" | "linux" => {
|
||||
println!("cargo:rustc-link-lib=dylib=waku");
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", libs_dir.display());
|
||||
}
|
||||
other => {
|
||||
panic!("Unsupported target OS: {other}. Only macOS and Linux are supported.");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
23
chat/Cargo.toml
Normal file
23
chat/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "chat-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "chat_rs"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "dev"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
async-trait = "0.1.89"
|
||||
base64 = "0.22"
|
||||
chrono = "0.4.42"
|
||||
mpsc = "0.2.6"
|
||||
serde_json = "1"
|
||||
tokio = { version = "1.48.0", features = ["full", "tokio-macros"] }
|
||||
tracing = "0.1"
|
||||
libchat = {git = "https://github.com/logos-messaging/libchat.git"}
|
||||
12
chat/build.rs
Normal file
12
chat/build.rs
Normal file
@ -0,0 +1,12 @@
|
||||
fn main() {
|
||||
// libwaku.dylib lives in <workspace_root>/libs/
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let libs_dir = std::path::Path::new(&manifest_dir)
|
||||
.parent()
|
||||
.expect("chat package should have a parent directory")
|
||||
.join("libs");
|
||||
|
||||
println!("cargo:rustc-link-search=native={}", libs_dir.display());
|
||||
println!("cargo:rustc-link-lib=dylib=waku");
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", libs_dir.display());
|
||||
}
|
||||
0
chat/src/client.rs
Normal file
0
chat/src/client.rs
Normal file
47
chat/src/ds.rs
Normal file
47
chat/src/ds.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use std::sync::mpsc;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DeliveryServiceError {
|
||||
WakuNodeAlreadyInitialized(String),
|
||||
WakuPublishMessageError(String),
|
||||
Other(anyhow::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DeliveryServiceError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::WakuNodeAlreadyInitialized(s) => write!(f, "waku node already initialized: {s}"),
|
||||
Self::WakuPublishMessageError(s) => write!(f, "waku publish error: {s}"),
|
||||
Self::Other(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DeliveryServiceError {}
|
||||
|
||||
pub mod transport {
|
||||
use std::sync::mpsc;
|
||||
|
||||
use super::DeliveryServiceError;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InboundPacket {
|
||||
pub payload: Vec<u8>,
|
||||
pub subtopic: String,
|
||||
pub group_id: String,
|
||||
pub app_id: Vec<u8>,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
pub struct OutboundPacket {
|
||||
pub group_id: String,
|
||||
pub subtopic: String,
|
||||
pub payload: Vec<u8>,
|
||||
pub app_id: Vec<u8>,
|
||||
}
|
||||
|
||||
pub trait DeliveryService {
|
||||
fn send(&self, pkt: OutboundPacket) -> Result<String, DeliveryServiceError>;
|
||||
fn subscribe(&self) -> mpsc::Receiver<InboundPacket>;
|
||||
}
|
||||
}
|
||||
6
chat/src/lib.rs
Normal file
6
chat/src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod client;
|
||||
pub mod ds;
|
||||
pub mod waku;
|
||||
|
||||
pub const GROUP_VERSION: &str = "1";
|
||||
pub const SUBTOPICS: &[&str] = &["chat"];
|
||||
26
chat/src/main.rs
Normal file
26
chat/src/main.rs
Normal file
@ -0,0 +1,26 @@
|
||||
mod ds;
|
||||
mod waku;
|
||||
|
||||
use ds::transport::DeliveryService;
|
||||
use waku::{WakuConfig, WakuDeliveryService};
|
||||
|
||||
const GROUP_VERSION: &str = "1";
|
||||
const SUBTOPICS: &[&str] = &["chat"];
|
||||
|
||||
fn main() {
|
||||
let result = WakuDeliveryService::start(WakuConfig::default())
|
||||
.expect("failed to start waku node");
|
||||
|
||||
println!("Waku node started. Listening for messages...");
|
||||
|
||||
let rx = result.service.subscribe();
|
||||
for pkt in rx {
|
||||
println!(
|
||||
"--- received {} bytes (group={}, subtopic={}) ---",
|
||||
pkt.payload.len(),
|
||||
pkt.group_id,
|
||||
pkt.subtopic
|
||||
);
|
||||
println!("raw bytes: {:?}", pkt.payload);
|
||||
}
|
||||
}
|
||||
360
chat/src/waku/mod.rs
Normal file
360
chat/src/waku/mod.rs
Normal file
@ -0,0 +1,360 @@
|
||||
//! Waku transport implementation and Waku-backed `DeliveryService`.
|
||||
|
||||
pub(crate) mod sys;
|
||||
pub(crate) mod wrapper;
|
||||
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::ds::{
|
||||
DeliveryServiceError,
|
||||
transport::{DeliveryService, InboundPacket, OutboundPacket},
|
||||
};
|
||||
use wrapper::WakuNodeCtx;
|
||||
|
||||
use super::{GROUP_VERSION, SUBTOPICS};
|
||||
|
||||
/// The pubsub topic for the Waku Node.
|
||||
pub fn pubsub_topic() -> String {
|
||||
"/waku/2/rs/15/1".to_string()
|
||||
}
|
||||
|
||||
/// Build the content topics for a group.
|
||||
pub fn build_content_topics(group_name: &str) -> Vec<String> {
|
||||
SUBTOPICS
|
||||
.iter()
|
||||
.map(|subtopic| build_content_topic(group_name, GROUP_VERSION, subtopic))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Build the content topic string: `/{group_name}/{version}/{subtopic}/proto`
|
||||
pub fn build_content_topic(group_name: &str, group_version: &str, subtopic: &str) -> String {
|
||||
format!("/{group_name}/{group_version}/{subtopic}/proto")
|
||||
}
|
||||
|
||||
// ── Outbound command ────────────────────────────────────────────────────────
|
||||
struct OutboundCommand {
|
||||
pkt: OutboundPacket,
|
||||
reply: mpsc::SyncSender<Result<String, DeliveryServiceError>>,
|
||||
}
|
||||
|
||||
// ── Subscriber registry ─────────────────────────────────────────────────────
|
||||
type SubscriberList = Arc<Mutex<Vec<mpsc::SyncSender<InboundPacket>>>>;
|
||||
|
||||
/// Result returned by [`WakuDeliveryService::start`].
|
||||
pub struct WakuStartResult {
|
||||
pub service: WakuDeliveryService,
|
||||
/// The local ENR (Ethereum Node Record) if discv5 is enabled.
|
||||
/// Pass this to other nodes via `WakuConfig::discv5_bootstrap_enrs`.
|
||||
pub enr: Option<String>,
|
||||
}
|
||||
|
||||
/// Waku-backed delivery service.
|
||||
///
|
||||
/// The service starts an embedded Waku node on a dedicated `std::thread`.
|
||||
/// All interaction is via synchronous `std::sync::mpsc` channels.
|
||||
///
|
||||
/// Use [`WakuDeliveryService::start`] to create an instance. Call
|
||||
/// [`shutdown`](WakuDeliveryService::shutdown) for explicit cleanup, or
|
||||
/// simply drop all clones to stop the background thread.
|
||||
#[derive(Clone)]
|
||||
pub struct WakuDeliveryService {
|
||||
outbound: mpsc::SyncSender<OutboundCommand>,
|
||||
subscribers: SubscriberList,
|
||||
enr: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WakuConfig {
|
||||
pub node_port: u16,
|
||||
/// Enable discv5 peer discovery. Nodes on the same clusterId/shard
|
||||
/// will find each other automatically.
|
||||
pub discv5: bool,
|
||||
/// UDP port for discv5 discovery (default: 9000).
|
||||
pub discv5_udp_port: u16,
|
||||
/// Bootstrap ENR strings for discv5. If empty and discv5 is enabled,
|
||||
/// this node acts as a bootstrap node (others must know its ENR).
|
||||
pub discv5_bootstrap_enrs: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for WakuConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
node_port: 60000,
|
||||
discv5: false,
|
||||
discv5_udp_port: 9000,
|
||||
discv5_bootstrap_enrs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WakuDeliveryService {
|
||||
/// Start a Waku node and return both the delivery service and the local ENR
|
||||
/// (if discv5 is enabled).
|
||||
pub fn start(cfg: WakuConfig) -> Result<WakuStartResult, DeliveryServiceError> {
|
||||
let (out_tx, out_rx) = mpsc::sync_channel::<OutboundCommand>(256);
|
||||
let subscribers: SubscriberList = Arc::new(Mutex::new(Vec::new()));
|
||||
let (ready_tx, ready_rx) = mpsc::channel::<Result<Option<String>, DeliveryServiceError>>();
|
||||
|
||||
let subs_for_thread = subscribers.clone();
|
||||
|
||||
thread::Builder::new()
|
||||
.name("waku-node".into())
|
||||
.spawn(move || {
|
||||
if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
Self::node_thread(cfg, out_rx, subs_for_thread, ready_tx);
|
||||
})) {
|
||||
let msg = panic
|
||||
.downcast_ref::<&str>()
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| panic.downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "unknown panic".to_string());
|
||||
error!("waku-node thread panicked: {msg}");
|
||||
}
|
||||
})
|
||||
.map_err(|e| DeliveryServiceError::Other(anyhow::anyhow!(e)))?;
|
||||
|
||||
// Wait for the node to either start or fail.
|
||||
let enr = ready_rx
|
||||
.recv()
|
||||
.map_err(|e| DeliveryServiceError::Other(anyhow::anyhow!(e)))??;
|
||||
|
||||
let service = Self {
|
||||
outbound: out_tx,
|
||||
subscribers,
|
||||
enr: enr.clone(),
|
||||
};
|
||||
Ok(WakuStartResult { service, enr })
|
||||
}
|
||||
|
||||
/// The local ENR (Ethereum Node Record), available when discv5 is enabled.
|
||||
pub fn enr(&self) -> Option<&str> {
|
||||
self.enr.as_deref()
|
||||
}
|
||||
|
||||
/// Explicitly shut down the background Waku node thread.
|
||||
///
|
||||
/// After calling this, all subsequent [`send`](DeliveryService::send) calls
|
||||
/// will return an error. Alternatively, dropping all clones of this service
|
||||
/// achieves the same effect.
|
||||
pub fn shutdown(self) {
|
||||
drop(self.outbound);
|
||||
}
|
||||
|
||||
fn node_thread(
|
||||
cfg: WakuConfig,
|
||||
out_rx: mpsc::Receiver<OutboundCommand>,
|
||||
subscribers: SubscriberList,
|
||||
ready_tx: mpsc::Sender<Result<Option<String>, DeliveryServiceError>>,
|
||||
) {
|
||||
let mut config = serde_json::json!({
|
||||
"host": "0.0.0.0",
|
||||
"port": cfg.node_port,
|
||||
"relay": true,
|
||||
"clusterId": 15,
|
||||
"shards": [1],
|
||||
"numShardsInNetwork": 8,
|
||||
"logLevel": "ERROR",
|
||||
// Keep metrics disabled for libwaku runtime to avoid exposing
|
||||
// Prometheus/logging endpoints from embedded nodes.
|
||||
"metricsServer": false,
|
||||
"metricsLogging": false,
|
||||
});
|
||||
|
||||
if cfg.discv5 {
|
||||
config["discv5Discovery"] = serde_json::json!(true);
|
||||
config["discv5UdpPort"] = serde_json::json!(cfg.discv5_udp_port);
|
||||
if !cfg.discv5_bootstrap_enrs.is_empty() {
|
||||
config["discv5BootstrapNodes"] = serde_json::json!(cfg.discv5_bootstrap_enrs);
|
||||
}
|
||||
}
|
||||
|
||||
let config_json = config.to_string();
|
||||
|
||||
// Create node
|
||||
let waku = match WakuNodeCtx::new(&config_json) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
let _ = ready_tx.send(Err(DeliveryServiceError::WakuNodeAlreadyInitialized(e)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Start node
|
||||
if let Err(e) = waku.start() {
|
||||
let _ = ready_tx.send(Err(DeliveryServiceError::WakuNodeAlreadyInitialized(e)));
|
||||
return;
|
||||
}
|
||||
info!("Waku node started");
|
||||
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
|
||||
// Retrieve ENR for discv5 bootstrapping (discv5 is started automatically
|
||||
// by waku_start when discv5Discovery=true is in the config JSON).
|
||||
let local_enr = if cfg.discv5 {
|
||||
match waku.get_enr() {
|
||||
Ok(enr) => {
|
||||
info!("Local ENR: {enr}");
|
||||
Some(enr)
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Could not retrieve ENR: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Explicit relay subscribe as safety net (config shards may auto-subscribe,
|
||||
// but this ensures we're subscribed regardless of libwaku version behavior).
|
||||
let topic = pubsub_topic();
|
||||
if let Err(e) = waku.relay_subscribe(&topic) {
|
||||
// Non-fatal: some libwaku versions reject duplicate subscribe
|
||||
info!("relay_subscribe returned (may already be subscribed): {e}");
|
||||
}
|
||||
|
||||
// Set event callback — this closure must live for the node lifetime.
|
||||
let subs_for_cb = subscribers.clone();
|
||||
let event_closure = move |_ret: i32, data: &str| {
|
||||
if let Some(pkt) = Self::parse_event(data) {
|
||||
let guard = subs_for_cb.lock();
|
||||
// If the mutex is poisoned, subscribers are lost — log and skip.
|
||||
let mut guard = match guard {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
error!("Subscriber mutex poisoned: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
guard.retain(|tx| match tx.try_send(pkt.clone()) {
|
||||
Ok(()) => true,
|
||||
Err(mpsc::TrySendError::Full(_)) => true, // keep — just slow
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => false, // drop — dead
|
||||
});
|
||||
}
|
||||
};
|
||||
// Heap-allocated closure — must stay alive until node stops.
|
||||
let _event_cb_guard = waku.set_event_callback(event_closure);
|
||||
|
||||
// Signal ready (with ENR)
|
||||
let _ = ready_tx.send(Ok(local_enr));
|
||||
|
||||
// Outbound loop — blocks until all senders drop
|
||||
let topic = pubsub_topic();
|
||||
while let Ok(cmd) = out_rx.recv() {
|
||||
let res = Self::do_publish(&waku, &topic, cmd.pkt);
|
||||
let _ = cmd.reply.try_send(res);
|
||||
}
|
||||
|
||||
// _event_cb_guard dropped here, then waku dropped (calls waku_stop via Drop)
|
||||
info!("Waku outbound loop finished");
|
||||
}
|
||||
|
||||
fn do_publish(
|
||||
waku: &WakuNodeCtx,
|
||||
pubsub_topic: &str,
|
||||
pkt: OutboundPacket,
|
||||
) -> Result<String, DeliveryServiceError> {
|
||||
let content_topic = build_content_topic(&pkt.group_id, GROUP_VERSION, &pkt.subtopic);
|
||||
let payload_b64 = BASE64.encode(&pkt.payload);
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
let meta_b64 = BASE64.encode(&pkt.app_id);
|
||||
|
||||
let msg_json = serde_json::json!({
|
||||
"payload": payload_b64,
|
||||
"contentTopic": content_topic,
|
||||
"timestamp": timestamp as u64,
|
||||
"version": 2,
|
||||
"meta": meta_b64,
|
||||
})
|
||||
.to_string();
|
||||
|
||||
waku.relay_publish(pubsub_topic, &msg_json, 10_000)
|
||||
.map_err(|e| {
|
||||
error!("Failed to relay publish: {e}");
|
||||
DeliveryServiceError::WakuPublishMessageError(e)
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a waku event JSON into an InboundPacket (if it's a message event).
|
||||
fn parse_event(data: &str) -> Option<InboundPacket> {
|
||||
let v: serde_json::Value = serde_json::from_str(data).ok()?;
|
||||
|
||||
let waku_msg = v.get("wakuMessage")?;
|
||||
let payload_b64 = waku_msg.get("payload")?.as_str()?;
|
||||
let payload = BASE64.decode(payload_b64).ok()?;
|
||||
let content_topic = waku_msg.get("contentTopic")?.as_str()?;
|
||||
let timestamp = waku_msg
|
||||
.get("timestamp")
|
||||
.and_then(|t| t.as_i64())
|
||||
.unwrap_or(0);
|
||||
|
||||
let meta = waku_msg
|
||||
.get("meta")
|
||||
.and_then(|m| m.as_str())
|
||||
.and_then(|m| BASE64.decode(m).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Parse content topic: /{group_name}/{version}/{subtopic}/proto
|
||||
let (group_id, subtopic) = Self::parse_content_topic(content_topic)?;
|
||||
|
||||
debug!("Inbound message: group={group_id} subtopic={subtopic}");
|
||||
|
||||
Some(InboundPacket {
|
||||
payload,
|
||||
subtopic,
|
||||
group_id,
|
||||
app_id: meta,
|
||||
timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse `/{group_name}/{version}/{subtopic}/proto` into (group_id, subtopic).
|
||||
fn parse_content_topic(ct: &str) -> Option<(String, String)> {
|
||||
// Expected: "/{group_name}/{version}/{subtopic}/proto"
|
||||
let mut parts = ct.split('/');
|
||||
let _empty = parts.next()?; // leading ""
|
||||
let group_id = parts.next()?;
|
||||
let _version = parts.next()?;
|
||||
let subtopic = parts.next()?;
|
||||
if group_id.is_empty() || subtopic.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((group_id.to_owned(), subtopic.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl DeliveryService for WakuDeliveryService {
|
||||
fn send(&self, pkt: OutboundPacket) -> Result<String, DeliveryServiceError> {
|
||||
let (reply_tx, reply_rx) = mpsc::sync_channel(1);
|
||||
self.outbound
|
||||
.send(OutboundCommand {
|
||||
pkt,
|
||||
reply: reply_tx,
|
||||
})
|
||||
.map_err(|e| DeliveryServiceError::Other(anyhow::anyhow!(e)))?;
|
||||
|
||||
reply_rx
|
||||
.recv()
|
||||
.map_err(|e| DeliveryServiceError::Other(anyhow::anyhow!(e)))?
|
||||
}
|
||||
|
||||
fn subscribe(&self) -> mpsc::Receiver<InboundPacket> {
|
||||
let (tx, rx) = mpsc::sync_channel(256);
|
||||
match self.subscribers.lock() {
|
||||
Ok(mut g) => g.push(tx),
|
||||
Err(e) => {
|
||||
error!("Subscriber mutex poisoned, subscription lost: {e}");
|
||||
}
|
||||
}
|
||||
rx
|
||||
}
|
||||
}
|
||||
96
chat/src/waku/sys.rs
Normal file
96
chat/src/waku/sys.rs
Normal file
@ -0,0 +1,96 @@
|
||||
//! Raw FFI declarations matching libwaku.h (trampoline pattern).
|
||||
#![allow(unused)]
|
||||
|
||||
use std::os::raw::{c_char, c_int, c_uint, c_void};
|
||||
use std::slice;
|
||||
|
||||
pub type FFICallBack = unsafe extern "C" fn(c_int, *const c_char, usize, *const c_void);
|
||||
|
||||
#[link(name = "waku")]
|
||||
unsafe extern "C" {
|
||||
pub fn waku_new(
|
||||
config_json: *const c_char,
|
||||
cb: FFICallBack,
|
||||
user_data: *const c_void,
|
||||
) -> *mut c_void;
|
||||
|
||||
pub fn waku_start(ctx: *mut c_void, cb: FFICallBack, user_data: *const c_void) -> c_int;
|
||||
|
||||
pub fn waku_stop(ctx: *mut c_void, cb: FFICallBack, user_data: *const c_void) -> c_int;
|
||||
|
||||
pub fn waku_version(ctx: *mut c_void, cb: FFICallBack, user_data: *const c_void) -> c_int;
|
||||
|
||||
pub fn set_event_callback(ctx: *mut c_void, cb: FFICallBack, user_data: *const c_void);
|
||||
|
||||
pub fn waku_relay_publish(
|
||||
ctx: *mut c_void,
|
||||
cb: FFICallBack,
|
||||
user_data: *const c_void,
|
||||
pubsub_topic: *const c_char,
|
||||
json_message: *const c_char,
|
||||
timeout_ms: c_uint,
|
||||
) -> c_int;
|
||||
|
||||
pub fn waku_relay_subscribe(
|
||||
ctx: *mut c_void,
|
||||
cb: FFICallBack,
|
||||
user_data: *const c_void,
|
||||
pubsub_topic: *const c_char,
|
||||
) -> c_int;
|
||||
|
||||
pub fn waku_connect(
|
||||
ctx: *mut c_void,
|
||||
cb: FFICallBack,
|
||||
user_data: *const c_void,
|
||||
peer_multi_addr: *const c_char,
|
||||
timeout_ms: c_uint,
|
||||
) -> c_int;
|
||||
|
||||
pub fn waku_get_my_peerid(ctx: *mut c_void, cb: FFICallBack, user_data: *const c_void)
|
||||
-> c_int;
|
||||
|
||||
pub fn waku_start_discv5(ctx: *mut c_void, cb: FFICallBack, user_data: *const c_void) -> c_int;
|
||||
|
||||
pub fn waku_stop_discv5(ctx: *mut c_void, cb: FFICallBack, user_data: *const c_void) -> c_int;
|
||||
|
||||
pub fn waku_get_my_enr(ctx: *mut c_void, cb: FFICallBack, user_data: *const c_void) -> c_int;
|
||||
|
||||
pub fn waku_discv5_update_bootnodes(
|
||||
ctx: *mut c_void,
|
||||
cb: FFICallBack,
|
||||
user_data: *const c_void,
|
||||
bootnodes: *const c_char,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
// ── Trampoline pattern ──────────────────────────────────────────────────────
|
||||
|
||||
pub unsafe extern "C" fn trampoline<C>(
|
||||
return_val: c_int,
|
||||
buffer: *const c_char,
|
||||
buffer_len: usize,
|
||||
data: *const c_void,
|
||||
) where
|
||||
C: FnMut(i32, &str),
|
||||
{
|
||||
if data.is_null() {
|
||||
return;
|
||||
}
|
||||
let closure = unsafe { &mut *(data as *mut C) };
|
||||
if buffer.is_null() || buffer_len == 0 {
|
||||
closure(return_val, "");
|
||||
return;
|
||||
}
|
||||
let bytes = unsafe { slice::from_raw_parts(buffer as *const u8, buffer_len) };
|
||||
let buffer_str = String::from_utf8_lossy(bytes);
|
||||
closure(return_val, &buffer_str);
|
||||
}
|
||||
|
||||
pub fn get_trampoline<C>(_closure: &C) -> FFICallBack
|
||||
where
|
||||
C: FnMut(i32, &str),
|
||||
{
|
||||
trampoline::<C>
|
||||
}
|
||||
|
||||
pub const RET_OK: i32 = 0;
|
||||
282
chat/src/waku/wrapper.rs
Normal file
282
chat/src/waku/wrapper.rs
Normal file
@ -0,0 +1,282 @@
|
||||
//! Safe synchronous wrapper around the raw libwaku FFI.
|
||||
#![allow(unused)]
|
||||
use std::cell::OnceCell;
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_void;
|
||||
|
||||
use super::sys::{self as waku_sys, RET_OK, get_trampoline};
|
||||
|
||||
/// Opaque handle to a libwaku node context.
|
||||
pub struct WakuNodeCtx {
|
||||
ctx: *mut c_void,
|
||||
}
|
||||
|
||||
// The libwaku ctx pointer is thread-safe (single node, serialized calls inside C).
|
||||
unsafe impl Send for WakuNodeCtx {}
|
||||
unsafe impl Sync for WakuNodeCtx {}
|
||||
|
||||
impl WakuNodeCtx {
|
||||
/// Create a new waku node. `config_json` is the JSON string for node configuration.
|
||||
pub fn new(config_json: &str) -> Result<Self, String> {
|
||||
let config_cstr = CString::new(config_json).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut err: Option<String> = None;
|
||||
let mut closure = |ret: i32, data: &str| {
|
||||
if ret != RET_OK {
|
||||
err = Some(data.to_string());
|
||||
}
|
||||
};
|
||||
let cb = get_trampoline(&closure);
|
||||
|
||||
let ctx = unsafe {
|
||||
waku_sys::waku_new(
|
||||
config_cstr.as_ptr(),
|
||||
cb,
|
||||
&mut closure as *mut _ as *const c_void,
|
||||
)
|
||||
};
|
||||
|
||||
if ctx.is_null() || err.is_some() {
|
||||
return Err(err.unwrap_or_else(|| "waku_new returned null".into()));
|
||||
}
|
||||
|
||||
Ok(Self { ctx })
|
||||
}
|
||||
|
||||
/// Start the node.
|
||||
pub fn start(&self) -> Result<(), String> {
|
||||
let mut err: Option<String> = None;
|
||||
let mut closure = |ret: i32, data: &str| {
|
||||
if ret != RET_OK {
|
||||
err = Some(data.to_string());
|
||||
}
|
||||
};
|
||||
let cb = get_trampoline(&closure);
|
||||
|
||||
let ret =
|
||||
unsafe { waku_sys::waku_start(self.ctx, cb, &mut closure as *mut _ as *const c_void) };
|
||||
|
||||
if ret != RET_OK || err.is_some() {
|
||||
return Err(err.unwrap_or_else(|| format!("waku_start returned {ret}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the node version string.
|
||||
pub fn version(&self) -> Result<String, String> {
|
||||
let version: OnceCell<String> = OnceCell::new();
|
||||
let mut closure = |ret: i32, data: &str| {
|
||||
if ret == RET_OK {
|
||||
let _ = version.set(data.to_string());
|
||||
}
|
||||
};
|
||||
let cb = get_trampoline(&closure);
|
||||
|
||||
let ret = unsafe {
|
||||
waku_sys::waku_version(self.ctx, cb, &mut closure as *mut _ as *const c_void)
|
||||
};
|
||||
|
||||
if ret != RET_OK {
|
||||
return Err(format!("waku_version returned {ret}"));
|
||||
}
|
||||
version
|
||||
.into_inner()
|
||||
.ok_or_else(|| "no version returned".into())
|
||||
}
|
||||
|
||||
/// Get the local peer ID.
|
||||
pub fn get_peer_id(&self) -> Result<String, String> {
|
||||
let peer_id: OnceCell<String> = OnceCell::new();
|
||||
let mut closure = |ret: i32, data: &str| {
|
||||
if ret == RET_OK {
|
||||
let _ = peer_id.set(data.to_string());
|
||||
}
|
||||
};
|
||||
let cb = get_trampoline(&closure);
|
||||
|
||||
let ret = unsafe {
|
||||
waku_sys::waku_get_my_peerid(self.ctx, cb, &mut closure as *mut _ as *const c_void)
|
||||
};
|
||||
|
||||
if ret != RET_OK {
|
||||
return Err(format!("waku_get_my_peerid returned {ret}"));
|
||||
}
|
||||
peer_id
|
||||
.into_inner()
|
||||
.ok_or_else(|| "no peer id returned".into())
|
||||
}
|
||||
|
||||
/// Connect to a peer by multiaddr.
|
||||
pub fn connect(&self, peer_multi_addr: &str, timeout_ms: u32) -> Result<(), String> {
|
||||
let addr_cstr = CString::new(peer_multi_addr).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut err: Option<String> = None;
|
||||
let mut closure = |ret: i32, data: &str| {
|
||||
if ret != RET_OK {
|
||||
err = Some(data.to_string());
|
||||
}
|
||||
};
|
||||
let cb = get_trampoline(&closure);
|
||||
|
||||
let ret = unsafe {
|
||||
waku_sys::waku_connect(
|
||||
self.ctx,
|
||||
cb,
|
||||
&mut closure as *mut _ as *const c_void,
|
||||
addr_cstr.as_ptr(),
|
||||
timeout_ms,
|
||||
)
|
||||
};
|
||||
|
||||
if ret != RET_OK || err.is_some() {
|
||||
return Err(err.unwrap_or_else(|| format!("waku_connect returned {ret}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Publish a message via relay. `json_message` is the waku message JSON.
|
||||
pub fn relay_publish(
|
||||
&self,
|
||||
pubsub_topic: &str,
|
||||
json_message: &str,
|
||||
timeout_ms: u32,
|
||||
) -> Result<String, String> {
|
||||
let topic_cstr = CString::new(pubsub_topic).map_err(|e| e.to_string())?;
|
||||
let msg_cstr = CString::new(json_message).map_err(|e| e.to_string())?;
|
||||
|
||||
let result: OnceCell<String> = OnceCell::new();
|
||||
let mut err: Option<String> = None;
|
||||
let mut closure = |ret: i32, data: &str| {
|
||||
if ret == RET_OK {
|
||||
let _ = result.set(data.to_string());
|
||||
} else {
|
||||
err = Some(data.to_string());
|
||||
}
|
||||
};
|
||||
let cb = get_trampoline(&closure);
|
||||
|
||||
let ret = unsafe {
|
||||
waku_sys::waku_relay_publish(
|
||||
self.ctx,
|
||||
cb,
|
||||
&mut closure as *mut _ as *const c_void,
|
||||
topic_cstr.as_ptr(),
|
||||
msg_cstr.as_ptr(),
|
||||
timeout_ms,
|
||||
)
|
||||
};
|
||||
|
||||
if ret != RET_OK || err.is_some() {
|
||||
return Err(err.unwrap_or_else(|| format!("waku_relay_publish returned {ret}")));
|
||||
}
|
||||
Ok(result.into_inner().unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Subscribe to a relay pubsub topic.
|
||||
pub fn relay_subscribe(&self, pubsub_topic: &str) -> Result<(), String> {
|
||||
let topic_cstr = CString::new(pubsub_topic).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut err: Option<String> = None;
|
||||
let mut closure = |ret: i32, data: &str| {
|
||||
if ret != RET_OK {
|
||||
err = Some(data.to_string());
|
||||
}
|
||||
};
|
||||
let cb = get_trampoline(&closure);
|
||||
|
||||
let ret = unsafe {
|
||||
waku_sys::waku_relay_subscribe(
|
||||
self.ctx,
|
||||
cb,
|
||||
&mut closure as *mut _ as *const c_void,
|
||||
topic_cstr.as_ptr(),
|
||||
)
|
||||
};
|
||||
|
||||
if ret != RET_OK || err.is_some() {
|
||||
return Err(err.unwrap_or_else(|| format!("waku_relay_subscribe returned {ret}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register the event callback. Returns the boxed closure — caller must keep
|
||||
/// it alive for the lifetime of the node (dropping it invalidates the FFI pointer).
|
||||
pub fn set_event_callback<C>(&self, closure: C) -> Box<C>
|
||||
where
|
||||
C: FnMut(i32, &str),
|
||||
{
|
||||
let mut boxed = Box::new(closure);
|
||||
let cb = get_trampoline(&*boxed);
|
||||
unsafe {
|
||||
waku_sys::set_event_callback(self.ctx, cb, &mut *boxed as *mut C as *const c_void);
|
||||
}
|
||||
boxed
|
||||
}
|
||||
|
||||
/// Stop the node. Should be called before dropping to cleanly release resources.
|
||||
pub fn stop(&self) -> Result<(), String> {
|
||||
let mut err: Option<String> = None;
|
||||
let mut closure = |ret: i32, data: &str| {
|
||||
if ret != RET_OK {
|
||||
err = Some(data.to_string());
|
||||
}
|
||||
};
|
||||
let cb = get_trampoline(&closure);
|
||||
|
||||
let ret =
|
||||
unsafe { waku_sys::waku_stop(self.ctx, cb, &mut closure as *mut _ as *const c_void) };
|
||||
|
||||
if ret != RET_OK || err.is_some() {
|
||||
return Err(err.unwrap_or_else(|| format!("waku_stop returned {ret}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the discv5 discovery service.
|
||||
pub fn start_discv5(&self) -> Result<(), String> {
|
||||
let mut err: Option<String> = None;
|
||||
let mut closure = |ret: i32, data: &str| {
|
||||
if ret != RET_OK {
|
||||
err = Some(data.to_string());
|
||||
}
|
||||
};
|
||||
let cb = get_trampoline(&closure);
|
||||
|
||||
let ret = unsafe {
|
||||
waku_sys::waku_start_discv5(self.ctx, cb, &mut closure as *mut _ as *const c_void)
|
||||
};
|
||||
|
||||
if ret != RET_OK || err.is_some() {
|
||||
return Err(err.unwrap_or_else(|| format!("waku_start_discv5 returned {ret}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the local ENR (Ethereum Node Record) string.
|
||||
pub fn get_enr(&self) -> Result<String, String> {
|
||||
let enr: OnceCell<String> = OnceCell::new();
|
||||
let mut closure = |ret: i32, data: &str| {
|
||||
if ret == RET_OK {
|
||||
let _ = enr.set(data.to_string());
|
||||
}
|
||||
};
|
||||
let cb = get_trampoline(&closure);
|
||||
|
||||
let ret = unsafe {
|
||||
waku_sys::waku_get_my_enr(self.ctx, cb, &mut closure as *mut _ as *const c_void)
|
||||
};
|
||||
|
||||
if ret != RET_OK {
|
||||
return Err(format!("waku_get_my_enr returned {ret}"));
|
||||
}
|
||||
enr.into_inner().ok_or_else(|| "no ENR returned".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WakuNodeCtx {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = self.stop() {
|
||||
tracing::warn!("waku_stop failed during drop: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user