Compare commits

...

33 Commits

Author SHA1 Message Date
Darshan
eb847fdb80
fix: ci workflow (#125) 2025-12-16 17:53:30 +05:30
Darshan
a02e631d53
chore: align with recent rename (#124) 2025-12-16 01:40:40 +05:30
Darshan K
4ec20e2ebe
chore: upgrade to V0.36.0 (#123) 2025-11-13 00:12:21 +05:30
gabrielmer
57505a1c06
chore: update store json to camelCase (#117) 2025-04-11 14:26:07 +03:00
Ivan FB
1d95d05dd7
avoid closure to be dropped in release mode (#116) 2025-03-05 09:39:15 +01:00
Ivan FB
9c9900897e
Adapt for nwaku v0.35 (#114)
* build.rs: initialize submodules the first time cargo build is invoked in waku-sys
* messagehash.rs only contains a String as the hex representation of msg hash
* events.rs adapt test to parse message event containing msg-hash in hex string format
* bump nwaku to v0.35.0
* waku-sys/build.rs use STATIC=1 instead of STATIC=true
2025-02-12 10:06:16 +01:00
richΛrd
90577d1d58
feat: add discv5_bootstrap_nodes attribute to the config (#112) 2025-02-03 13:33:14 -04:00
Ivan FB
247f740f1d
docs: add simple README.md for all examples (#111)
Co-authored-by: richΛrd <info@richardramos.me>
2025-01-20 16:15:45 +01:00
Ivan FB
75b5c4c4f8
add Makefile and scripts allow build libwaku in cargo publish (#108) 2025-01-14 17:03:40 +01:00
Ivan Folgueira Bande
64736c0284
README.md change go-waku with nwaku 2025-01-13 15:30:08 +01:00
Ivan FB
c2412134d3
upgrade major version to create 1.0.0 (#107) 2025-01-13 09:50:05 +01:00
Ivan FB
0c0b834aa0
Recover toy chat app powered by libwaku/nwaku (#105)
* bump nwaku to the current master branch ( commit: 625c8ee5 )
* make the waku crate to behave tokio-asynchronously
* use of store
* use of lightpush and filter
* add waku-bindings/src/general/messagehash.rs
* add waku-bindings/src/general/time.rs
* add waku-bindings/src/general/waku_decode.rs
* add WakuEvent management (WakuMessage, ConnectionChange, TopicHealthChange.)
* add waku-bindings/src/macros.rs
2025-01-10 15:19:31 +01:00
Ivan FB
fd7e73a7f0
Tic tac toe example (#104)
* update nwaku vendor to v0.33.1
* build.rs: add negentropy dependency and cmdCount cmdLine dependencies
* fix: call waku_setup when instantiating waku_new
* Properly decode a Vec<Multiaddr>
* First commit tic-tac-toe
* adding some simple game logic to coordinate the turns a little
* some logic to panic if a proper event callback hasn't been set
* restoring back the type state pattern introduced by Richard
* new PubsubTopic type
* fix clippy issues

---------

Co-authored-by: Richard Ramos <info@richardramos.me>
2024-11-28 10:35:41 +01:00
richΛrd
7a2e4d1d01
feat: RLN Relay (#101) 2024-04-01 15:15:50 -04:00
richΛrd
1ed7dd48ed
chore: disable windows builds temporarily (#100) 2024-03-21 09:17:59 -04:00
richΛrd
201a38a64e
feat: example app (#99) 2024-03-11 12:00:30 -04:00
richΛrd
69a48725ca
refactor: node handle constructor and messageHash on publish (#98)
* refactor: node handle constructor and messageId on publish
* refactor: add back typestate
* chore: rename messageId to messageHash
2024-03-01 11:31:00 -04:00
richΛrd
a10a5c2d22
refactor: use a enum for handling the responses, and don't ignore the return code (#97)
* refactor: handle return code and use an enum to handle responses
* fix: nwaku does not return an envelope hash on publish
2024-03-01 11:25:02 -04:00
richΛrd
646f6f0080
refactor: hide *mut c_void behind WakunodeContext (#96) 2024-02-26 11:13:30 -04:00
Richard Ramos
c434e9ebf5
refactor: remove getters 2024-02-22 10:57:02 -04:00
Richard Ramos
9d3b575037
chore: remove example (for now) 2024-02-21 15:54:55 -04:00
Richard Ramos
ac96b834a0
chore: use rust node address instead of fleet node in tests 2024-02-21 14:07:42 -04:00
Richard Ramos
79b8428bb5
feat: add listen addresses 2024-02-21 11:59:40 -04:00
Richard Ramos
9c696e6097
feat: expose waku_version 2024-02-20 16:06:03 -04:00
Richard Ramos
685a6aef0a
fix: event handler 2024-02-16 10:45:20 -04:00
Richard Ramos
1f9283a849
fix: event callback signature 2024-02-13 18:03:34 -04:00
Richard Ramos
5687e2585c
chore: make sure waku_new, waku_start and waku_stop work 2024-02-13 16:18:16 -04:00
Richard Ramos
ca72e70bb6
chore: link required libraries and RLN 2024-02-13 14:50:00 -04:00
Richard Ramos
1d8626b110
chore: remove code no longer needed (for now) 2024-02-12 20:13:49 -04:00
Richard Ramos
8755d9a7c8
refactor: remove unneeded functions and change some data types 2024-02-08 17:16:34 -04:00
Richard Ramos
9d73230c8a
chore: remove unneeded files (for now) 2024-02-08 17:15:49 -04:00
Richard Ramos
9750a329ab
feat: use nwaku instead of go-waku 2024-02-08 11:54:49 -04:00
richΛrd
1ea96f80b2
feat: add dns discovery parameters (#85) 2024-02-06 13:38:51 -04:00
52 changed files with 9172 additions and 3466 deletions

View File

@ -4,7 +4,7 @@ on:
branches:
- master
name: Codecov
name: Code Coverage
jobs:
test:

View File

@ -10,26 +10,24 @@ name: CI
jobs:
check:
name: Check
name: Build
strategy:
matrix:
include:
- os: ubuntu-latest
toolchain: stable-x86_64-unknown-linux-gnu
- os: windows-latest
toolchain: stable-x86_64-pc-windows-gnu
toolchain: stable
#- os: windows-latest
# toolchain: stable-x86_64-pc-windows-gnu
- os: macos-latest
toolchain: stable-x86_64-apple-darwin
toolchain: stable
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Checkout submodules
run: git submodule update --init --recursive
- uses: actions/setup-go@v3 # we need go to build go-waku
with:
go-version: '1.20'
- uses: actions-rs/toolchain@v1
with:
profile: minimal
@ -42,26 +40,24 @@ jobs:
command: check
test:
name: Test Suite
name: Test
strategy:
matrix:
include:
- os: ubuntu-latest
toolchain: stable-x86_64-unknown-linux-gnu
- os: windows-latest
toolchain: stable-x86_64-pc-windows-gnu
toolchain: stable
#- os: windows-latest
# toolchain: stable-x86_64-pc-windows-gnu
- os: macos-latest
toolchain: stable-x86_64-apple-darwin
toolchain: stable
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Checkout submodules
run: git submodule update --init --recursive
- uses: actions/setup-go@v3 # we need go to build go-waku
with:
go-version: '1.20'
- uses: actions-rs/toolchain@v1
with:
profile: minimal
@ -78,7 +74,7 @@ jobs:
command: test
lints:
name: Rust lints
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@ -86,9 +82,6 @@ jobs:
submodules: true
- name: Checkout submodules
run: git submodule update --init --recursive
- uses: actions/setup-go@v3 # we need go to build go-waku
with:
go-version: '1.20'
- uses: actions-rs/toolchain@v1
with:
profile: minimal

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
**target
/Cargo.lock
/.idea
/.fleet
/.fleet
nimcache/

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "waku-sys/vendor"]
path = waku-sys/vendor
url = https://github.com/status-im/go-waku.git
url = https://github.com/logos-messaging/logos-messaging-nim

2224
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,5 +3,5 @@
members = [
"waku-bindings",
"waku-sys",
"examples/toy-chat"
"examples/basic"
]

View File

@ -9,12 +9,12 @@
[crates-url]: https://crates.io/crates/waku-bindings
[docs-badge]: https://docs.rs/waku-bindings/badge.svg
[docs-url]: https://docs.rs/waku-bindings
[actions-badge]: https://github.com/waku-org/waku-rust-bindings/workflows/CI/badge.svg
[actions-url]: https://github.com/waku-org/waku-rust-bindings/actions/workflows/main.yml?query=workflow%3ACI+branch%3Amaster
[codecov-badge]: https://codecov.io/github/waku-org/waku-rust-bindings/branch/main/graph/badge.svg?token=H4CQWRUCUS
[codecov-url]: https://codecov.io/github/waku-org/waku-rust-bindings
[actions-badge]: https://github.com/logos-messaging/logos-messaging-rust-bindings/workflows/CI/badge.svg
[actions-url]: https://github.com/logos-messaging/logos-messaging-rust-bindings/actions/workflows/main.yml?query=workflow%3ACI+branch%3Amaster
[codecov-badge]: https://codecov.io/github/logos-messaging/logos-messaging-rust-bindings/branch/main/graph/badge.svg?token=H4CQWRUCUS
[codecov-url]: https://codecov.io/github/logos-messaging/logos-messaging-rust-bindings
Rust layer on top of [`go-waku`](https://github.com/waku-org/go-waku) [C FFI bindings](https://github.com/waku-org/go-waku/blob/master/library/README.md).
Rust layer on top of [`logos-messaging-nim`](https://github.com/logos-messaging/logos-messaging-nim) [C FFI bindings](https://github.com/logos-messaging/logos-messaging-nim/blob/master/library/libwaku.h).
## About Waku

5161
examples/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
[workspace]
members = [
"basic",
"tic-tac-toe-gui",
"toy-chat"
]
]

13
examples/basic/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "basic"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
futures = "0.3.30"
tokio = { version = "1.36.0", features = ["full"] }
tokio-util = { version = "0.7.10", features = ["rt"] }
waku = { path = "../../waku-bindings", package = "waku-bindings" }
serde_json = "1.0"

19
examples/basic/README.md Normal file
View File

@ -0,0 +1,19 @@
### Description
This is a very simplistic example where two waku nodes are instantiated within the same Rust app.
### What it does
1. Instantiates two Waku nodes
2. Each node registers an event callback (waku message event, connection change event, etc.)
3. Each node starts
4. Each node perform relay subscription
5. "node1" publishes a waku message
6. Both nodes are stopped
### How to run
From within the `examples/basic/` foder run:
```code
cargo run
```

147
examples/basic/src/main.rs Normal file
View File

@ -0,0 +1,147 @@
use std::io::Error;
use std::str::from_utf8;
use tokio::time::{sleep, Duration};
use waku::{
general::pubsubtopic::PubsubTopic, waku_new, Encoding, LibwakuResponse, WakuContentTopic,
WakuEvent, WakuMessage, WakuNodeConfig,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let node1 = waku_new(Some(WakuNodeConfig {
tcp_port: Some(60010), // TODO: use any available port.
..Default::default()
}))
.await
.expect("should instantiate");
let node2 = waku_new(Some(WakuNodeConfig {
tcp_port: Some(60020), // TODO: use any available port.
..Default::default()
}))
.await
.expect("should instantiate");
// ========================================================================
// Setting an event callback to be executed each time a message is received
node2
.set_event_callback(|response| {
if let LibwakuResponse::Success(v) = response {
let event: WakuEvent =
serde_json::from_str(v.unwrap().as_str()).expect("Parsing event to succeed");
match event {
WakuEvent::WakuMessage(evt) => {
// println!("WakuMessage event received: {:?}", evt.waku_message);
let message = evt.waku_message;
let payload = message.payload.to_vec();
let msg = from_utf8(&payload).expect("should be valid message");
println!("::::::::::::::::::::::::::::::::::::::::::::::::::::");
println!("Message Received in NODE 2: {}", msg);
println!("::::::::::::::::::::::::::::::::::::::::::::::::::::");
}
WakuEvent::RelayTopicHealthChange(_evt) => {
// dbg!("Relay topic change evt", evt);
}
WakuEvent::ConnectionChange(_evt) => {
// dbg!("Conn change evt", evt);
}
WakuEvent::Unrecognized(err) => panic!("Unrecognized waku event: {:?}", err),
_ => panic!("event case not expected"),
};
}
})
.expect("set event call back working");
node1
.set_event_callback(|response| {
if let LibwakuResponse::Success(v) = response {
let event: WakuEvent =
serde_json::from_str(v.unwrap().as_str()).expect("Parsing event to succeed");
match event {
WakuEvent::WakuMessage(evt) => {
// println!("WakuMessage event received: {:?}", evt.waku_message);
let message = evt.waku_message;
let payload = message.payload.to_vec();
let msg = from_utf8(&payload).expect("should be valid message");
println!("::::::::::::::::::::::::::::::::::::::::::::::::::::");
println!("Message Received in NODE 1: {}", msg);
println!("::::::::::::::::::::::::::::::::::::::::::::::::::::");
}
WakuEvent::RelayTopicHealthChange(_evt) => {
// dbg!("Relay topic change evt", evt);
}
WakuEvent::ConnectionChange(_evt) => {
// dbg!("Conn change evt", evt);
}
WakuEvent::Unrecognized(err) => panic!("Unrecognized waku event: {:?}", err),
_ => panic!("event case not expected"),
};
}
})
.expect("set event call back working");
let node1 = node1.start().await.expect("node1 should start");
let node2 = node2.start().await.expect("node2 should start");
// ========================================================================
// Subscribe to pubsub topic
let topic = PubsubTopic::new("test");
node1
.relay_subscribe(&topic)
.await
.expect("node1 should subscribe");
node2
.relay_subscribe(&topic)
.await
.expect("node2 should subscribe");
// ========================================================================
// Connect nodes with each other
let addresses2 = node2
.listen_addresses()
.await
.expect("should obtain the addresses");
node1
.connect(&addresses2[0], None)
.await
.expect("node1 should connect to node2");
// ========================================================================
// Wait for gossipsub mesh to form
sleep(Duration::from_secs(2)).await;
// ========================================================================
// Publish a message
let content_topic = WakuContentTopic::new("waku", "2", "test", Encoding::Proto);
let message = WakuMessage::new("Hello world", content_topic, 0, Vec::new(), false);
node1
.relay_publish_message(&message, &topic, None)
.await
.expect("should have sent the message");
// ========================================================================
// Waiting for message to arrive
sleep(Duration::from_secs(1)).await;
// ========================================================================
// Stop both instances
let node1 = node1.stop().await.expect("should stop");
let node2 = node2.stop().await.expect("should stop");
// ========================================================================
// Free resources
node1.waku_destroy().await.expect("should deallocate");
node2.waku_destroy().await.expect("should deallocate");
Ok(())
}

View File

@ -0,0 +1,17 @@
[package]
name = "tic-tac-toe-gui"
version = "0.1.0"
edition = "2021"
[dependencies]
waku = { path = "../../waku-bindings", package = "waku-bindings" }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
ark-std = "0.4"
ctrlc = "3.2.4"
tokio = { version = "1", features = ["full"] }
tokio-util = "0.6" # for utility functions if needed
egui = "0.22"
eframe = "0.22"
secp256k1 = { version = "0.26", features = ["rand", "recovery", "serde"] }

View File

@ -0,0 +1,24 @@
### Description
This is a tic-tac-toe example that aims to show how to deal with
a Waku node in an app with UI. The example is very naïve and it
assumes only two tic-tac-toe instances are running globally. Therefore, the game messages might collide with other plays.
The game board is shown within a Rust eframe.
### What it does
1. Instantiates one Waku node
2. Starts the Waku node
3. Registers the node to waku events (messages, connection change, topic health, etc.)
4. Subscribes de node to the game_topic
### How to run
From within the `examples/tic-tac-toe/` foder run:
```code
cargo run
```
Another player can start their instance in either another
terminal or another machine.

View File

@ -0,0 +1,409 @@
use eframe::egui;
use serde::{Deserialize, Serialize};
use std::str::from_utf8;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::task;
use tokio::sync::mpsc;
use waku::{
waku_new, Encoding, WakuEvent, LibwakuResponse, WakuContentTopic,
WakuMessage, WakuNodeConfig, WakuNodeHandle, Initialized, Running,
general::pubsubtopic::PubsubTopic,
};
#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone)]
enum Player {
X,
O,
}
#[derive(Serialize, Deserialize, Clone)]
struct GameState {
board: [[Option<Player>; 3]; 3],
current_turn: Player,
moves_left: usize,
}
struct TicTacToeApp<State> {
game_state: Arc<Mutex<GameState>>,
waku: WakuNodeHandle<State>,
game_topic: PubsubTopic,
tx: mpsc::Sender<String>, // Sender to send `msg` to main thread
player_role: Option<Player>, // Store the player's role (X or O)
}
impl TicTacToeApp<Initialized> {
fn new(
waku: WakuNodeHandle<Initialized>,
game_topic: PubsubTopic,
game_state: Arc<Mutex<GameState>>,
tx: mpsc::Sender<String>,
) -> Self {
Self {
game_state,
waku,
game_topic,
tx,
player_role: None,
}
}
async fn start(self) -> TicTacToeApp<Running> {
let tx_clone = self.tx.clone();
let game_content_topic = WakuContentTopic::new("waku", "2", "tictactoegame", Encoding::Proto);
let my_closure = move |response| {
if let LibwakuResponse::Success(v) = response {
let event: WakuEvent =
serde_json::from_str(v.unwrap().as_str()).expect("Parsing event to succeed");
match event {
WakuEvent::WakuMessage(evt) => {
let message = evt.waku_message;
// Filter: only process messages for our game content topic
if message.content_topic != game_content_topic {
return; // Skip messages from other apps
}
let payload = message.payload.to_vec();
match from_utf8(&payload) {
Ok(msg) => {
// Lock succeeded, proceed to send the message
if tx_clone.blocking_send(msg.to_string()).is_err() {
eprintln!("Failed to send message to async task");
}
}
Err(e) => {
eprintln!("Failed to decode payload as UTF-8: {}", e);
// Handle the error as needed, or just log and skip
}
}
},
WakuEvent::RelayTopicHealthChange(_evt) => {
// dbg!("Relay topic change evt", evt);
},
WakuEvent::ConnectionChange(_evt) => {
// dbg!("Conn change evt", evt);
},
WakuEvent::Unrecognized(err) => panic!("Unrecognized waku event: {:?}", err),
_ => panic!("event case not expected"),
};
}
};
// Establish a closure that handles the incoming messages
self.waku.set_event_callback(my_closure).expect("set event call back working");
// Start the waku node
let waku = self.waku.start().await.expect("waku should start");
// Subscribe to desired topic using the relay protocol
waku.relay_subscribe(&self.game_topic).await.expect("waku should subscribe");
// Example filter subscription. This is needed in edge nodes (resource-restricted devices)
// Nodes usually use either relay or lightpush/filter protocols
// let ctopic = WakuContentTopic::new("waku", "2", "tictactoegame", Encoding::Proto);
// let content_topics = vec![ctopic];
// waku.filter_subscribe(&self.game_topic, content_topics).await.expect("waku should subscribe");
// End filter example ----------------------------------------
// Example to establish direct connection to a well-known node
// Connect to hard-coded node
// let target_node_multi_addr =
// "/ip4/159.223.242.94/tcp/30303/p2p/16Uiu2HAmAUdrQ3uwzuE4Gy4D56hX6uLKEeerJAnhKEHZ3DxF1EfT"
// // "/dns4/store-01.do-ams3.status.prod.status.im/tcp/30303/p2p/16Uiu2HAmAUdrQ3uwzuE4Gy4D56hX6uLKEeerJAnhKEHZ3DxF1EfT"
// // "/ip4/24.144.78.119/tcp/30303/p2p/16Uiu2HAm3xVDaz6SRJ6kErwC21zBJEZjavVXg7VSkoWzaV1aMA3F"
// .parse::<Multiaddr>().expect("parse multiaddress");
// self.waku.connect(&target_node_multi_addr, None)
// .expect("waku should connect to other node");
// End example direct connection
TicTacToeApp {
game_state: self.game_state,
waku,
game_topic: self.game_topic,
tx: self.tx,
player_role: self.player_role,
}
}
}
impl TicTacToeApp<Running> {
async fn send_game_state(&self, game_state: &GameState) {
let serialized_game_state = serde_json::to_string(game_state).unwrap();
let content_topic = WakuContentTopic::new("waku", "2", "tictactoegame", Encoding::Proto);
let message = WakuMessage::new(
&serialized_game_state,
content_topic,
0,
Vec::new(),
false,
);
if let Ok(msg_hash) = self.waku.relay_publish_message(&message, &self.game_topic, None).await {
dbg!(format!("message hash published: {}", msg_hash));
}
// Example lightpush publish message. This is needed in edge nodes (resource-restricted devices)
// Nodes usually use either relay or lightpush/filter protocols
//
// let msg_hash_ret = self.waku.lightpush_publish_message(&message, &self.game_topic).await;
// match msg_hash_ret {
// Ok(msg_hash) => println!("Published message hash {:?}", msg_hash.to_string()),
// Err(error) => println!("Failed to publish with lightpush: {}", error)
// }
// End example lightpush publish message
}
fn make_move(&mut self, row: usize, col: usize) {
if let Ok(mut game_state) = self.game_state.try_lock() {
if let Some(my_role) = self.player_role {
if game_state.current_turn != my_role {
return; // skip click if not my turn
}
}
if game_state.board[row][col].is_none() && game_state.moves_left > 0 {
game_state.board[row][col] = Some(game_state.current_turn);
game_state.moves_left -= 1;
if let Some(winner) = self.check_winner(&game_state) {
game_state.current_turn = winner;
} else {
game_state.current_turn = match game_state.current_turn {
Player::X => Player::O,
Player::O => Player::X,
};
}
// Call the async function in a blocking context
task::block_in_place(|| {
// Obtain the current runtime handle
let handle = tokio::runtime::Handle::current();
// Block on the async function
handle.block_on(async {
// Assuming `self` is available in the current context
self.send_game_state(&game_state).await;
});
});
}
}
}
fn check_winner(&self, game_state: &GameState) -> Option<Player> {
// Check rows, columns, and diagonals
for i in 0..3 {
if game_state.board[i][0] == game_state.board[i][1] &&
game_state.board[i][1] == game_state.board[i][2] {
if let Some(player) = game_state.board[i][0] {
return Some(player);
}
}
if game_state.board[0][i] == game_state.board[1][i] &&
game_state.board[1][i] == game_state.board[2][i] {
if let Some(player) = game_state.board[0][i] {
return Some(player);
}
}
}
if game_state.board[0][0] == game_state.board[1][1] &&
game_state.board[1][1] == game_state.board[2][2] {
if let Some(player) = game_state.board[0][0] {
return Some(player);
}
}
if game_state.board[0][2] == game_state.board[1][1] &&
game_state.board[1][1] == game_state.board[2][0] {
if let Some(player) = game_state.board[0][2] {
return Some(player);
}
}
None
}
fn reset_game(&mut self) {
self.game_state = Arc::new(Mutex::new(GameState {
board: [[None; 3]; 3],
current_turn: Player::X,
moves_left: 9,
}));
self.player_role = None
}
}
impl eframe::App for TicTacToeApp<Running> {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Request a repaint every second
ctx.request_repaint_after(Duration::from_secs(1));
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Tic-Tac-Toe");
// If the player hasn't selected a role, show the role selection buttons
if self.player_role.is_none() {
ui.label("Select your role:");
if ui.button("Play as X").clicked() {
self.player_role = Some(Player::X);
}
if ui.button("Play as O").clicked() {
self.player_role = Some(Player::O);
// Player O waits for Player X to make the first move
// No need to change current_turn as it's already X
}
return; // Exit early until a role is selected
}
let player_role = self.player_role.unwrap(); // Safe to unwrap because we've ensured it's Some
// Main game UI
ui.label(format!("You are playing as: {:?}", player_role));
// Draw the game board and handle the game state
let board_size = ui.available_size();
let cell_size = board_size.x / 4.0;
ui.horizontal(|ui| {
for row in 0..3 {
ui.vertical(|ui| {
for col in 0..3 {
let label;
{
if let Ok(game_state) = self.game_state.try_lock() {
label = match game_state.board[row][col] {
Some(Player::X) => "X",
Some(Player::O) => "O",
None => "-",
};
}
else {
label = "#";
}
}
let button = ui.add(egui::Button::new(label).min_size(egui::vec2(cell_size, cell_size)).sense(egui::Sense::click()));
if button.clicked() {
self.make_move(row, col);
}
}
});
if row < 2 {
ui.add_space(4.0);
}
}
});
if let Ok(game_state) = self.game_state.try_lock() {
if let Some(winner) = self.check_winner(&game_state) {
ui.label(format!(
"Player {} wins!",
match winner {
Player::X => "X",
Player::O => "O",
}
));
} else if game_state.moves_left == 0 {
ui.label("It's a tie!");
} else {
ui.label(format!(
"Player {}'s turn",
match game_state.current_turn {
Player::X => "X",
Player::O => "O",
}
));
}
}
if ui.add(egui::Button::new("Restart Game")).clicked() {
self.reset_game();
}
});
}
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
// TODO: implement the cleanup an proper stop of waku node
}
}
#[tokio::main]
async fn main() -> eframe::Result<()> {
let (tx, mut rx) = mpsc::channel::<String>(3200); // Channel to communicate between threads
let game_topic = PubsubTopic::new("/waku/2/rs/16/32");
// Create a Waku instance
let waku = waku_new(Some(WakuNodeConfig {
tcp_port: Some(60010),
cluster_id: Some(16),
shards: vec![1, 32, 64, 128, 256],
// node_key: Some(SecretKey::from_str("2fc0515879e52b7b73297cfd6ab3abf7c344ef84b7a90ff6f4cc19e05a198027").unwrap()),
max_message_size: Some("1024KiB".to_string()),
relay_topics: vec![String::from(&game_topic)],
log_level: Some("FATAL"), // Supported: TRACE, DEBUG, INFO, NOTICE, WARN, ERROR or FATAL
keep_alive: Some(true),
// Discovery
dns_discovery: Some(true),
dns_discovery_url: Some("enrtree://AMOJVZX4V6EXP7NTJPMAYJYST2QP6AJXYW76IU6VGJS7UVSNDYZG4@boot.prod.status.nodes.status.im"),
// discv5_discovery: Some(true),
// discv5_udp_port: Some(9001),
// discv5_enr_auto_update: Some(false),
..Default::default()
})).await
.expect("should instantiate");
let game_state = GameState {
board: [[None; 3]; 3],
current_turn: Player::X,
moves_left: 9,
};
let shared_state = Arc::new(Mutex::new(game_state));
let clone = shared_state.clone();
let app = TicTacToeApp::new(waku, game_topic, clone, tx);
let app = app.start().await;
let clone = shared_state.clone();
// Listen for messages in the main thread
tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
// println!("MSG received: {}", msg);
// Handle the received message, e.g., update the UI or game state
if let Ok(parsed_value) = serde_json::from_str::<GameState>(&msg)
{
if let Ok(mut unclocked_game_state) = clone.lock(){
*unclocked_game_state = parsed_value;
}
}
else {
eprintln!("Failed to parse JSON");
}
}
});
eframe::run_native(
"Tic-Tac-Toe Multiplayer via Waku",
eframe::NativeOptions {
initial_window_size: Some(egui::vec2(400.0, 400.0)),
..Default::default()
},
Box::new(|_cc| Box::new(app)),
)?;
Ok(())
}

View File

@ -8,9 +8,12 @@ authors = [
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
waku-bindings = { path = "../../waku-bindings" }
waku = { path = "../../waku-bindings", package = "waku-bindings" }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tui = "0.19"
crossterm = "0.25"
unicode-width = "0.1"
prost = "0.11"
chrono = "0.4"
chrono = "0.4"
tokio = { version = "1", features = ["full"] }

View File

@ -0,0 +1,21 @@
### Description
This is a chat example where multiple participants can talk within the same room.
### What it does
1. Instantiates one Waku node
2. Starts the Waku node
3. Registers the node to waku events (messages, connection change, topic health, etc.)
4. Subscribes de node to the game_topic
5. Retrieves previous chat messages at the beginning
### How to run
From within the `examples/toy-chat/` folder, run the following to start a chat using the given nick name.
e.g.:
```code
cargo run "Alice"
```

View File

@ -1,18 +1,18 @@
mod protocol;
use crate::protocol::{Chat2Message, TOY_CHAT_CONTENT_TOPIC};
use chrono::Utc;
use tokio::task;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use prost::Message;
use chrono::Utc;
use std::io::Write;
use std::str::FromStr;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use std::{error::Error, io};
use std::time::Duration;
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
@ -22,9 +22,9 @@ use tui::{
Frame, Terminal,
};
use unicode_width::UnicodeWidthStr;
use waku_bindings::{
waku_default_pubsub_topic, waku_new, waku_set_event_callback, ContentFilter, Multiaddr,
PagingOptions, ProtocolId, Running, StoreQuery, WakuMessage, WakuNodeHandle,
use waku::{
general::pubsubtopic::PubsubTopic, general::Result, waku_new, Initialized, LibwakuResponse, Running, WakuEvent,
WakuMessage, WakuNodeConfig, WakuNodeHandle,
};
enum InputMode {
@ -32,14 +32,12 @@ enum InputMode {
Editing,
}
const NODES: &[&str] = &[
"/dns4/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm",
"/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ",
"/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS"
];
const STORE_NODE: &str = "/dns4/store-01.do-ams3.status.staging.status.im/tcp/30303/p2p/16Uiu2HAm3xVDaz6SRJ6kErwC21zBJEZjavVXg7VSkoWzaV1aMA3F";
const DEFAULT_PUBSUB_TOPIC: &str = "/waku/2/rs/16/32";
/// App holds the state of the application
struct App {
struct App<State> {
/// Current value of the input box
input: String,
nick: String,
@ -47,77 +45,212 @@ struct App {
input_mode: InputMode,
/// History of recorded messages
messages: Arc<RwLock<Vec<Chat2Message>>>,
node_handle: WakuNodeHandle<Running>,
waku: WakuNodeHandle<State>,
}
impl App {
fn new(nick: String, node_handle: WakuNodeHandle<Running>) -> App {
App {
impl App<Initialized> {
async fn new(nick: String) -> Result<App<Initialized>> {
let pubsub_topic = PubsubTopic::new(DEFAULT_PUBSUB_TOPIC);
let waku = waku_new(Some(WakuNodeConfig {
tcp_port: Some(60010),
cluster_id: Some(16),
shards: vec![1, 32, 64, 128, 256],
// node_key: Some(SecretKey::from_str("2fc0515879e52b7b73297cfd6ab3abf7c344ef84b7a90ff6f4cc19e05a198027").unwrap()),
max_message_size: Some("1024KiB".to_string()),
relay_topics: vec![String::from(&pubsub_topic)],
log_level: Some("FATAL"), // Supported: TRACE, DEBUG, INFO, NOTICE, WARN, ERROR or FATAL
keep_alive: Some(true),
// Discovery
dns_discovery: Some(true),
dns_discovery_url: Some("enrtree://AMOJVZX4V6EXP7NTJPMAYJYST2QP6AJXYW76IU6VGJS7UVSNDYZG4@boot.prod.status.nodes.status.im"),
// discv5_discovery: Some(true),
// discv5_udp_port: Some(9001),
// discv5_enr_auto_update: Some(false),
..Default::default()
})).await?;
Ok(App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Arc::new(RwLock::new(Vec::new())),
node_handle,
nick,
waku,
})
}
async fn start_waku_node(self) -> Result<App<Running>> {
let shared_messages = Arc::clone(&self.messages);
self.waku.set_event_callback(move|response| {
if let LibwakuResponse::Success(v) = response {
let event: WakuEvent =
serde_json::from_str(v.unwrap().as_str()).expect("failed parsing event in set_event_callback");
match event {
WakuEvent::WakuMessage(evt) => {
if evt.waku_message.content_topic != TOY_CHAT_CONTENT_TOPIC {
return; // skip the messages that don't belong to the toy chat
}
match <Chat2Message as Message>::decode(evt.waku_message.payload()) {
Ok(chat_message) => {
// Add the new message to the front
{
let mut messages_lock = shared_messages.write().unwrap();
messages_lock.insert(0, chat_message); // Insert at the front (index 0)
}
}
Err(e) => {
let mut out = std::io::stderr();
write!(out, "{e:?}").unwrap();
}
}
},
WakuEvent::RelayTopicHealthChange(_evt) => {
// dbg!("Relay topic change evt", evt);
},
WakuEvent::ConnectionChange(_evt) => {
// dbg!("Conn change evt", evt);
},
WakuEvent::Unrecognized(err) => eprintln!("Unrecognized waku event: {:?}", err),
_ => eprintln!("event case not expected"),
};
}
})?;
let waku = self.waku.start().await?;
Ok(App {
input: self.input,
nick: self.nick,
input_mode: self.input_mode,
messages: self.messages,
waku,
})
}
}
impl App<Running> {
async fn retrieve_history(&mut self) {
let one_day_in_secs = 60 * 60 * 24;
let time_start = (Duration::from_secs(Utc::now().timestamp() as u64)
- Duration::from_secs(one_day_in_secs))
.as_nanos() as u64;
let include_data = true;
let messages = self.waku.store_query(None,
vec![TOY_CHAT_CONTENT_TOPIC.clone()],
STORE_NODE,
include_data,
Some(time_start),
None,
None).await.unwrap();
let messages: Vec<_> = messages
.into_iter()
// we expect messages because the query was passed with include_data == true
.filter(|item| item.message.is_some())
.map(|store_resp_msg| {
<Chat2Message as Message>::decode(store_resp_msg.message.unwrap().payload())
.expect("Toy chat messages should be decodeable")
})
.collect();
if !messages.is_empty() {
*self.messages.write().unwrap() = messages;
}
}
}
fn retrieve_history(
node_handle: &WakuNodeHandle<Running>,
) -> waku_bindings::Result<Vec<Chat2Message>> {
let self_id = node_handle.peer_id().unwrap();
let peer = node_handle
.peers()?
.iter()
.find(|&peer| peer.peer_id() != &self_id)
.cloned()
.unwrap();
let result = node_handle.store_query(
&StoreQuery {
pubsub_topic: None,
content_topics: vec![TOY_CHAT_CONTENT_TOPIC.clone()],
start_time: Some(
(Duration::from_secs(Utc::now().timestamp() as u64)
- Duration::from_secs(60 * 60 * 24))
.as_nanos() as usize,
),
end_time: None,
paging_options: Some(PagingOptions {
page_size: 25,
cursor: None,
forward: true,
}),
},
peer.peer_id(),
Some(Duration::from_secs(10)),
)?;
fn run_main_loop<B: Backend>(
&mut self,
terminal: &mut Terminal<B>,
) -> std::result::Result<(), Box<dyn Error>> {
loop {
terminal.draw(|f| ui(f, self))?;
Ok(result
.messages()
.iter()
.map(|waku_message| {
<Chat2Message as Message>::decode(waku_message.payload())
.expect("Toy chat messages should be decodeable")
})
.collect())
}
if event::poll(Duration::from_millis(500)).unwrap() {
if let Event::Key(key) = event::read()? {
match self.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('e') => {
self.input_mode = InputMode::Editing;
}
KeyCode::Char('q') => {
return Ok(());
}
_ => {}
},
InputMode::Editing => match key.code {
KeyCode::Enter => {
let message_content: String = self.input.drain(..).collect();
let message = Chat2Message::new(&self.nick, &message_content);
let mut buff = Vec::new();
let meta = Vec::new();
Message::encode(&message, &mut buff)?;
let waku_message = WakuMessage::new(
buff,
TOY_CHAT_CONTENT_TOPIC.clone(),
1,
meta,
false,
);
fn setup_node_handle() -> std::result::Result<WakuNodeHandle<Running>, Box<dyn Error>> {
let node_handle = waku_new(None)?;
let node_handle = node_handle.start()?;
for address in NODES.iter().map(|a| Multiaddr::from_str(a).unwrap()) {
let peerid = node_handle.add_peer(&address, ProtocolId::Relay)?;
node_handle.connect_peer_with_id(&peerid, None)?;
// Call the async function in a blocking context
task::block_in_place(|| {
// Obtain the current runtime handle
let handle = tokio::runtime::Handle::current();
// Block on the async function
handle.block_on(async {
// Assuming `self` is available in the current context
let pubsub_topic = PubsubTopic::new(DEFAULT_PUBSUB_TOPIC);
if let Err(e) = self.waku.relay_publish_message(
&waku_message,
&pubsub_topic,
None,
).await {
let mut out = std::io::stderr();
write!(out, "{e:?}").unwrap();
}
});
});
}
KeyCode::Char(c) => {
self.input.push(c);
}
KeyCode::Backspace => {
self.input.pop();
}
KeyCode::Esc => {
self.input_mode = InputMode::Normal;
}
_ => {}
},
}
}
}
}
}
let content_filter = ContentFilter::new(Some(waku_default_pubsub_topic()), vec![]);
node_handle.relay_subscribe(&content_filter)?;
Ok(node_handle)
async fn stop_app(self) {
self.waku.stop().await.expect("the node should stop properly");
}
}
fn main() -> std::result::Result<(), Box<dyn Error>> {
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn Error>> {
let nick = std::env::args().nth(1).expect("Nick to be set");
let app = App::new(nick).await?;
let mut app = app.start_waku_node().await?;
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
@ -125,40 +258,9 @@ fn main() -> std::result::Result<(), Box<dyn Error>> {
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let node_handle = setup_node_handle()?;
// create app and run it
let mut app = App::new(nick, node_handle);
let history = retrieve_history(&app.node_handle)?;
if !history.is_empty() {
*app.messages.write().unwrap() = history;
}
let shared_messages = Arc::clone(&app.messages);
waku_set_event_callback(move |signal| match signal.event() {
waku_bindings::Event::WakuMessage(event) => {
if event.waku_message().content_topic() != &TOY_CHAT_CONTENT_TOPIC {
return;
}
match <Chat2Message as Message>::decode(event.waku_message().payload()) {
Ok(chat_message) => {
shared_messages.write().unwrap().push(chat_message);
}
Err(e) => {
let mut out = std::io::stderr();
write!(out, "{e:?}").unwrap();
}
}
}
waku_bindings::Event::Unrecognized(data) => {
let mut out = std::io::stderr();
write!(out, "Error, received unrecognized event {data}").unwrap();
}
_ => {}
});
// app.node_handle.relay_publish_message(&WakuMessage::new(Chat2Message::new(&app.nick, format!(""))))
let res = run_app(&mut terminal, &mut app);
app.retrieve_history().await;
let res = app.run_main_loop(&mut terminal);
app.stop_app().await;
// restore terminal
disable_raw_mode()?;
@ -168,7 +270,6 @@ fn main() -> std::result::Result<(), Box<dyn Error>> {
DisableMouseCapture
)?;
terminal.show_cursor()?;
app.node_handle.stop()?;
if let Err(err) = res {
println!("{err:?}")
@ -176,65 +277,7 @@ fn main() -> std::result::Result<(), Box<dyn Error>> {
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
app: &mut App,
) -> std::result::Result<(), Box<dyn Error>> {
loop {
terminal.draw(|f| ui(f, app))?;
if let Event::Key(key) = event::read()? {
match app.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('e') => {
app.input_mode = InputMode::Editing;
}
KeyCode::Char('q') => {
return Ok(());
}
_ => {}
},
InputMode::Editing => match key.code {
KeyCode::Enter => {
let message_content: String = app.input.drain(..).collect();
let message = Chat2Message::new(&app.nick, &message_content);
let mut buff = Vec::new();
let meta = Vec::new();
Message::encode(&message, &mut buff)?;
let waku_message = WakuMessage::new(
buff,
TOY_CHAT_CONTENT_TOPIC.clone(),
1,
Utc::now().timestamp_nanos() as usize,
meta,
false,
);
if let Err(e) = app.node_handle.relay_publish_message(
&waku_message,
Some(waku_default_pubsub_topic()),
None,
) {
let mut out = std::io::stderr();
write!(out, "{e:?}").unwrap();
}
}
KeyCode::Char(c) => {
app.input.push(c);
}
KeyCode::Backspace => {
app.input.pop();
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
_ => {}
},
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui<B: Backend, State>(f: &mut Frame<B>, app: &App<State>) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)

View File

@ -1,6 +1,6 @@
use chrono::{DateTime, LocalResult, TimeZone, Utc};
use prost::Message;
use waku_bindings::{Encoding, WakuContentTopic};
use waku::{Encoding, WakuContentTopic};
pub static TOY_CHAT_CONTENT_TOPIC: WakuContentTopic =
WakuContentTopic::new("toy-chat", "2", "huilong", Encoding::Proto);

View File

@ -1,13 +1,15 @@
[package]
name = "waku-bindings"
version = "0.5.0"
version = "1.0.0"
edition = "2021"
authors = [
"Daniel Sanchez Quiros <danielsq@status.im>"
"Daniel Sanchez Quiros <danielsq@status.im>",
"Richard Ramos <richard@waku.org>",
"Ivan Folgueira Bande <ivansete@status.im>"
]
description = "Waku networking library"
license = "MIT OR Apache-2.0"
repository = "https://github.com/waku-org/waku-rust-bindings"
repository = "https://github.com/logos-messaging/logos-messaging-rust-bindings"
keywords = ["waku", "peer-to-peer", "libp2p", "networking"]
categories = ["network-programming"]
@ -26,9 +28,14 @@ serde_json = "1.0"
sscanf = "0.4"
smart-default = "0.6"
url = "2.3"
waku-sys = { version = "0.5.0", path = "../waku-sys" }
waku-sys = { version = "1.0.0", path = "../waku-sys" }
libc = "0.2"
serde-aux = "4.3.1"
rln = "0.3.4"
tokio = { version = "1", features = ["full"] }
regex = "1"
chrono = "0.4"
uuid = { version = "1.3", features = ["v4"] }
[dev-dependencies]
futures = "0.3.25"

View File

@ -1,9 +1,9 @@
# Waku Rust bindings
[<img alt="github" src="https://img.shields.io/badge/github-Github-red?style=for-the-badge&labelColor=555555&logo=github" height="20">](https://github.com/waku-org/waku-rust-bindings)
[<img alt="github" src="https://img.shields.io/badge/github-Github-red?style=for-the-badge&labelColor=555555&logo=github" height="20">](https://github.com/logos-messaging/logos-messaging-rust-bindings)
[<img alt="crates.io" src="https://img.shields.io/crates/v/waku-bindings.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/waku-bindings)
[<img alt="docs.rs" src="https://img.shields.io/badge/doc/waku-bindings-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/waku-bindings)
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/waku-org/waku-rust-bindings/main.yml?branch=master" height="20">](https://github.com/waku-org/waku-rust-bindings/actions/workflows/main.yml?query=branch%3Amaster)
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/logos-messaging/logos-messaging-rust-bindings/main.yml?branch=master" height="20">](https://github.com/logos-messaging/logos-messaging-rust-bindings/actions/workflows/main.yml?query=branch%3Amaster)
Rust api on top of [`waku-sys`](https://crates.io/crates/waku-sys) bindgen bindings to [c ffi bindings](https://github.com/status-im/go-waku/blob/v0.2.2/library/README.md).

View File

@ -1,91 +0,0 @@
//! Symmetric and asymmetric waku messages [decrypting](https://rfc.vac.dev/spec/36/#decrypting-messages) methods
// std
use std::ffi::CString;
// crates
use aes_gcm::{Aes256Gcm, Key};
use libc::*;
use secp256k1::SecretKey;
// internal
use crate::general::{DecodedPayload, Result, WakuMessage};
use crate::utils::{get_trampoline, handle_json_response};
/// Decrypt a message using a symmetric key
///
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_decode_symmetricchar-messagejson-char-symmetrickey)
pub fn waku_decode_symmetric(
message: &WakuMessage,
symmetric_key: &Key<Aes256Gcm>,
) -> Result<DecodedPayload> {
let symk = hex::encode(symmetric_key.as_slice());
let message_ptr = CString::new(
serde_json::to_string(&message)
.expect("WakuMessages should always be able to success serializing"),
)
.expect("CString should build properly from the serialized waku message")
.into_raw();
let symk_ptr = CString::new(symk)
.expect("CString should build properly from hex encoded symmetric key")
.into_raw();
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_decode_symmetric(
message_ptr,
symk_ptr,
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(message_ptr));
drop(CString::from_raw(symk_ptr));
out
};
handle_json_response(code, &result)
}
/// Decrypt a message using a symmetric key
///
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_decode_asymmetricchar-messagejson-char-privatekey)
pub fn waku_decode_asymmetric(
message: &WakuMessage,
asymmetric_key: &SecretKey,
) -> Result<DecodedPayload> {
let sk = hex::encode(asymmetric_key.secret_bytes());
let message_ptr = CString::new(
serde_json::to_string(&message)
.expect("WakuMessages should always be able to success serializing"),
)
.expect("CString should build properly from the serialized waku message")
.into_raw();
let sk_ptr = CString::new(sk)
.expect("CString should build properly from hex encoded symmetric key")
.into_raw();
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_decode_asymmetric(
message_ptr,
sk_ptr,
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(message_ptr));
drop(CString::from_raw(sk_ptr));
out
};
handle_json_response(code, &result)
}

View File

@ -1,101 +0,0 @@
// std
use std::ffi::CString;
// crates
use aes_gcm::{Aes256Gcm, Key};
use libc::*;
use secp256k1::{PublicKey, SecretKey};
// internal
use crate::general::{Result, WakuMessage};
use crate::utils::{get_trampoline, handle_json_response};
/// Optionally sign and encrypt a message using asymmetric encryption
pub fn waku_encode_asymmetric(
message: &WakuMessage,
public_key: &PublicKey,
signing_key: Option<&SecretKey>,
) -> Result<WakuMessage> {
let pk = hex::encode(public_key.serialize_uncompressed());
let sk = signing_key
.map(|signing_key| hex::encode(signing_key.secret_bytes()))
.unwrap_or_default();
let message_ptr = CString::new(
serde_json::to_string(&message)
.expect("WakuMessages should always be able to success serializing"),
)
.expect("CString should build properly from the serialized waku message")
.into_raw();
let pk_ptr = CString::new(pk)
.expect("CString should build properly from hex encoded public key")
.into_raw();
let sk_ptr = CString::new(sk)
.expect("CString should build properly from hex encoded signing key")
.into_raw();
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_encode_asymmetric(
message_ptr,
pk_ptr,
sk_ptr,
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(message_ptr));
drop(CString::from_raw(pk_ptr));
drop(CString::from_raw(sk_ptr));
out
};
handle_json_response(code, &result)
}
/// Optionally sign and encrypt a message using symmetric encryption
pub fn waku_encode_symmetric(
message: &WakuMessage,
symmetric_key: &Key<Aes256Gcm>,
signing_key: Option<&SecretKey>,
) -> Result<WakuMessage> {
let symk = hex::encode(symmetric_key.as_slice());
let sk = signing_key
.map(|signing_key| hex::encode(signing_key.secret_bytes()))
.unwrap_or_default();
let message_ptr = CString::new(
serde_json::to_string(&message)
.expect("WakuMessages should always be able to success serializing"),
)
.expect("CString should build properly from the serialized waku message")
.into_raw();
let symk_ptr = CString::new(symk)
.expect("CString should build properly from hex encoded symmetric key")
.into_raw();
let sk_ptr = CString::new(sk)
.expect("CString should build properly from hex encoded signing key")
.into_raw();
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_encode_symmetric(
message_ptr,
symk_ptr,
sk_ptr,
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(message_ptr));
drop(CString::from_raw(symk_ptr));
drop(CString::from_raw(sk_ptr));
out
};
handle_json_response(code, &result)
}

View File

@ -1,123 +0,0 @@
//! Waku message [event](https://rfc.vac.dev/spec/36/#events) related items
//!
//! Asynchronous events require a callback to be registered.
//! An example of an asynchronous event that might be emitted is receiving a message.
//! When an event is emitted, this callback will be triggered receiving a [`Signal`]
// std
use std::ffi::{c_char, c_int, c_void, CStr};
use std::ops::Deref;
use std::sync::Mutex;
// crates
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
// internal
use crate::general::{WakuMessage, WakuPubSubTopic};
use crate::MessageId;
/// Event signal
#[derive(Serialize, Deserialize)]
pub struct Signal {
/// Type of signal being emitted. Currently, only message is available
#[serde(alias = "type")]
_type: String,
/// Format depends on the type of signal
event: Event,
}
impl Signal {
pub fn event(&self) -> &Event {
&self.event
}
}
/// Waku event
/// For now just WakuMessage is supported
#[non_exhaustive]
#[derive(Serialize, Deserialize)]
#[serde(untagged, rename_all = "camelCase")]
pub enum Event {
WakuMessage(WakuMessageEvent),
Unrecognized(serde_json::Value),
}
/// Type of `event` field for a `message` event
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WakuMessageEvent {
/// The pubsub topic on which the message was received
pubsub_topic: WakuPubSubTopic,
/// The message id
message_id: MessageId,
/// The message in [`WakuMessage`] format
waku_message: WakuMessage,
}
impl WakuMessageEvent {
pub fn pubsub_topic(&self) -> &WakuPubSubTopic {
&self.pubsub_topic
}
pub fn message_id(&self) -> &String {
&self.message_id
}
pub fn waku_message(&self) -> &WakuMessage {
&self.waku_message
}
}
/// Shared callback slot. Callbacks are registered here so they can be accessed by the extern "C"
#[allow(clippy::type_complexity)]
static CALLBACK: Lazy<Mutex<Box<dyn FnMut(Signal) + Send + Sync>>> =
Lazy::new(|| Mutex::new(Box::new(|_| {})));
/// Register global callback
fn set_callback<F: FnMut(Signal) + Send + Sync + 'static>(f: F) {
*CALLBACK.lock().unwrap() = Box::new(f);
}
/// Wrapper callback, it transformst the `*const c_char` into a [`Signal`]
/// and executes the [`CALLBACK`] funtion with it
extern "C" fn callback(_ret_code: c_int, data: *const c_char, _user_data: *mut c_void) {
let raw_response = unsafe { CStr::from_ptr(data) }
.to_str()
.expect("Not null ptr");
let data: Signal = serde_json::from_str(raw_response).expect("Parsing signal to succeed");
(CALLBACK
.deref()
.lock()
.expect("Access to the shared callback")
.as_mut())(data)
}
/// Register callback to act as event handler and receive application signals,
/// which are used to react to asynchronous events in Waku
pub fn waku_set_event_callback<F: FnMut(Signal) + Send + Sync + 'static>(f: F) {
set_callback(f);
unsafe { waku_sys::waku_set_event_callback(Some(callback)) };
}
#[cfg(test)]
mod tests {
use crate::events::waku_set_event_callback;
use crate::{Event, Signal};
// TODO: how to actually send a signal and check if the callback is run?
#[test]
fn set_event_callback() {
waku_set_event_callback(|_signal| {});
}
#[test]
fn deserialize_signal() {
let s = "{\"type\":\"message\",\"event\":{\"messageId\":\"0x26ff3d7fbc950ea2158ce62fd76fd745eee0323c9eac23d0713843b0f04ea27c\",\"pubsubTopic\":\"/waku/2/default-waku/proto\",\"wakuMessage\":{\"payload\":\"SGkgZnJvbSDwn6aAIQ==\",\"contentTopic\":\"/toychat/2/huilong/proto\",\"timestamp\":1665580926660}}}";
let _: Signal = serde_json::from_str(s).unwrap();
}
#[test]
fn deserialize_event() {
let e = "{\"messageId\":\"0x26ff3d7fbc950ea2158ce62fd76fd745eee0323c9eac23d0713843b0f04ea27c\",\"pubsubTopic\":\"/waku/2/default-waku/proto\",\"wakuMessage\":{\"payload\":\"SGkgZnJvbSDwn6aAIQ==\",\"contentTopic\":\"/toychat/2/huilong/proto\",\"timestamp\":1665580926660}}";
let _: Event = serde_json::from_str(e).unwrap();
}
}

View File

@ -0,0 +1,142 @@
// std
use crate::general::waku_decode::WakuDecode;
use crate::general::Result;
use std::borrow::Cow;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
use sscanf::{scanf, RegexRepresentation};
/// WakuMessage encoding scheme
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub enum Encoding {
#[default]
Proto,
Rlp,
Rfc26,
Unknown(String),
}
impl Display for Encoding {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = match self {
Encoding::Proto => "proto",
Encoding::Rlp => "rlp",
Encoding::Rfc26 => "rfc26",
Encoding::Unknown(value) => value,
};
f.write_str(s)
}
}
impl FromStr for Encoding {
type Err = std::io::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"proto" => Ok(Self::Proto),
"rlp" => Ok(Self::Rlp),
"rfc26" => Ok(Self::Rfc26),
encoding => Ok(Self::Unknown(encoding.to_string())),
}
}
}
impl RegexRepresentation for Encoding {
const REGEX: &'static str = r"\w";
}
/// A waku content topic `/{application_name}/{version}/{content_topic_name}/{encdoing}`
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct WakuContentTopic {
pub application_name: Cow<'static, str>,
pub version: Cow<'static, str>,
pub content_topic_name: Cow<'static, str>,
pub encoding: Encoding,
}
impl WakuContentTopic {
pub const fn new(
application_name: &'static str,
version: &'static str,
content_topic_name: &'static str,
encoding: Encoding,
) -> Self {
Self {
application_name: Cow::Borrowed(application_name),
version: Cow::Borrowed(version),
content_topic_name: Cow::Borrowed(content_topic_name),
encoding,
}
}
pub fn join_content_topics(topics: Vec<WakuContentTopic>) -> String {
topics
.iter()
.map(|topic| topic.to_string())
.collect::<Vec<_>>()
.join(",")
}
}
impl WakuDecode for WakuContentTopic {
fn decode(input: &str) -> Result<Self> {
Ok(serde_json::from_str(input).expect("could not parse store resp"))
}
}
impl FromStr for WakuContentTopic {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if let Ok((application_name, version, content_topic_name, encoding)) =
scanf!(s, "/{}/{}/{}/{:/.+?/}", String, String, String, Encoding)
{
Ok(WakuContentTopic {
application_name: Cow::Owned(application_name),
version: Cow::Owned(version),
content_topic_name: Cow::Owned(content_topic_name),
encoding,
})
} else {
Err(
format!(
"Wrong pub-sub topic format. Should be `/{{application-name}}/{{version-of-the-application}}/{{content-topic-name}}/{{encoding}}`. Got: {s}"
)
)
}
}
}
impl Display for WakuContentTopic {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"/{}/{}/{}/{}",
self.application_name, self.version, self.content_topic_name, self.encoding
)
}
}
impl Serialize for WakuContentTopic {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
self.to_string().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for WakuContentTopic {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let as_string: String = String::deserialize(deserializer)?;
as_string
.parse::<WakuContentTopic>()
.map_err(D::Error::custom)
}
}

View File

@ -0,0 +1,64 @@
use crate::general::waku_decode::WakuDecode;
use crate::general::Result;
use std::convert::TryFrom;
use std::str;
use waku_sys::{RET_ERR, RET_MISSING_CALLBACK, RET_OK};
#[derive(Debug, Clone, Default, PartialEq)]
pub enum LibwakuResponse {
Success(Option<String>),
Failure(String),
MissingCallback,
#[default]
Undefined,
}
impl TryFrom<(u32, &str)> for LibwakuResponse {
type Error = String;
fn try_from((ret_code, response): (u32, &str)) -> std::result::Result<Self, Self::Error> {
let opt_value = Some(response.to_string()).filter(|s| !s.is_empty());
match ret_code {
RET_OK => Ok(LibwakuResponse::Success(opt_value)),
RET_ERR => Ok(LibwakuResponse::Failure(format!(
"waku error: {}",
response
))),
RET_MISSING_CALLBACK => Ok(LibwakuResponse::MissingCallback),
_ => Err(format!("undefined return code {}", ret_code)),
}
}
}
/// Used in cases where the FFI call doesn't return additional information in the
/// callback. Instead, it returns RET_OK, RET_ERR, etc.
pub fn handle_no_response(code: i32, result: LibwakuResponse) -> Result<()> {
if result == LibwakuResponse::Undefined && code as u32 == RET_OK {
// Some functions will only execute the callback on error
return Ok(());
}
match result {
LibwakuResponse::Success(_) => Ok(()),
LibwakuResponse::Failure(v) => Err(v),
LibwakuResponse::MissingCallback => panic!("callback is required"),
LibwakuResponse::Undefined => panic!(
"undefined ffi state: code({}) was returned but callback was not executed",
code
),
}
}
/// Used in cases where the FFI function returns a code (RET_OK, RET_ERR, etc) plus additional
/// information, i.e. LibwakuResponse
pub fn handle_response<F: WakuDecode>(code: i32, result: LibwakuResponse) -> Result<F> {
match result {
LibwakuResponse::Success(v) => WakuDecode::decode(&v.unwrap_or_default()),
LibwakuResponse::Failure(v) => Err(v),
LibwakuResponse::MissingCallback => panic!("callback is required"),
LibwakuResponse::Undefined => panic!(
"undefined ffi state: code({}) was returned but callback was not executed",
code
),
}
}

View File

@ -0,0 +1,30 @@
use crate::general::waku_decode::WakuDecode;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::hash::Hash;
use std::str::FromStr;
/// Waku message hash, hex encoded sha256 digest of the message
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Hash)]
pub struct MessageHash(String);
impl FromStr for MessageHash {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Ok(MessageHash(s.to_string()))
}
}
impl WakuDecode for MessageHash {
fn decode(input: &str) -> Result<Self, String> {
MessageHash::from_str(input)
}
}
// Implement the Display trait
impl fmt::Display for MessageHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}

View File

@ -1,94 +1,73 @@
//! Waku [general](https://rfc.vac.dev/spec/36/#general) types
// std
use std::borrow::Cow;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
pub mod contenttopic;
pub mod libwaku_response;
pub mod messagehash;
pub mod pubsubtopic;
pub mod time;
pub mod waku_decode;
// crates
use aes_gcm::{Aes256Gcm, Key};
use base64::Engine;
use secp256k1::{ecdsa::Signature, PublicKey, SecretKey};
use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
use crate::general::time::get_now_in_nanosecs;
use contenttopic::WakuContentTopic;
use serde::{Deserialize, Serialize};
use serde_aux::prelude::*;
use sscanf::{scanf, RegexRepresentation};
// internal
use crate::decrypt::{waku_decode_asymmetric, waku_decode_symmetric};
use crate::encrypt::{waku_encode_asymmetric, waku_encode_symmetric};
/// Waku message version
pub type WakuMessageVersion = usize;
/// Base58 encoded peer id
pub type PeerId = String;
/// Waku message id, hex encoded sha256 digest of the message
pub type MessageId = String;
/// Waku pubsub topic
pub type WakuPubSubTopic = String;
/// Protocol identifiers
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub enum ProtocolId {
Store,
Lightpush,
Filter,
Relay,
}
impl ProtocolId {
pub fn as_string_with_version(&self, version: &str) -> String {
format!("{self}/{version}")
}
}
impl Display for ProtocolId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let tag = match self {
ProtocolId::Store => "/vac/waku/store",
ProtocolId::Lightpush => "/vac/waku/lightpush",
ProtocolId::Filter => "/vac/waku/filter",
ProtocolId::Relay => "/vac/waku/relay",
#[allow(unreachable_patterns)]
_ => unreachable!(),
};
write!(f, "{tag}")
}
}
/// Waku response, just a `Result` with an `String` error.
pub type Result<T> = std::result::Result<T, String>;
// TODO: Properly type and deserialize payload form base64 encoded string
/// Waku message in JSON format.
/// as per the [specification](https://rfc.vac.dev/spec/36/#jsonmessage-type)
#[derive(Clone, Serialize, Deserialize, Debug)]
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct WakuMessage {
#[serde(with = "base64_serde", default = "Vec::new")]
payload: Vec<u8>,
pub payload: Vec<u8>,
/// The content topic to be set on the message
content_topic: WakuContentTopic,
// TODO: check if missing default should be 0
pub content_topic: WakuContentTopic,
/// The Waku Message version number
#[serde(default)]
version: WakuMessageVersion,
pub version: WakuMessageVersion,
/// Unix timestamp in nanoseconds
#[serde(deserialize_with = "deserialize_number_from_string")]
timestamp: usize,
pub timestamp: u64,
#[serde(with = "base64_serde", default = "Vec::new")]
meta: Vec<u8>,
pub meta: Vec<u8>,
#[serde(default)]
ephemeral: bool,
pub ephemeral: bool,
// TODO: implement RLN fields
#[serde(flatten)]
_extras: serde_json::Value,
}
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct WakuStoreRespMessage {
// #[serde(with = "base64_serde", default = "Vec::new")]
pub payload: Vec<u8>,
/// The content topic to be set on the message
// #[serde(rename = "contentTopic")]
pub content_topic: String,
// #[serde(with = "base64_serde", default = "Vec::new")]
pub meta: Vec<u8>,
/// The Waku Message version number
#[serde(default)]
pub version: WakuMessageVersion,
/// Unix timestamp in nanoseconds
pub timestamp: usize,
#[serde(default)]
pub ephemeral: bool,
pub proof: Vec<u8>,
}
impl WakuMessage {
pub fn new<PAYLOAD: AsRef<[u8]>, META: AsRef<[u8]>>(
payload: PAYLOAD,
content_topic: WakuContentTopic,
version: WakuMessageVersion,
timestamp: usize,
meta: META,
ephemeral: bool,
) -> Self {
@ -99,7 +78,7 @@ impl WakuMessage {
payload,
content_topic,
version,
timestamp,
timestamp: get_now_in_nanosecs(),
meta,
ephemeral,
_extras: Default::default(),
@ -109,413 +88,11 @@ impl WakuMessage {
pub fn payload(&self) -> &[u8] {
&self.payload
}
pub fn content_topic(&self) -> &WakuContentTopic {
&self.content_topic
}
pub fn version(&self) -> WakuMessageVersion {
self.version
}
pub fn timestamp(&self) -> usize {
self.timestamp
}
pub fn meta(&self) -> &[u8] {
&self.meta
}
pub fn ephemeral(&self) -> bool {
self.ephemeral
}
/// Optionally sign and encrypt a message using symmetric encryption
pub fn encode_symmetric(
&self,
symmetric_key: &Key<Aes256Gcm>,
signing_key: Option<&SecretKey>,
) -> Result<WakuMessage> {
waku_encode_symmetric(self, symmetric_key, signing_key)
}
/// Try decode the message with an expected symmetric key
///
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_decode_symmetricchar-messagejson-char-symmetrickey)
pub fn try_decode_symmetric(&self, symmetric_key: &Key<Aes256Gcm>) -> Result<DecodedPayload> {
waku_decode_symmetric(self, symmetric_key)
}
/// Optionally sign and encrypt a message using asymmetric encryption
pub fn encode_asymmetric(
&self,
public_key: &PublicKey,
signing_key: Option<&SecretKey>,
) -> Result<WakuMessage> {
waku_encode_asymmetric(self, public_key, signing_key)
}
/// Try decode the message with an expected asymmetric key
///
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_decode_asymmetricchar-messagejson-char-privatekey)
pub fn try_decode_asymmetric(&self, asymmetric_key: &SecretKey) -> Result<DecodedPayload> {
waku_decode_asymmetric(self, asymmetric_key)
}
}
/// A payload once decoded, used when a received Waku Message is encrypted
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DecodedPayload {
/// Public key that signed the message (optional), hex encoded with 0x prefix
#[serde(deserialize_with = "deserialize_optional_pk", default)]
public_key: Option<PublicKey>,
/// Message signature (optional), hex encoded with 0x prefix
#[serde(deserialize_with = "deserialize_optional_signature", default)]
signature: Option<Signature>,
/// Decrypted message payload base64 encoded
#[serde(with = "base64_serde")]
data: Vec<u8>,
/// Padding base64 encoded
#[serde(with = "base64_serde")]
padding: Vec<u8>,
}
impl DecodedPayload {
pub fn public_key(&self) -> Option<&PublicKey> {
self.public_key.as_ref()
}
pub fn signature(&self) -> Option<&Signature> {
self.signature.as_ref()
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn padding(&self) -> &[u8] {
&self.padding
}
}
/// The content topic of a Waku message
/// as per the [specification](https://rfc.vac.dev/spec/36/#contentfilter-type)
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LegacyContentFilter {
/// The content topic of a Waku message
content_topic: WakuContentTopic,
}
impl LegacyContentFilter {
pub fn new(content_topic: WakuContentTopic) -> Self {
Self { content_topic }
}
pub fn content_topic(&self) -> &WakuContentTopic {
&self.content_topic
}
}
/// The criteria to create subscription to a light node in JSON Format
/// as per the [specification](https://rfc.vac.dev/spec/36/#filtersubscription-type)
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LegacyFilterSubscription {
/// Array of [`ContentFilter`] being subscribed to / unsubscribed from
content_filters: Vec<ContentFilter>,
/// Optional pubsub topic
pubsub_topic: Option<WakuPubSubTopic>,
}
impl LegacyFilterSubscription {
pub fn new(content_filters: Vec<ContentFilter>, pubsub_topic: Option<WakuPubSubTopic>) -> Self {
Self {
content_filters,
pubsub_topic,
}
}
pub fn content_filters(&self) -> &[ContentFilter] {
&self.content_filters
}
pub fn pubsub_topic(&self) -> Option<&WakuPubSubTopic> {
self.pubsub_topic.as_ref()
}
}
/// The criteria to create subscription to a filter full node matching a content filter.
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ContentFilter {
/// optional if using autosharding, mandatory if using static or named sharding.
pubsub_topic: Option<WakuPubSubTopic>,
/// mandatory, at least one required, with a max of 10
content_topics: Vec<WakuContentTopic>,
}
impl ContentFilter {
pub fn new(
pubsub_topic: Option<WakuPubSubTopic>,
content_topics: Vec<WakuContentTopic>,
) -> Self {
Self {
content_topics,
pubsub_topic,
}
}
pub fn content_topics(&self) -> &[WakuContentTopic] {
&self.content_topics
}
pub fn pubsub_topic(&self) -> Option<&WakuPubSubTopic> {
self.pubsub_topic.as_ref()
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FilterSubscriptionDetail {
#[serde(rename = "peerID")]
peer_id: PeerId,
content_topics: Vec<WakuContentTopic>,
pubsub_topic: WakuPubSubTopic,
}
impl FilterSubscriptionDetail {
pub fn new(
peer_id: PeerId,
content_topics: Vec<WakuContentTopic>,
pubsub_topic: WakuPubSubTopic,
) -> Self {
Self {
peer_id,
content_topics,
pubsub_topic,
}
}
pub fn peer_id(&self) -> &PeerId {
&self.peer_id
}
pub fn content_topics(&self) -> &[WakuContentTopic] {
&self.content_topics
}
pub fn pubsub_topic(&self) -> &WakuPubSubTopic {
&self.pubsub_topic
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FilterSubscriptionResult {
subscriptions: Vec<FilterSubscriptionDetail>,
error: Option<String>,
}
impl FilterSubscriptionResult {
pub fn new(subscriptions: Vec<FilterSubscriptionDetail>, error: Option<String>) -> Self {
Self {
subscriptions,
error,
}
}
pub fn subscriptions(&self) -> &[FilterSubscriptionDetail] {
&self.subscriptions
}
pub fn error(&self) -> &Option<String> {
&self.error
}
}
/// Criteria used to retrieve historical messages
#[derive(Clone, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct StoreQuery {
/// The pubsub topic on which messages are published
pub pubsub_topic: Option<WakuPubSubTopic>,
/// Array of [`WakuContentTopic`] to query for historical messages
pub content_topics: Vec<WakuContentTopic>,
/// The inclusive lower bound on the timestamp of queried messages.
/// This field holds the Unix epoch time in nanoseconds
pub start_time: Option<usize>,
/// The inclusive upper bound on the timestamp of queried messages.
/// This field holds the Unix epoch time in nanoseconds
pub end_time: Option<usize>,
/// Paging information in [`PagingOptions`] format
pub paging_options: Option<PagingOptions>,
}
/// The response received after doing a query to a store node
#[derive(Clone, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct StoreResponse {
/// Array of retrieved historical messages in [`WakuMessage`] format
#[serde(default)]
pub messages: Vec<WakuMessage>,
/// Paging information in [`PagingOptions`] format from which to resume further historical queries
pub paging_options: Option<PagingOptions>,
}
impl StoreResponse {
pub fn messages(&self) -> &[WakuMessage] {
&self.messages
}
pub fn paging_options(&self) -> Option<&PagingOptions> {
self.paging_options.as_ref()
}
}
/// Paging information
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PagingOptions {
/// Number of messages to retrieve per page
pub page_size: usize,
/// Message Index from which to perform pagination.
/// If not included and forward is set to `true`, paging will be performed from the beginning of the list.
/// If not included and forward is set to `false`, paging will be performed from the end of the list
pub cursor: Option<MessageIndex>,
/// `true` if paging forward, `false` if paging backward
pub forward: bool,
}
/// Pagination index type
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MessageIndex {
/// Hash of the message at this [``MessageIndex`]
pub digest: String,
/// UNIX timestamp in nanoseconds at which the message at this [`MessageIndex`] was received
pub receiver_time: usize,
/// UNIX timestamp in nanoseconds at which the message is generated by its sender
pub sender_time: usize,
/// The pubsub topic of the message at this [`MessageIndex`]
pub pubsub_topic: WakuPubSubTopic,
}
/// WakuMessage encoding scheme
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Encoding {
Proto,
Rlp,
Rfc26,
Unknown(String),
}
impl Display for Encoding {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = match self {
Encoding::Proto => "proto",
Encoding::Rlp => "rlp",
Encoding::Rfc26 => "rfc26",
Encoding::Unknown(value) => value,
};
f.write_str(s)
}
}
impl FromStr for Encoding {
type Err = std::io::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"proto" => Ok(Self::Proto),
"rlp" => Ok(Self::Rlp),
"rfc26" => Ok(Self::Rfc26),
encoding => Ok(Self::Unknown(encoding.to_string())),
}
}
}
impl RegexRepresentation for Encoding {
const REGEX: &'static str = r"\w";
}
/// A waku content topic `/{application_name}/{version}/{content_topic_name}/{encdoing}`
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WakuContentTopic {
pub application_name: Cow<'static, str>,
pub version: Cow<'static, str>,
pub content_topic_name: Cow<'static, str>,
pub encoding: Encoding,
}
impl WakuContentTopic {
pub const fn new(
application_name: &'static str,
version: &'static str,
content_topic_name: &'static str,
encoding: Encoding,
) -> Self {
Self {
application_name: Cow::Borrowed(application_name),
version: Cow::Borrowed(version),
content_topic_name: Cow::Borrowed(content_topic_name),
encoding,
}
}
}
impl FromStr for WakuContentTopic {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if let Ok((application_name, version, content_topic_name, encoding)) =
scanf!(s, "/{}/{}/{}/{:/.+?/}", String, String, String, Encoding)
{
Ok(WakuContentTopic {
application_name: Cow::Owned(application_name),
version: Cow::Owned(version),
content_topic_name: Cow::Owned(content_topic_name),
encoding,
})
} else {
Err(
format!(
"Wrong pub-sub topic format. Should be `/{{application-name}}/{{version-of-the-application}}/{{content-topic-name}}/{{encoding}}`. Got: {s}"
)
)
}
}
}
impl Display for WakuContentTopic {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"/{}/{}/{}/{}",
self.application_name, self.version, self.content_topic_name, self.encoding
)
}
}
impl Serialize for WakuContentTopic {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
self.to_string().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for WakuContentTopic {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let as_string: String = String::deserialize(deserializer)?;
as_string
.parse::<WakuContentTopic>()
.map_err(D::Error::custom)
impl WakuStoreRespMessage {
pub fn payload(&self) -> &[u8] {
&self.payload
}
}
@ -544,93 +121,13 @@ mod base64_serde {
}
}
pub fn deserialize_optional_pk<'de, D>(
deserializer: D,
) -> std::result::Result<Option<PublicKey>, D::Error>
where
D: Deserializer<'de>,
{
let base64_str: Option<String> = Option::<String>::deserialize(deserializer)?;
base64_str
.map(|base64_str| {
let raw_bytes = base64::engine::general_purpose::STANDARD
.decode(base64_str)
.map_err(D::Error::custom)?;
PublicKey::from_slice(&raw_bytes).map_err(D::Error::custom)
})
.transpose()
}
pub fn deserialize_optional_signature<'de, D>(
deserializer: D,
) -> std::result::Result<Option<Signature>, D::Error>
where
D: Deserializer<'de>,
{
let hex_str: Option<String> = Option::<String>::deserialize(deserializer)?;
hex_str
.map(|hex_str| {
let raw_bytes = hex::decode(hex_str.strip_prefix("0x").unwrap_or(&hex_str))
.map_err(D::Error::custom)?;
if ![64, 65].contains(&raw_bytes.len()) {
return Err(D::Error::custom(
"Invalid signature, only 64 or 65 bytes len are supported",
));
}
Signature::from_compact(&raw_bytes[..64]).map_err(D::Error::custom)
})
.transpose()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::WakuPubSubTopic;
use secp256k1::{rand, Secp256k1};
use std::time::SystemTime;
#[test]
fn parse_waku_topic() {
let s = "/waku/2/default-waku/proto";
let _: WakuPubSubTopic = s.parse().unwrap();
}
#[test]
fn deserialize_waku_message() {
let message = "{\"payload\":\"SGkgZnJvbSDwn6aAIQ==\",\"contentTopic\":\"/toychat/2/huilong/proto\",\"timestamp\":1665580926660,\"ephemeral\":true,\"meta\":\"SGkgZnJvbSDwn6aAIQ==\"}";
let _: WakuMessage = serde_json::from_str(message).unwrap();
}
#[test]
fn encode_decode() {
let content_topic = WakuContentTopic::new("hello", "2", "world", Encoding::Proto);
let message = WakuMessage::new(
"hello",
content_topic,
1,
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis()
.try_into()
.unwrap(),
Vec::new(),
false,
);
let secp = Secp256k1::new();
let signing_key = SecretKey::new(&mut rand::thread_rng());
let encrypt_key = SecretKey::new(&mut rand::thread_rng());
let public_key = PublicKey::from_secret_key(&secp, &encrypt_key);
let encoded_message = message
.encode_asymmetric(&public_key, Some(&signing_key))
.expect("could not encode");
let decoded_message = encoded_message
.try_decode_asymmetric(&encrypt_key)
.expect("could not decode");
assert!(message.payload() != encoded_message.payload());
assert!(encoded_message.version() == 1);
assert!(message.payload() == decoded_message.data());
}
}

View File

@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PubsubTopic(String);
impl PubsubTopic {
// Constructor to create a new MyString
pub fn new(value: &str) -> Self {
PubsubTopic(value.to_string())
}
}
// to allow conversion from `PubsubTopic` to `String`
impl From<&PubsubTopic> for String {
fn from(topic: &PubsubTopic) -> Self {
topic.0.to_string()
}
}

View File

@ -0,0 +1,7 @@
use std::time::{SystemTime, UNIX_EPOCH};
pub fn get_now_in_nanosecs() -> u64 {
let now = SystemTime::now();
let since_epoch = now.duration_since(UNIX_EPOCH).expect("Time went backwards");
since_epoch.as_secs() * 1_000_000_000 + since_epoch.subsec_nanos() as u64
}

View File

@ -0,0 +1,26 @@
use crate::general::Result;
use multiaddr::Multiaddr;
// Define the WakuDecode trait
pub trait WakuDecode: Sized {
fn decode(input: &str) -> Result<Self>;
}
impl WakuDecode for String {
fn decode(input: &str) -> Result<Self> {
Ok(input.to_string())
}
}
pub fn decode<T: WakuDecode>(input: String) -> Result<T> {
T::decode(input.as_str())
}
impl WakuDecode for Vec<Multiaddr> {
fn decode(input: &str) -> Result<Self> {
input
.split(',')
.map(|s| s.trim().parse::<Multiaddr>().map_err(|err| err.to_string()))
.collect::<Result<Vec<Multiaddr>>>() // Collect results into a Vec
.map_err(|err| format!("could not parse Multiaddr: {}", err))
}
}

View File

@ -1,25 +1,23 @@
//! # Waku
//!
//! Implementation on top of [`waku-bindings`](https://rfc.vac.dev/spec/36/)
mod decrypt;
mod encrypt;
mod events;
mod general;
mod node;
mod utils;
pub mod general;
mod macros;
pub mod node;
// Re-export the LibwakuResponse type to make it accessible outside this module
pub use general::libwaku_response::LibwakuResponse;
// Required so functions inside libwaku can call RLN functions even if we
// use it within the bindings functions
#[allow(clippy::single_component_path_imports)]
#[allow(unused)]
use rln;
pub use node::{
waku_create_content_topic, waku_default_pubsub_topic, waku_discv5_update_bootnodes,
waku_dns_discovery, waku_new, Aes256Gcm, DnsInfo, GossipSubParams, Initialized, Key, Multiaddr,
Protocol, PublicKey, Running, SecretKey, WakuLogLevel, WakuNodeConfig, WakuNodeHandle,
WakuPeerData, WakuPeers, WebsocketParams,
waku_create_content_topic, waku_new, Initialized, Key, Multiaddr, PublicKey, RLNConfig,
Running, SecretKey, WakuEvent, WakuMessageEvent, WakuNodeConfig, WakuNodeHandle,
};
pub use general::{
ContentFilter, DecodedPayload, Encoding, FilterSubscriptionDetail, FilterSubscriptionResult,
LegacyContentFilter, LegacyFilterSubscription, MessageId, MessageIndex, PagingOptions, PeerId,
ProtocolId, Result, StoreQuery, StoreResponse, WakuContentTopic, WakuMessage,
WakuMessageVersion, WakuPubSubTopic,
};
pub use events::{waku_set_event_callback, Event, Signal, WakuMessageEvent};
pub use general::contenttopic::{Encoding, WakuContentTopic};
pub use general::{messagehash::MessageHash, Result, WakuMessage, WakuMessageVersion};

View File

@ -0,0 +1,73 @@
use crate::general::libwaku_response::LibwakuResponse;
use std::{slice, str};
use waku_sys::WakuCallBack;
unsafe extern "C" fn trampoline<F>(
ret_code: ::std::os::raw::c_int,
data: *const ::std::os::raw::c_char,
data_len: usize,
user_data: *mut ::std::os::raw::c_void,
) where
F: FnMut(LibwakuResponse),
{
let closure = &mut *(user_data as *mut F);
let response = if data.is_null() {
""
} else {
str::from_utf8(slice::from_raw_parts(data as *mut u8, data_len))
.expect("could not retrieve response")
};
let result = LibwakuResponse::try_from((ret_code as u32, response))
.expect("invalid response obtained from libwaku");
closure(result);
}
pub fn get_trampoline<F>(_closure: &F) -> WakuCallBack
where
F: FnMut(LibwakuResponse),
{
Some(trampoline::<F>)
}
#[macro_export]
macro_rules! handle_ffi_call {
// Case: With or without additional arguments
($waku_fn:expr, $resp_hndlr:expr, $ctx:expr $(, $($arg:expr),*)?) => {{
use $crate::macros::get_trampoline;
use std::sync::Arc;
use tokio::sync::Notify;
use libc::*;
let mut result = LibwakuResponse::default();
let notify = Arc::new(Notify::new());
let notify_clone = notify.clone();
// Callback to update the result and notify the waiter
let result_cb = |r: LibwakuResponse| {
result = r;
notify_clone.notify_one();
};
let mut closure = result_cb;
// Create trampoline and invoke the `waku_sys` function
let code = unsafe {
let cb = get_trampoline(&closure);
$waku_fn(
$ctx, // Pass the context
$($($arg),*,)? // Expand the variadic arguments if provided
cb, // Pass the callback trampoline
&mut closure as *mut _ as *mut c_void
)
};
// Wait for the callback to notify us
notify.notified().await;
// Handle the response
$resp_hndlr(code, result)
}};
}

View File

@ -1,11 +1,7 @@
//! Waku node [configuration](https://rfc.vac.dev/spec/36/#jsonconfig-type) related items
use std::fmt::{Display, Formatter};
use std::str::FromStr;
// std
// crates
use crate::WakuPubSubTopic;
use multiaddr::Multiaddr;
use secp256k1::SecretKey;
use serde::{Deserialize, Serialize};
use smart_default::SmartDefault;
@ -20,253 +16,92 @@ pub struct WakuNodeConfig {
pub host: Option<std::net::IpAddr>,
/// Libp2p TCP listening port. Default `60000`. Use `0` for **random**
#[default(Some(60000))]
pub port: Option<usize>,
/// External address to advertise to other nodes. Can be ip4, ip6 or dns4, dns6.
/// If null, the multiaddress(es) generated from the ip and port specified in the config (or default ones) will be used.
/// Default: null
pub advertise_addr: Option<Multiaddr>,
pub tcp_port: Option<usize>,
/// Secp256k1 private key in Hex format (`0x123...abc`). Default random
#[serde(with = "secret_key_serde")]
#[serde(with = "secret_key_serde", rename = "key")]
pub node_key: Option<SecretKey>,
/// Interval in seconds for pinging peers to keep the connection alive. Default `20`
#[default(Some(20))]
pub keep_alive_interval: Option<usize>,
/// Enable relay protocol. Default `true`
/// Cluster id that the node is running in
#[default(Some(0))]
pub cluster_id: Option<usize>,
/// Relay protocol
#[default(Some(true))]
pub relay: Option<bool>,
/// Enable store protocol to persist message history
pub relay_topics: Vec<String>,
#[default(vec![1])]
pub shards: Vec<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_message_size: Option<String>,
/// Store protocol
#[serde(skip_serializing_if = "Option::is_none")]
pub storenode: Option<&'static str>,
/// RLN configuration
#[serde(skip_serializing_if = "Option::is_none")]
pub rln_relay: Option<RLNConfig>,
// Discovery
#[default(Some(false))]
pub store: Option<bool>,
/// Url connection string. Accepts SQLite and PostgreSQL connection strings
#[default(Some("sqlite3://store.db".to_string()))]
pub database_url: Option<String>,
/// Max number of messages to store in the databas
#[default(Some(1000))]
pub store_retention_max_messages: Option<usize>,
/// Max number of seconds that a message will be persisted in the database, default 1 day
#[default(Some(86400))]
pub store_retention_max_seconds: Option<usize>,
pub relay_topics: Vec<WakuPubSubTopic>,
/// The minimum number of peers required on a topic to allow broadcasting a message. Default `0`
#[default(Some(0))]
pub min_peers_to_publish: Option<usize>,
/// Enable filter protocol. Default `false`
pub dns_discovery: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns_discovery_url: Option<&'static str>,
#[serde(
skip_serializing_if = "Option::is_none",
rename = "discV5BootstrapNodes"
)]
pub discv5_bootstrap_nodes: Option<Vec<String>>,
#[default(Some(false))]
#[serde(rename = "legacyFilter")]
pub filter: Option<bool>,
/// Set the log level. Default `INFO`. Allowed values "DEBUG", "INFO", "WARN", "ERROR", "DPANIC", "PANIC", "FATAL"
#[default(Some(WakuLogLevel::Info))]
pub log_level: Option<WakuLogLevel>,
/// Enable DiscoveryV5. Default `false`
pub discv5_discovery: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discv5_udp_port: Option<usize>,
#[default(Some(false))]
#[serde(rename = "discV5")]
pub discv5: Option<bool>,
/// Array of bootstrap nodes ENR.
#[serde(rename = "discV5BootstrapNodes", default)]
pub discv5_bootstrap_nodes: Vec<String>,
/// UDP port for DiscoveryV5. Default `9000`.
#[default(Some(9000))]
#[serde(rename = "discV5UDPPort")]
pub discv5_udp_port: Option<u16>,
/// Gossipsub custom configuration.
pub gossipsub_params: Option<GossipSubParams>,
/// The domain name resolving to the node's public IPv4 address.
#[serde(rename = "dns4DomainName")]
pub dns4_domain_name: Option<String>,
/// Custom websocket support parameters
#[serde(rename = "websockets")]
pub websocket_params: Option<WebsocketParams>,
pub discv5_enr_auto_update: Option<bool>,
// other settings
#[serde(skip_serializing_if = "Option::is_none")]
pub log_level: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keep_alive: Option<bool>,
}
/// RLN Relay configuration
#[derive(Clone, SmartDefault, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GossipSubParams {
/// Sets the optimal degree for a GossipSub topic mesh. For example, if D == 6,
/// each peer will want to have about six peers in their mesh for each topic they're subscribed to.
/// `d` should be set somewhere between `dlo` and `dhi`.
#[serde(rename = "d")]
pub d: Option<i32>,
/// Sets the lower bound on the number of peers we keep in a GossipSub topic mesh.
/// If we have fewer than dlo peers, we will attempt to graft some more into the mesh at
/// the next heartbeat.
#[serde(rename = "d_low")]
pub dlo: Option<i32>,
/// Sets the upper bound on the number of peers we keep in a GossipSub topic mesh.
/// If we have more than dhi peers, we will select some to prune from the mesh at the next heartbeat.
#[serde(rename = "d_high")]
pub dhi: Option<i32>,
/// `dscore` affects how peers are selected when pruning a mesh due to over subscription.
/// At least dscore of the retained peers will be high-scoring, while the remainder are
/// chosen randomly.
#[serde(rename = "d_score")]
pub dscore: Option<i32>,
/// Sets the quota for the number of outbound connections to maintain in a topic mesh.
/// When the mesh is pruned due to over subscription, we make sure that we have outbound connections
/// to at least dout of the survivor peers. This prevents sybil attackers from overwhelming
/// our mesh with incoming connections.
///
/// dout must be set below Dlo, and must not exceed D / 2.
#[serde(rename = "d_out")]
pub dout: Option<i32>,
/// Controls the size of the message cache used for gossip.
/// The message cache will remember messages for history_length heartbeats.
pub history_length: Option<i32>,
/// Controls how many cached message ids we will advertise in
/// IHAVE gossip messages. When asked for our seen message IDs, we will return
/// only those from the most recent history_gossip heartbeats. The slack between
/// history_gossip and history_length allows us to avoid advertising messages
/// that will be expired by the time they're requested.
///
/// history_gossip must be less than or equal to history_length to
/// avoid a runtime panic.
pub history_gossip: Option<i32>,
/// dlazy affects how many peers we will emit gossip to at each heartbeat.
/// We will send gossip to at least dlazy peers outside our mesh. The actual
/// number may be more, depending on gossip_factor and how many peers we're
/// connected to.
pub dlazy: Option<i32>,
/// `gossip_factor` affects how many peers we will emit gossip to at each heartbeat.
/// We will send gossip to gossip_factor * (total number of non-mesh peers), or
/// Dlazy, whichever is greater.
pub gossip_factor: Option<f64>,
/// Controls how many times we will allow a peer to request
/// the same message id through IWANT gossip before we start ignoring them. This is designed
/// to prevent peers from spamming us with requests and wasting our resources.
pub gossip_retransmission: Option<i32>,
/// Short delay before the heartbeat timer begins
/// after the router is initialized.
pub heartbeat_initial_delay_ms: Option<i32>,
/// Controls the time between heartbeats.
pub heartbeat_interval_seconds: Option<i32>,
/// Duration threshold for heartbeat processing before emitting
/// a warning; this would be indicative of an overloaded peer.
pub slow_heartbeat_warning: Option<f64>,
/// Controls how long we keep track of the fanout state. If it's been
/// fanout_ttl_seconds since we've published to a topic that we're not subscribed to,
/// we'll delete the fanout map for that topic.
pub fanout_ttl_seconds: Option<i32>,
/// Controls the number of peers to include in prune Peer eXchange.
/// When we prune a peer that's eligible for PX (has a good score, etc), we will try to
/// send them signed peer records for up to prune_peers other peers that we
/// know of.
pub prune_peers: Option<i32>,
/// Controls the backoff time for pruned peers. This is how long
/// a peer must wait before attempting to graft into our mesh again after being pruned.
/// When pruning a peer, we send them our value of PruneBackoff so they know
/// the minimum time to wait. Peers running older versions may not send a backoff time,
/// so if we receive a prune message without one, we will wait at least PruneBackoff
/// before attempting to re-graft.
pub prune_backoff_seconds: Option<i32>,
/// Controls the backoff time to use when unsuscribing
/// from a topic. A peer should not resubscribe to this topic before this
/// duration.
pub unsubscribe_backoff_seconds: Option<i32>,
/// Controls the number of active connection attempts for peers obtained through PX.
pub connectors: Option<i32>,
/// Sets the maximum number of pending connections for peers attempted through px.
pub max_pending_connections: Option<i32>,
/// Controls the timeout for connection attempts.
pub connection_timeout_seconds: Option<i32>,
/// Number of heartbeat ticks for attempting to reconnect direct peers
/// that are not currently connected.
pub direct_connect_ticks: Option<u64>,
/// Initial delay before opening connections to direct peers
pub direct_connect_initial_delay_seconds: Option<i32>,
/// Number of heartbeat ticks for attempting to improve the mesh
/// with opportunistic grafting. Every opportunistic_graft_ticks we will attempt to select some
/// high-scoring mesh peers to replace lower-scoring ones, if the median score of our mesh peers falls
/// below a threshold (see https://godoc.org/github.com/libp2p/go-libp2p-pubsub#PeerScoreThresholds).
pub opportunistic_graft_ticks: Option<u64>,
/// Number of peers to opportunistically graft.
pub opportunistic_graft_peers: Option<i32>,
/// If a GRAFT comes before graft_flood_threshold_seconds has elapsed since the last PRUNE,
/// then there is an extra score penalty applied to the peer through P7.
pub graft_flood_threshold_seconds: Option<i32>,
/// Maximum number of messages to include in an IHAVE message.
/// Also controls the maximum number of IHAVE ids we will accept and request with IWANT from a
/// peer within a heartbeat, to protect from IHAVE floods. You should adjust this value from the
/// default if your system is pushing more than 5000 messages in history_gossip heartbeats;
/// with the defaults this is 1666 messages/s.
#[serde(rename = "maxIHaveLength")]
pub max_ihave_length: Option<i32>,
/// Maximum number of IHAVE messages to accept from a peer within a heartbeat.
#[serde(rename = "maxIHaveMessages")]
pub max_ihave_messages: Option<i32>,
/// Time to wait for a message requested through IWANT following an IHAVE advertisement.
/// If the message is not received within this window, a broken promise is declared and
/// the router may apply bahavioural penalties.
#[serde(rename = "iwantFollowupTimeSeconds")]
pub iwant_followup_time_seconds: Option<i32>,
// Time until a previously seen message ID can be forgotten about.
#[serde(rename = "seenMessagesTTLSeconds")]
pub seen_messages_ttl_seconds: Option<i32>,
}
#[derive(Clone, SmartDefault, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct WebsocketParams {
/// Indicates if websockets support will be enabled
#[default(Some(false))]
pub enabled: Option<bool>,
/// Listening address for websocket connections. Default `0.0.0.0`
#[default(Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0))))]
pub host: Option<std::net::IpAddr>,
/// TCP listening port for websocket connection. Use `0` for **random**. Default `60001`, if secure websockets support is enabled, the default is `6443“`
pub port: Option<usize>,
/// Enable secure websockets support
#[default(Some(false))]
pub secure: Option<bool>,
/// Secure websocket certificate path. Mandatory if secure websockets support is enabled.
pub cert_path: Option<String>,
/// Secure websocket key path. Mandatory if secure websockets support is enabled.
pub key_path: Option<String>,
}
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
pub enum WakuLogLevel {
#[default]
Info,
Debug,
Warn,
Error,
DPanic,
Panic,
Fatal,
}
impl FromStr for WakuLogLevel {
type Err = std::io::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"info" => Ok(Self::Info),
"debug" => Ok(Self::Debug),
"warn" => Ok(Self::Warn),
"error" => Ok(Self::Error),
"dpanic" => Ok(Self::DPanic),
"panic" => Ok(Self::Panic),
"fatal" => Ok(Self::Fatal),
_ => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Unrecognized waku log level: {s}. Allowed values \"DEBUG\", \"INFO\", \"WARN\", \"ERROR\", \"DPANIC\", \"PANIC\", \"FATAL\""),
)),
}
}
}
impl Display for WakuLogLevel {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let tag = match self {
WakuLogLevel::Info => "INFO",
WakuLogLevel::Debug => "DEBUG",
WakuLogLevel::Warn => "WARN",
WakuLogLevel::Error => "ERROR",
WakuLogLevel::DPanic => "DPANIC",
WakuLogLevel::Panic => "PANIC",
WakuLogLevel::Fatal => "FATAL",
};
write!(f, "{tag}")
}
#[serde(rename_all = "kebab-case")]
pub struct RLNConfig {
/// Indicates if RLN support will be enabled.
pub enabled: bool,
/// Index of the onchain commitment to use
#[serde(skip_serializing_if = "Option::is_none", rename = "membership-index")]
pub membership_index: Option<usize>,
/// On-chain dynamic group management
#[serde(skip_serializing_if = "Option::is_none")]
pub dynamic: Option<bool>,
/// Path to the RLN merkle tree sled db (https://github.com/spacejam/sled)
#[serde(skip_serializing_if = "Option::is_none")]
pub tree_path: Option<String>,
/// Message rate in bytes/sec after which verification of proofs should happen
#[serde(skip_serializing_if = "Option::is_none")]
pub bandwidth_threshold: Option<usize>,
/// Path for persisting rln-relay credential
#[serde(skip_serializing_if = "Option::is_none")]
pub cred_path: Option<String>,
/// HTTP address of an Ethereum testnet client e.g., http://localhost:8540/
#[serde(skip_serializing_if = "Option::is_none")]
pub eth_client_address: Option<String>,
/// Address of membership contract on an Ethereum testnet
#[serde(skip_serializing_if = "Option::is_none")]
pub eth_contract_address: Option<String>,
/// Password for encrypting RLN credentials
#[serde(skip_serializing_if = "Option::is_none")]
pub cred_password: Option<String>,
/// Set a user message limit for the rln membership registration
#[serde(skip_serializing_if = "Option::is_none")]
pub user_message_limit: Option<u64>,
/// Epoch size in seconds used to rate limit RLN memberships
#[serde(skip_serializing_if = "Option::is_none")]
pub epoch_sec: Option<u64>,
}
mod secret_key_serde {

View File

@ -0,0 +1,63 @@
use std::ffi::c_void;
use std::ptr::null_mut;
use std::sync::{Arc, Mutex};
use crate::general::libwaku_response::LibwakuResponse;
use crate::macros::get_trampoline;
type LibwakuResponseClosure = dyn FnMut(LibwakuResponse) + Send + Sync;
pub struct WakuNodeContext {
obj_ptr: *mut c_void,
msg_observer: Arc<Mutex<Box<LibwakuResponseClosure>>>,
}
impl WakuNodeContext {
pub fn new(obj_ptr: *mut c_void) -> Self {
let me = Self {
obj_ptr,
msg_observer: Arc::new(Mutex::new(Box::new(|_| {}))),
};
// By default we set a callback that will panic if the user didn't specify a valid callback.
// And by valid callback we mean a callback that can properly handle the waku events.
me.waku_set_event_callback(WakuNodeContext::panic_callback)
.expect("correctly set default callback");
me
}
// default callback that does nothing. A valid callback should be set
fn panic_callback(_response: LibwakuResponse) {
panic!("callback not set. Please use waku_set_event_callback to set a valid callback")
}
pub fn get_ptr(&self) -> *mut c_void {
self.obj_ptr
}
pub fn reset_ptr(mut self) {
self.obj_ptr = null_mut();
}
/// Register callback to act as event handler and receive application events,
/// which are used to react to asynchronous events in Waku
pub fn waku_set_event_callback<F: FnMut(LibwakuResponse) + 'static + Sync + Send>(
&self,
closure: F,
) -> Result<(), String> {
if let Ok(mut boxed_closure) = self.msg_observer.lock() {
*boxed_closure = Box::new(closure);
unsafe {
let cb = get_trampoline(&(*boxed_closure));
waku_sys::waku_set_event_callback(
self.obj_ptr,
cb,
&mut (*boxed_closure) as *mut _ as *mut c_void,
)
};
Ok(())
} else {
Err("Failed to acquire lock in waku_set_event_callback!".to_string())
}
}
}

View File

@ -1,108 +0,0 @@
// std
use std::ffi::CString;
use std::time::Duration;
// crates
use enr::Enr;
use libc::*;
use multiaddr::Multiaddr;
use serde::Deserialize;
use url::{Host, Url};
// internal
use crate::utils::{get_trampoline, handle_json_response, handle_no_response};
use crate::{PeerId, Result};
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DnsInfo {
#[serde(alias = "peerID")]
pub peer_id: PeerId,
#[serde(default, alias = "multiaddrs")]
pub addresses: Vec<Multiaddr>,
pub enr: Option<Enr<enr::secp256k1::SecretKey>>,
}
/// RetrieveNodes returns a list of multiaddress given a url to a DNS discoverable ENR tree.
/// The nameserver can optionally be specified to resolve the enrtree url. Otherwise uses the default system dns.
pub fn waku_dns_discovery(
url: &Url,
server: Option<&Host>,
timeout: Option<Duration>,
) -> Result<Vec<DnsInfo>> {
let url = CString::new(url.to_string())
.expect("CString should build properly from a valid Url")
.into_raw();
let server = CString::new(server.map(|host| host.to_string()).unwrap_or_default())
.expect("CString should build properly from a String nameserver")
.into_raw();
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_dns_discovery(
url,
server,
timeout
.map(|timeout| {
timeout
.as_millis()
.try_into()
.expect("Duration as milliseconds should fit in a i32")
})
.unwrap_or(0),
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(url));
drop(CString::from_raw(server));
out
};
handle_json_response(code, &result)
}
/// Update the bootnodes used by DiscoveryV5 by passing a list of ENRs
pub fn waku_discv5_update_bootnodes(bootnodes: Vec<String>) -> Result<()> {
let bootnodes_ptr = CString::new(
serde_json::to_string(&bootnodes)
.expect("Serialization from properly built bootnode array should never fail"),
)
.expect("CString should build properly from the string vector")
.into_raw();
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_discv5_update_bootnodes(
bootnodes_ptr,
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(bootnodes_ptr));
out
};
handle_no_response(code, &error)
}
#[cfg(test)]
mod test {
use url::Url;
#[test]
fn test_dns_discovery() {
let enrtree: Url =
"enrtree://AO47IDOLBKH72HIZZOXQP6NMRESAN7CHYWIBNXDXWRJRZWLODKII6@test.wakuv2.nodes.status.im".parse().unwrap();
let result = super::waku_dns_discovery(&enrtree, None, None);
assert!(result.is_ok());
assert!(!result.as_ref().unwrap().is_empty());
println!("{result:?}");
}
}

View File

@ -0,0 +1,105 @@
//! Waku message [event](https://rfc.vac.dev/spec/36/#events) related items
//!
//! Asynchronous events require a callback to be registered.
//! An example of an asynchronous event that might be emitted is receiving a message.
//! When an event is emitted, this callback will be triggered receiving an [`WakuEvent`]
// crates
use serde::{Deserialize, Serialize};
// internal
use crate::general::WakuMessage;
use std::str;
use crate::MessageHash;
/// Waku event
/// For now just WakuMessage is supported
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "eventType", rename_all = "camelCase")]
pub enum WakuEvent {
#[serde(rename = "message")]
WakuMessage(WakuMessageEvent),
#[serde(rename = "relay_topic_health_change")]
RelayTopicHealthChange(TopicHealthEvent),
#[serde(rename = "connection_change")]
ConnectionChange(ConnectionChangeEvent),
Unrecognized(serde_json::Value),
}
/// Type of `event` field for a `message` event
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct WakuMessageEvent {
/// The pubsub topic on which the message was received
pub pubsub_topic: String,
/// The message hash
pub message_hash: MessageHash,
/// The message in [`WakuMessage`] format
pub waku_message: WakuMessage,
}
/// Type of `event` field for a `topic health` event
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TopicHealthEvent {
/// The pubsub topic on which the message was received
pub pubsub_topic: String,
/// The message hash
pub topic_health: String,
}
/// Type of `event` field for a `connection change` event
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ConnectionChangeEvent {
/// The pubsub topic on which the message was received
pub peer_id: String,
/// The message hash
pub peer_event: String,
}
#[cfg(test)]
mod tests {
use crate::WakuEvent;
use crate::WakuEvent::{ConnectionChange, RelayTopicHealthChange};
#[test]
fn deserialize_message_event() {
let s = "{\"eventType\":\"message\",\"messageHash\":\"0xd40aa51bbb4867fe40329a255575cfc9ef4000358cc7321b2668b008cba94b30\",\"pubsubTopic\":\"/waku/2/default-waku/proto\",\"wakuMessage\":{\"payload\":\"SGkgZnJvbSDwn6aAIQ==\",\"contentTopic\":\"/toychat/2/huilong/proto\",\"timestamp\":1665580926660}}";
let evt: WakuEvent = serde_json::from_str(s).unwrap();
assert!(matches!(evt, WakuEvent::WakuMessage(_)));
}
#[test]
fn deserialize_topic_health_change_event() {
let s = "{\"eventType\":\"relay_topic_health_change\", \"pubsubTopic\":\"/waku/2/rs/16/1\",\"topicHealth\":\"MinimallyHealthy\"}";
let evt: WakuEvent = serde_json::from_str(s).unwrap();
match evt {
RelayTopicHealthChange(topic_health_event) => {
assert_eq!(topic_health_event.pubsub_topic, "/waku/2/rs/16/1");
assert_eq!(topic_health_event.topic_health, "MinimallyHealthy");
}
_ => panic!("Expected RelayTopicHealthChange event, but got {:?}", evt),
}
}
#[test]
fn deserialize_connection_change_event() {
let s = "{\"eventType\":\"connection_change\", \"peerId\":\"16Uiu2HAmAR24Mbb6VuzoyUiGx42UenDkshENVDj4qnmmbabLvo31\",\"peerEvent\":\"Joined\"}";
let evt: WakuEvent = serde_json::from_str(s).unwrap();
match evt {
ConnectionChange(conn_change_event) => {
assert_eq!(
conn_change_event.peer_id,
"16Uiu2HAmAR24Mbb6VuzoyUiGx42UenDkshENVDj4qnmmbabLvo31"
);
assert_eq!(conn_change_event.peer_event, "Joined");
}
_ => panic!("Expected RelayTopicHealthChange event, but got {:?}", evt),
}
}
}

View File

@ -1,182 +1,59 @@
//! Waku [filter](https://rfc.vac.dev/spec/36/#waku-filter) protocol related methods
//! Waku filter protocol related methods
// std
use std::ffi::CString;
use std::time::Duration;
// crates
use libc::*;
// internal
use crate::general::contenttopic::WakuContentTopic;
use crate::general::libwaku_response::{handle_no_response, LibwakuResponse};
use crate::general::pubsubtopic::PubsubTopic;
use crate::general::Result;
use crate::general::{ContentFilter, FilterSubscriptionResult, PeerId};
use crate::utils::{get_trampoline, handle_json_response, handle_no_response};
use crate::handle_ffi_call;
use crate::node::context::WakuNodeContext;
/// Creates a subscription in a lightnode for messages that matches a content filter and optionally a [`WakuPubSubTopic`](`crate::general::WakuPubSubTopic`)
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_filter_subscribechar-filterjson-char-peerid-int-timeoutms)
pub fn waku_filter_subscribe(
content_filter: &ContentFilter,
peer_id: Option<PeerId>,
timeout: Option<Duration>,
) -> Result<FilterSubscriptionResult> {
let content_filter_ptr = CString::new(
serde_json::to_string(content_filter)
.expect("ContentFilter should always succeed to serialize"),
)
.expect("ContentFilter should always be able to be serialized")
.into_raw();
let peer_id_ptr = match peer_id {
None => CString::new(""),
Some(t) => CString::new(t),
}
.expect("CString should build properly from peer id")
.into_raw();
let mut response: String = Default::default();
let response_cb = |v: &str| response = v.to_string();
let code = unsafe {
let mut closure = response_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_filter_subscribe(
content_filter_ptr,
peer_id_ptr,
timeout
.map(|timeout| {
timeout
.as_millis()
.try_into()
.expect("Duration as milliseconds should fit in a i32")
})
.unwrap_or(0),
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(content_filter_ptr));
drop(CString::from_raw(peer_id_ptr));
out
};
handle_json_response(code, &response)
}
/// Used to know if a service node has an active subscription for this client
/// peerID should contain the ID of a peer we are subscribed to, supporting the filter protocol
pub fn waku_filter_ping(peer_id: PeerId, timeout: Option<Duration>) -> Result<()> {
let peer_id_ptr = CString::new(peer_id)
.expect("PeerId should always be able to be serialized")
.into_raw();
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_filter_ping(
peer_id_ptr,
timeout
.map(|timeout| {
timeout
.as_millis()
.try_into()
.expect("Duration as milliseconds should fit in a i32")
})
.unwrap_or(0),
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(peer_id_ptr));
out
};
handle_no_response(code, &error)
}
/// Sends a requests to a service node to stop pushing messages matching this filter to this client.
/// It might be used to modify an existing subscription by providing a subset of the original filter
/// criteria
pub fn waku_filter_unsubscribe(
content_filter: &ContentFilter,
peer_id: PeerId,
timeout: Option<Duration>,
pub async fn waku_filter_subscribe(
ctx: &WakuNodeContext,
pubsub_topic: &PubsubTopic,
content_topics: Vec<WakuContentTopic>,
) -> Result<()> {
let content_filter_ptr = CString::new(
serde_json::to_string(content_filter)
.expect("ContentFilter should always succeed to serialize"),
let pubsub_topic = CString::new(String::from(pubsub_topic))
.expect("CString should build properly from pubsub topic");
let content_topics = WakuContentTopic::join_content_topics(content_topics);
let content_topics =
CString::new(content_topics).expect("CString should build properly from content topic");
handle_ffi_call!(
waku_sys::waku_filter_subscribe,
handle_no_response,
ctx.get_ptr(),
pubsub_topic.as_ptr(),
content_topics.as_ptr()
)
.expect("CString should build properly from the serialized filter subscription")
.into_raw();
let peer_id_ptr = CString::new(peer_id)
.expect("PeerId should always be able to be serialized")
.into_raw();
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_filter_unsubscribe(
content_filter_ptr,
peer_id_ptr,
timeout
.map(|timeout| {
timeout
.as_millis()
.try_into()
.expect("Duration as milliseconds should fit in a i32")
})
.unwrap_or(0),
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(content_filter_ptr));
drop(CString::from_raw(peer_id_ptr));
out
};
handle_no_response(code, &error)
}
/// Sends a requests to a service node (or all service nodes) to stop pushing messages
/// peerID should contain the ID of a peer this client is subscribed to, or can be None
/// to stop all active subscriptions
pub fn waku_filter_unsubscribe_all(
peer_id: Option<PeerId>,
timeout: Option<Duration>,
pub async fn waku_filter_unsubscribe(
ctx: &WakuNodeContext,
pubsub_topic: &PubsubTopic,
content_topics: Vec<WakuContentTopic>, // comma-separated list of content topics
) -> Result<()> {
let peer_id_ptr = match peer_id {
None => CString::new(""),
Some(t) => CString::new(t),
}
.expect("CString should build properly from peer id")
.into_raw();
let pubsub_topic = CString::new(String::from(pubsub_topic))
.expect("CString should build properly from pubsub topic");
let content_topics = WakuContentTopic::join_content_topics(content_topics);
let content_topics =
CString::new(content_topics).expect("CString should build properly from content topic");
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_filter_unsubscribe_all(
peer_id_ptr,
timeout
.map(|timeout| {
timeout
.as_millis()
.try_into()
.expect("Duration as milliseconds should fit in a i32")
})
.unwrap_or(0),
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(peer_id_ptr));
out
};
handle_no_response(code, &error)
handle_ffi_call!(
waku_sys::waku_filter_unsubscribe,
handle_no_response,
ctx.get_ptr(),
pubsub_topic.as_ptr(),
content_topics.as_ptr()
)
}
pub async fn waku_filter_unsubscribe_all(ctx: &WakuNodeContext) -> Result<()> {
handle_ffi_call!(
waku_sys::waku_filter_unsubscribe_all,
handle_no_response,
ctx.get_ptr()
)
}

View File

@ -1,89 +0,0 @@
//! Waku [filter](https://rfc.vac.dev/spec/36/#waku-filter) protocol related methods
// std
use std::ffi::CString;
use std::time::Duration;
// crates
use libc::*;
// internal
use crate::general::Result;
use crate::general::{LegacyFilterSubscription, PeerId};
use crate::utils::{get_trampoline, handle_no_response};
/// Creates a subscription in a lightnode for messages that matches a content filter and optionally a [`WakuPubSubTopic`](`crate::general::WakuPubSubTopic`)
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_legacy_filter_subscribechar-filterjson-char-peerid-int-timeoutms)
pub fn waku_legacy_filter_subscribe(
filter_subscription: &LegacyFilterSubscription,
peer_id: PeerId,
timeout: Duration,
) -> Result<()> {
let filter_subscription_ptr = CString::new(
serde_json::to_string(filter_subscription)
.expect("FilterSubscription should always succeed to serialize"),
)
.expect("FilterSubscription should always be able to be serialized")
.into_raw();
let peer_id_ptr = CString::new(peer_id)
.expect("PeerId should always be able to be serialized")
.into_raw();
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_legacy_filter_subscribe(
filter_subscription_ptr,
peer_id_ptr,
timeout
.as_millis()
.try_into()
.expect("Duration as milliseconds should fit in a i32"),
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(filter_subscription_ptr));
drop(CString::from_raw(peer_id_ptr));
out
};
handle_no_response(code, &error)
}
/// Removes subscriptions in a light node matching a content filter and, optionally, a [`WakuPubSubTopic`](`crate::general::WakuPubSubTopic`)
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_filter_unsubscribechar-filterjson-int-timeoutms)
pub fn waku_legacy_filter_unsubscribe(
filter_subscription: &LegacyFilterSubscription,
timeout: Duration,
) -> Result<()> {
let filter_subscription_ptr = CString::new(
serde_json::to_string(filter_subscription)
.expect("FilterSubscription should always succeed to serialize"),
)
.expect("CString should build properly from the serialized filter subscription")
.into_raw();
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_legacy_filter_unsubscribe(
filter_subscription_ptr,
timeout
.as_millis()
.try_into()
.expect("Duration as milliseconds should fit in a i32"),
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(filter_subscription_ptr));
out
};
handle_no_response(code, &error)
}

View File

@ -1,66 +1,34 @@
//! Waku [lightpush](https://rfc.vac.dev/spec/36/#waku-lightpush) protocol related methods
//! Waku lightpush protocol related methods
// std
use std::ffi::CString;
use std::time::Duration;
// crates
use libc::*;
// internal
use crate::general::{MessageId, PeerId, Result, WakuMessage, WakuPubSubTopic};
use crate::node::waku_default_pubsub_topic;
use crate::utils::{get_trampoline, handle_response};
use crate::general::libwaku_response::{handle_response, LibwakuResponse};
use crate::general::{messagehash::MessageHash, Result, WakuMessage};
use crate::handle_ffi_call;
use crate::node::context::WakuNodeContext;
/// Publish a message using Waku Lightpush
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_lightpush_publishchar-messagejson-char-topic-char-peerid-int-timeoutms)
pub fn waku_lightpush_publish(
use crate::general::pubsubtopic::PubsubTopic;
pub async fn waku_lightpush_publish_message(
ctx: &WakuNodeContext,
message: &WakuMessage,
pubsub_topic: Option<WakuPubSubTopic>,
peer_id: PeerId,
timeout: Option<Duration>,
) -> Result<MessageId> {
let pubsub_topic = pubsub_topic
.unwrap_or_else(waku_default_pubsub_topic)
.to_string();
let message_ptr = CString::new(
pubsub_topic: &PubsubTopic,
) -> Result<MessageHash> {
let message = CString::new(
serde_json::to_string(&message)
.expect("WakuMessages should always be able to success serializing"),
)
.expect("CString should build properly from the serialized waku message")
.into_raw();
let topic_ptr = CString::new(pubsub_topic)
.expect("CString should build properly from pubsub topic")
.into_raw();
let peer_id_ptr = CString::new(peer_id)
.expect("CString should build properly from peer id")
.into_raw();
.expect("CString should build properly from the serialized waku message");
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_lightpush_publish(
message_ptr,
topic_ptr,
peer_id_ptr,
timeout
.map(|timeout| {
timeout
.as_millis()
.try_into()
.expect("Duration as milliseconds should fit in a i32")
})
.unwrap_or(0),
cb,
&mut closure as *mut _ as *mut c_void,
);
let pubsub_topic = CString::new(String::from(pubsub_topic))
.expect("CString should build properly from pubsub topic");
drop(CString::from_raw(message_ptr));
drop(CString::from_raw(topic_ptr));
drop(CString::from_raw(peer_id_ptr));
out
};
handle_response(code, &result)
handle_ffi_call!(
waku_sys::waku_lightpush_publish,
handle_response,
ctx.get_ptr(),
pubsub_topic.as_ptr(),
message.as_ptr()
)
}

View File

@ -1,123 +1,120 @@
//! Node lifcycle [mangement](https://rfc.vac.dev/spec/36/#node-management) related methods
// std
use multiaddr::Multiaddr;
use std::ffi::CString;
// crates
use libc::*;
use libc::c_void;
use multiaddr::Multiaddr;
use std::sync::Arc;
use tokio::sync::Notify;
// internal
use super::config::WakuNodeConfig;
use crate::general::{PeerId, Result};
use crate::utils::{get_trampoline, handle_json_response, handle_no_response, handle_response};
use crate::general::libwaku_response::{handle_no_response, handle_response, LibwakuResponse};
use crate::general::Result;
use crate::handle_ffi_call;
use crate::macros::get_trampoline;
use crate::node::context::WakuNodeContext;
/// Instantiates a Waku node
/// as per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_newchar-jsonconfig)
pub fn waku_new(config: Option<WakuNodeConfig>) -> Result<()> {
pub async fn waku_new(config: Option<WakuNodeConfig>) -> Result<WakuNodeContext> {
let config = config.unwrap_or_default();
let config_ptr = CString::new(
let config = CString::new(
serde_json::to_string(&config)
.expect("Serialization from properly built NodeConfig should never fail"),
)
.expect("CString should build properly from the config")
.into_raw();
.expect("CString should build properly from the config");
let config_ptr = config.as_ptr();
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let notify = Arc::new(Notify::new());
let notify_clone = notify.clone();
let mut result = LibwakuResponse::default();
let result_cb = |r: LibwakuResponse| {
result = r;
notify_clone.notify_one(); // Notify that the value has been updated
};
let mut closure = result_cb;
let obj_ptr = unsafe {
let cb = get_trampoline(&closure);
let out = waku_sys::waku_new(config_ptr, cb, &mut closure as *mut _ as *mut c_void);
drop(CString::from_raw(config_ptr));
out
waku_sys::waku_new(config_ptr, cb, &mut closure as *mut _ as *mut c_void)
};
handle_no_response(code, &error)
notify.notified().await; // Wait until a result is received
match result {
LibwakuResponse::MissingCallback => panic!("callback is required"),
LibwakuResponse::Failure(v) => Err(v),
_ => Ok(WakuNodeContext::new(obj_ptr)),
}
}
pub async fn waku_destroy(ctx: &WakuNodeContext) -> Result<()> {
handle_ffi_call!(waku_sys::waku_destroy, handle_no_response, ctx.get_ptr())
}
/// Start a Waku node mounting all the protocols that were enabled during the Waku node instantiation.
/// as per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_start)
pub fn waku_start() -> Result<()> {
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
waku_sys::waku_start(cb, &mut closure as *mut _ as *mut c_void)
};
handle_no_response(code, &error)
pub async fn waku_start(ctx: &WakuNodeContext) -> Result<()> {
handle_ffi_call!(waku_sys::waku_start, handle_no_response, ctx.get_ptr())
}
/// Stops a Waku node
/// as per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_stop)
pub fn waku_stop() -> Result<()> {
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
waku_sys::waku_stop(cb, &mut closure as *mut _ as *mut c_void)
};
handle_no_response(code, &error)
pub async fn waku_stop(ctx: &WakuNodeContext) -> Result<()> {
handle_ffi_call!(waku_sys::waku_stop, handle_no_response, ctx.get_ptr())
}
/// If the execution is successful, the result is the peer ID as a string (base58 encoded)
/// as per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_stop)
pub fn waku_peer_id() -> Result<PeerId> {
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
waku_sys::waku_peerid(cb, &mut closure as *mut _ as *mut c_void)
};
handle_response(code, &result)
/// nwaku version
pub async fn waku_version(ctx: &WakuNodeContext) -> Result<String> {
handle_ffi_call!(waku_sys::waku_version, handle_response, ctx.get_ptr())
}
/// Get the multiaddresses the Waku node is listening to
/// as per [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_listen_addresses)
pub fn waku_listen_addresses() -> Result<Vec<Multiaddr>> {
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
waku_sys::waku_listen_addresses(cb, &mut closure as *mut _ as *mut c_void)
};
handle_json_response(code, &result)
pub async fn waku_listen_addresses(ctx: &WakuNodeContext) -> Result<Vec<Multiaddr>> {
handle_ffi_call!(
waku_sys::waku_listen_addresses,
handle_response,
ctx.get_ptr()
)
}
#[cfg(test)]
mod test {
use super::waku_new;
use crate::node::management::{waku_listen_addresses, waku_peer_id, waku_start, waku_stop};
use crate::node::peers::waku_peer_count;
use crate::node::management::{
waku_destroy, waku_listen_addresses, waku_start, waku_stop, waku_version,
};
use serial_test::serial;
#[test]
#[tokio::test]
#[serial]
fn waku_flow() {
waku_new(None).unwrap();
waku_start().unwrap();
// test peer id call, since we cannot start different instances of the node
let id = waku_peer_id().unwrap();
dbg!(&id);
assert!(!id.is_empty());
async fn waku_flow() {
let node = waku_new(None).await.unwrap();
let peer_cnt = waku_peer_count().unwrap();
dbg!(peer_cnt);
waku_start(&node).await.unwrap();
// test addresses, since we cannot start different instances of the node
let addresses = waku_listen_addresses().unwrap();
// test addresses
let addresses = waku_listen_addresses(&node).await.unwrap();
dbg!(&addresses);
assert!(!addresses.is_empty());
waku_stop().unwrap();
waku_stop(&node).await.unwrap();
waku_destroy(&node).await.unwrap();
}
#[tokio::test]
#[serial]
async fn nwaku_version() {
let node = waku_new(None).await.unwrap();
let version = waku_version(&node)
.await
.expect("should return the version");
print!("Current version: {}", version);
assert!(!version.is_empty());
waku_destroy(&node).await.unwrap();
}
}

View File

@ -1,9 +1,9 @@
//! Waku node implementation
mod config;
mod discovery;
mod context;
mod events;
mod filter;
mod legacyfilter;
mod lightpush;
mod management;
mod peers;
@ -11,102 +11,97 @@ mod relay;
mod store;
// std
pub use aes_gcm::{Aes256Gcm, Key};
pub use aes_gcm::Key;
pub use multiaddr::Multiaddr;
pub use secp256k1::{PublicKey, SecretKey};
use std::marker::PhantomData;
use std::sync::Mutex;
use std::time::Duration;
// crates
use store::{StoreQueryRequest, StoreWakuMessageResponse};
// internal
use crate::general::contenttopic::{Encoding, WakuContentTopic};
use crate::general::libwaku_response::LibwakuResponse;
pub use crate::general::pubsubtopic::PubsubTopic;
use crate::general::{messagehash::MessageHash, Result, WakuMessage};
use crate::general::{
ContentFilter, FilterSubscriptionResult, LegacyFilterSubscription, MessageId, PeerId,
ProtocolId, Result, StoreQuery, StoreResponse, WakuMessage, WakuPubSubTopic,
};
use crate::node::context::WakuNodeContext;
pub use config::RLNConfig;
pub use config::WakuNodeConfig;
pub use events::{WakuEvent, WakuMessageEvent};
pub use relay::waku_create_content_topic;
pub use config::{GossipSubParams, WakuLogLevel, WakuNodeConfig, WebsocketParams};
pub use discovery::{waku_discv5_update_bootnodes, waku_dns_discovery, DnsInfo};
pub use peers::{Protocol, WakuPeerData, WakuPeers};
pub use relay::{waku_create_content_topic, waku_default_pubsub_topic};
pub use store::{waku_local_store_query, waku_store_query};
/// Shared flag to check if a waku node is already running in the current process
static WAKU_NODE_INITIALIZED: Mutex<bool> = Mutex::new(false);
/// Marker trait to disallow undesired waku node states in the handle
pub trait WakuNodeState {}
/// Waku node initialized state
// Define state marker types
pub struct Initialized;
/// Waku node running state
pub struct Running;
impl WakuNodeState for Initialized {}
impl WakuNodeState for Running {}
/// Handle to the underliying waku node
/// Safe to sendt to/through threads.
/// Only a waku node can be running at a time.
/// Referenes (`&`) to the handle can call queries and perform operations in a thread safe way.
/// Only an owned version of the handle can `start` or `stop` the node.
pub struct WakuNodeHandle<State: WakuNodeState>(PhantomData<State>);
/// We do not have any inner state, so the handle should be safe to be send among threads.
unsafe impl<State: WakuNodeState> Send for WakuNodeHandle<State> {}
/// References to the handle are safe to share, as they do not mutate the handle itself and
/// operations are performed by the bindings backend, which is supposed to be thread safe.
unsafe impl<State: WakuNodeState> Sync for WakuNodeHandle<State> {}
impl<State: WakuNodeState> WakuNodeHandle<State> {
/// If the execution is successful, the result is the peer ID as a string (base58 encoded)
/// as per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_stop)
pub fn peer_id(&self) -> Result<PeerId> {
management::waku_peer_id()
}
/// Get the multiaddresses the Waku node is listening to
/// as per [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_listen_addresses)
pub fn listen_addresses(&self) -> Result<Vec<Multiaddr>> {
management::waku_listen_addresses()
}
/// Add a node multiaddress and protocol to the waku nodes peerstore.
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_add_peerchar-address-char-protocolid)
pub fn add_peer(&self, address: &Multiaddr, protocol_id: ProtocolId) -> Result<PeerId> {
peers::waku_add_peers(address, protocol_id)
}
pub struct WakuNodeHandle<State> {
ctx: WakuNodeContext,
_state: PhantomData<State>,
}
fn stop_node() -> Result<()> {
let mut node_initialized = WAKU_NODE_INITIALIZED
.lock()
.expect("Access to the mutex at some point");
*node_initialized = false;
management::waku_stop().map(|_| ())
/// Spawn a new Waku node with the given configuration (default configuration if `None` provided)
/// as per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_newchar-jsonconfig)
pub async fn waku_new(config: Option<WakuNodeConfig>) -> Result<WakuNodeHandle<Initialized>> {
Ok(WakuNodeHandle {
ctx: management::waku_new(config).await?,
_state: PhantomData,
})
}
impl<State> WakuNodeHandle<State> {
/// Get the nwaku version
pub async fn version(&self) -> Result<String> {
management::waku_version(&self.ctx).await
}
pub async fn waku_destroy(self) -> Result<()> {
let res = management::waku_destroy(&self.ctx).await;
self.ctx.reset_ptr();
res
}
/// Subscribe to WakuRelay to receive messages matching a content filter.
pub async fn relay_subscribe(&self, pubsub_topic: &PubsubTopic) -> Result<()> {
relay::waku_relay_subscribe(&self.ctx, pubsub_topic).await
}
}
impl WakuNodeHandle<Initialized> {
/// Start a Waku node mounting all the protocols that were enabled during the Waku node instantiation.
/// as per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_start)
pub fn start(self) -> Result<WakuNodeHandle<Running>> {
management::waku_start().map(|_| WakuNodeHandle(Default::default()))
pub async fn start(self) -> Result<WakuNodeHandle<Running>> {
management::waku_start(&self.ctx)
.await
.map(|_| WakuNodeHandle {
ctx: self.ctx,
_state: PhantomData,
})
}
/// Stops a Waku node
/// as per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_stop)
pub fn stop(self) -> Result<()> {
stop_node()
pub fn set_event_callback<F: FnMut(LibwakuResponse) + 'static + Sync + Send>(
&self,
closure: F,
) -> Result<()> {
self.ctx.waku_set_event_callback(closure)
}
}
impl WakuNodeHandle<Running> {
/// Stops a Waku node
/// as per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_stop)
pub fn stop(self) -> Result<()> {
stop_node()
pub async fn stop(self) -> Result<WakuNodeHandle<Initialized>> {
management::waku_stop(&self.ctx)
.await
.map(|_| WakuNodeHandle {
ctx: self.ctx,
_state: PhantomData,
})
}
/// Get the multiaddresses the Waku node is listening to
/// as per [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_listen_addresses)
pub async fn listen_addresses(&self) -> Result<Vec<Multiaddr>> {
management::waku_listen_addresses(&self.ctx).await
}
/// Dial peer using a multiaddress
@ -114,198 +109,105 @@ impl WakuNodeHandle<Running> {
/// If the function execution takes longer than `timeout` value, the execution will be canceled and an error returned.
/// Use 0 for no timeout
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_connect_peerchar-address-int-timeoutms)
pub fn connect_peer_with_address(
pub async fn connect(&self, address: &Multiaddr, timeout: Option<Duration>) -> Result<()> {
peers::waku_connect(&self.ctx, address, timeout).await
}
pub async fn relay_publish_txt(
&self,
address: &Multiaddr,
pubsub_topic: &PubsubTopic,
msg_txt: &String,
content_topic_name: &'static str,
timeout: Option<Duration>,
) -> Result<()> {
peers::waku_connect_peer_with_address(address, timeout)
}
/// Dial peer using a peer id
/// If `timeout` as milliseconds doesn't fit into a `i32` it is clamped to [`i32::MAX`]
/// The peer must be already known.
/// It must have been added before with [`WakuNodeHandle::add_peer`] or previously dialed with [`WakuNodeHandle::connect_peer_with_address`]
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_connect_peeridchar-peerid-int-timeoutms)
pub fn connect_peer_with_id(&self, peer_id: &PeerId, timeout: Option<Duration>) -> Result<()> {
peers::waku_connect_peer_with_id(peer_id, timeout)
}
/// Disconnect a peer using its peer id
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_disconnect_peerchar-peerid)
pub fn disconnect_peer_with_id(&self, peer_id: &PeerId) -> Result<()> {
peers::waku_disconnect_peer_with_id(peer_id)
}
/// Get number of connected peers
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_peer_count)
pub fn peer_count(&self) -> Result<usize> {
peers::waku_peer_count()
}
/// Retrieve the list of peers known by the Waku node
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_peers)
pub fn peers(&self) -> Result<WakuPeers> {
peers::waku_peers()
) -> Result<MessageHash> {
let content_topic = WakuContentTopic::new("waku", "2", content_topic_name, Encoding::Proto);
let message = WakuMessage::new(msg_txt, content_topic, 0, Vec::new(), false);
relay::waku_relay_publish_message(&self.ctx, &message, pubsub_topic, timeout).await
}
/// Publish a message using Waku Relay.
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_relay_publishchar-messagejson-char-pubsubtopic-int-timeoutms)
/// The pubsub_topic parameter is optional and if not specified it will be derived from the contentTopic.
pub fn relay_publish_message(
pub async fn relay_publish_message(
&self,
message: &WakuMessage,
pubsub_topic: Option<WakuPubSubTopic>,
pubsub_topic: &PubsubTopic,
timeout: Option<Duration>,
) -> Result<MessageId> {
relay::waku_relay_publish_message(message, pubsub_topic, timeout)
}
/// Determine if there are enough peers to publish a message on a given pubsub topic
pub fn relay_enough_peers(&self, pubsub_topic: Option<WakuPubSubTopic>) -> Result<bool> {
relay::waku_enough_peers(pubsub_topic)
}
/// Subscribe to WakuRelay to receive messages matching a content filter.
pub fn relay_subscribe(&self, content_filter: &ContentFilter) -> Result<()> {
relay::waku_relay_subscribe(content_filter)
) -> Result<MessageHash> {
relay::waku_relay_publish_message(&self.ctx, message, pubsub_topic, timeout).await
}
/// Closes the pubsub subscription to stop receiving messages matching a content filter. No more messages will be received from this pubsub topic
pub fn relay_unsubscribe(&self, content_filter: &ContentFilter) -> Result<()> {
relay::waku_relay_unsubscribe(content_filter)
pub async fn relay_unsubscribe(&self, pubsub_topic: &PubsubTopic) -> Result<()> {
relay::waku_relay_unsubscribe(&self.ctx, pubsub_topic).await
}
/// Returns the list of pubsub topics the node is subscribed to in Waku Relay
pub fn relay_topics(&self) -> Result<Vec<String>> {
relay::waku_relay_topics()
}
/// Retrieves historical messages on specific content topics. This method may be called with [`PagingOptions`](`crate::general::PagingOptions`),
/// to retrieve historical messages on a per-page basis. If the request included [`PagingOptions`](`crate::general::PagingOptions`),
/// the node must return messages on a per-page basis and include [`PagingOptions`](`crate::general::PagingOptions`) in the response.
/// These [`PagingOptions`](`crate::general::PagingOptions`) must contain a cursor pointing to the Index from which a new page can be requested
pub fn store_query(
pub async fn filter_subscribe(
&self,
query: &StoreQuery,
peer_id: &PeerId,
timeout: Option<Duration>,
) -> Result<StoreResponse> {
store::waku_store_query(query, peer_id, timeout)
pubsub_topic: &PubsubTopic,
content_topics: Vec<WakuContentTopic>,
) -> Result<()> {
filter::waku_filter_subscribe(&self.ctx, pubsub_topic, content_topics).await
}
/// Retrieves locally stored historical messages on specific content topics. This method may be called with [`PagingOptions`](`crate::general::PagingOptions`),
/// to retrieve historical messages on a per-page basis. If the request included [`PagingOptions`](`crate::general::PagingOptions`),
/// the node must return messages on a per-page basis and include [`PagingOptions`](`crate::general::PagingOptions`) in the response.
/// These [`PagingOptions`](`crate::general::PagingOptions`) must contain a cursor pointing to the Index from which a new page can be requested
pub fn local_store_query(&self, query: &StoreQuery) -> Result<StoreResponse> {
store::waku_local_store_query(query)
pub async fn filter_unsubscribe(
&self,
pubsub_topic: &PubsubTopic,
content_topics: Vec<WakuContentTopic>,
) -> Result<()> {
filter::waku_filter_unsubscribe(&self.ctx, pubsub_topic, content_topics).await
}
/// Publish a message using Waku Lightpush.
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_lightpush_publishchar-messagejson-char-topic-char-peerid-int-timeoutms)
/// The pubsub_topic parameter is optional and if not specified it will be derived from the contentTopic.
pub fn lightpush_publish(
pub async fn filter_unsubscribe_all(&self) -> Result<()> {
filter::waku_filter_unsubscribe_all(&self.ctx).await
}
pub async fn lightpush_publish_message(
&self,
message: &WakuMessage,
pubsub_topic: Option<WakuPubSubTopic>,
peer_id: PeerId,
timeout: Option<Duration>,
) -> Result<MessageId> {
lightpush::waku_lightpush_publish(message, pubsub_topic, peer_id, timeout)
pubsub_topic: &PubsubTopic,
) -> Result<MessageHash> {
lightpush::waku_lightpush_publish_message(&self.ctx, message, pubsub_topic).await
}
/// Creates a subscription in a lightnode for messages that matches a content filter and optionally a [`WakuPubSubTopic`](`crate::general::WakuPubSubTopic`)
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_filter_subscribechar-filterjson-char-peerid-int-timeoutms)
#[deprecated]
pub fn legacy_filter_subscribe(
#[allow(clippy::too_many_arguments)]
pub async fn store_query(
&self,
filter_subscription: &LegacyFilterSubscription,
peer_id: PeerId,
timeout: Duration,
) -> Result<()> {
legacyfilter::waku_legacy_filter_subscribe(filter_subscription, peer_id, timeout)
}
pubsub_topic: Option<PubsubTopic>,
content_topics: Vec<WakuContentTopic>,
peer_addr: &str,
include_data: bool, // is true, resp contains payload, etc. Only msg_hashes otherwise
time_start: Option<u64>, // unix time nanoseconds
time_end: Option<u64>, // unix time nanoseconds
timeout_millis: Option<Duration>,
) -> Result<Vec<StoreWakuMessageResponse>> {
let mut cursor: Option<MessageHash> = None;
/// Removes subscriptions in a light node matching a content filter and, optionally, a [`WakuPubSubTopic`](`crate::general::WakuPubSubTopic`)
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_filter_unsubscribechar-filterjson-int-timeoutms)
#[deprecated]
pub fn legacy_filter_unsubscribe(
&self,
filter_subscription: &LegacyFilterSubscription,
timeout: Duration,
) -> Result<()> {
legacyfilter::waku_legacy_filter_unsubscribe(filter_subscription, timeout)
}
let mut messages: Vec<StoreWakuMessageResponse> = Vec::new();
/// Creates a subscription to a filter full node matching a content filter.
/// Returns the PeerId on which the filter subscription was created
pub fn filter_subscribe(
&self,
content_filter: &ContentFilter,
peer_id: Option<PeerId>,
timeout: Option<Duration>,
) -> Result<FilterSubscriptionResult> {
filter::waku_filter_subscribe(content_filter, peer_id, timeout)
}
loop {
let query = StoreQueryRequest::new()
.with_pubsub_topic(pubsub_topic.clone())
.with_content_topics(content_topics.clone())
.with_include_data(include_data)
.with_time_start(time_start)
.with_time_end(time_end)
.with_pagination_cursor(cursor)
.with_pagination_forward(true);
/// Used to know if a service node has an active subscription for this client
pub fn filter_ping(&self, peer_id: PeerId, timeout: Option<Duration>) -> Result<()> {
filter::waku_filter_ping(peer_id, timeout)
}
let response =
store::waku_store_query(&self.ctx, query, peer_addr, timeout_millis).await?;
/// Sends a requests to a service node to stop pushing messages matching this filter to this client.
/// It might be used to modify an existing subscription by providing a subset of the original filter
/// criteria
pub fn filter_unsubscribe(
&self,
content_filter: &ContentFilter,
peer_id: PeerId,
timeout: Option<Duration>,
) -> Result<()> {
filter::waku_filter_unsubscribe(content_filter, peer_id, timeout)
}
messages.extend(response.messages);
/// Sends a requests to a service node (or all service nodes) to stop pushing messages
pub fn filter_unsubscribe_all(
&self,
peer_id: Option<PeerId>,
timeout: Option<Duration>,
) -> Result<()> {
filter::waku_filter_unsubscribe_all(peer_id, timeout)
}
if response.pagination_cursor.is_none() {
break;
}
cursor = response.pagination_cursor;
}
/// Update the bootnodes used by DiscoveryV5 by passing a list of ENRs
pub fn discv5_update_bootnodes(bootnodes: Vec<String>) -> Result<()> {
discovery::waku_discv5_update_bootnodes(bootnodes)
}
}
/// Spawn a new Waku node with the given configuration (default configuration if `None` provided)
/// as per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_newchar-jsonconfig)
pub fn waku_new(config: Option<WakuNodeConfig>) -> Result<WakuNodeHandle<Initialized>> {
let mut node_initialized = WAKU_NODE_INITIALIZED
.lock()
.expect("Access to the mutex at some point");
if *node_initialized {
return Err("Waku node is already initialized".into());
}
*node_initialized = true;
management::waku_new(config).map(|_| WakuNodeHandle(Default::default()))
}
#[cfg(test)]
mod tests {
use super::waku_new;
use serial_test::serial;
#[test]
#[serial]
fn exclusive_running() {
let handle1 = waku_new(None).unwrap();
let handle2 = waku_new(None);
assert!(handle2.is_err());
let stop_handle = handle1.start().unwrap();
stop_handle.stop().unwrap();
messages.reverse();
Ok(messages)
}
}

View File

@ -4,224 +4,33 @@
use std::ffi::CString;
use std::time::Duration;
// crates
use libc::*;
use multiaddr::Multiaddr;
use serde::Deserialize;
// internal
use crate::general::{PeerId, ProtocolId, Result};
use crate::utils::{get_trampoline, handle_json_response, handle_no_response, handle_response};
/// Add a node multiaddress and protocol to the waku nodes peerstore.
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_add_peerchar-address-char-protocolid)
pub fn waku_add_peers(address: &Multiaddr, protocol_id: ProtocolId) -> Result<PeerId> {
let address_ptr = CString::new(address.to_string())
.expect("CString should build properly from the address")
.into_raw();
let protocol_id_ptr = CString::new(protocol_id.to_string())
.expect("CString should build properly from the protocol id")
.into_raw();
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_add_peer(
address_ptr,
protocol_id_ptr,
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(address_ptr));
drop(CString::from_raw(protocol_id_ptr));
out
};
handle_response(code, &result)
}
use crate::general::libwaku_response::{handle_no_response, LibwakuResponse};
use crate::general::Result;
use crate::handle_ffi_call;
use crate::node::context::WakuNodeContext;
/// Dial peer using a multiaddress
/// If `timeout` as milliseconds doesn't fit into a `i32` it is clamped to [`i32::MAX`]
/// If the function execution takes longer than `timeout` value, the execution will be canceled and an error returned.
/// Use 0 for no timeout
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_connect_peerchar-address-int-timeoutms)
pub fn waku_connect_peer_with_address(
pub async fn waku_connect(
ctx: &WakuNodeContext,
address: &Multiaddr,
timeout: Option<Duration>,
) -> Result<()> {
let address_ptr = CString::new(address.to_string())
.expect("CString should build properly from multiaddress")
.into_raw();
let address =
CString::new(address.to_string()).expect("CString should build properly from multiaddress");
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_connect(
address_ptr,
timeout
.map(|duration| duration.as_millis().try_into().unwrap_or(i32::MAX))
.unwrap_or(0),
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(address_ptr));
out
};
handle_no_response(code, &error)
}
/// Dial peer using a peer id
/// If `timeout` as milliseconds doesn't fit into a `i32` it is clamped to [`i32::MAX`]
/// The peer must be already known.
/// It must have been added before with [`waku_add_peers`] or previously dialed with [`waku_connect_peer_with_address`]
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_connect_peeridchar-peerid-int-timeoutms)
pub fn waku_connect_peer_with_id(peer_id: &PeerId, timeout: Option<Duration>) -> Result<()> {
let peer_id_ptr = CString::new(peer_id.as_bytes())
.expect("CString should build properly from peer id")
.into_raw();
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_connect_peerid(
peer_id_ptr,
timeout
.map(|duration| duration.as_millis().try_into().unwrap_or(i32::MAX))
.unwrap_or(0),
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(peer_id_ptr));
out
};
handle_no_response(code, &error)
}
/// Disconnect a peer using its peer id
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_disconnect_peerchar-peerid)
pub fn waku_disconnect_peer_with_id(peer_id: &PeerId) -> Result<()> {
let peer_id_ptr = CString::new(peer_id.as_bytes())
.expect("CString should build properly from peer id")
.into_raw();
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_disconnect(peer_id_ptr, cb, &mut closure as *mut _ as *mut c_void);
drop(CString::from_raw(peer_id_ptr));
out
};
handle_no_response(code, &error)
}
/// Get number of connected peers
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_peer_count)
pub fn waku_peer_count() -> Result<usize> {
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
waku_sys::waku_peer_cnt(cb, &mut closure as *mut _ as *mut c_void)
};
handle_response(code, &result)
}
/// Waku peer supported protocol
///
/// Examples:
/// `"/ipfs/id/1.0.0"`
/// `"/vac/waku/relay/2.0.0"`
/// `"/ipfs/ping/1.0.0"`
pub type Protocol = String;
/// Peer data from known/connected waku nodes
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct WakuPeerData {
/// Waku peer id
#[serde(alias = "peerID")]
peer_id: PeerId,
/// Supported node protocols
protocols: Vec<Protocol>,
/// Node available addresses
#[serde(alias = "addrs")]
addresses: Vec<Multiaddr>,
/// Already connected flag
connected: bool,
}
impl WakuPeerData {
pub fn peer_id(&self) -> &PeerId {
&self.peer_id
}
pub fn protocols(&self) -> &[Protocol] {
&self.protocols
}
pub fn addresses(&self) -> &[Multiaddr] {
&self.addresses
}
pub fn connected(&self) -> bool {
self.connected
}
}
/// List of [`WakuPeerData`]
pub type WakuPeers = Vec<WakuPeerData>;
/// Retrieve the list of peers known by the Waku node
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_peers)
pub fn waku_peers() -> Result<WakuPeers> {
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
waku_sys::waku_peers(cb, &mut closure as *mut _ as *mut c_void)
};
handle_json_response(code, &result)
}
#[cfg(test)]
mod tests {
use crate::node::peers::WakuPeerData;
#[test]
fn deserialize_waku_peer_data() {
let json_str = r#"{
"peerID": "16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47RedcBafeDCBA",
"protocols": [
"/ipfs/id/1.0.0",
"/vac/waku/relay/2.0.0",
"/ipfs/ping/1.0.0"
],
"addrs": [
"/ip4/1.2.3.4/tcp/30303"
],
"connected": true
}"#;
let _: WakuPeerData = serde_json::from_str(json_str).unwrap();
}
handle_ffi_call!(
waku_sys::waku_connect,
handle_no_response,
ctx.get_ptr(),
address.as_ptr(),
timeout
.map(|duration| duration.as_millis().try_into().unwrap_or(u32::MAX))
.unwrap_or(0)
)
}

View File

@ -3,214 +3,99 @@
// std
use std::ffi::CString;
use std::time::Duration;
// crates
use libc::*;
// internal
use crate::general::{
ContentFilter, Encoding, MessageId, Result, WakuContentTopic, WakuMessage, WakuPubSubTopic,
};
use crate::utils::{get_trampoline, handle_json_response, handle_no_response, handle_response};
use crate::general::contenttopic::{Encoding, WakuContentTopic};
use crate::general::libwaku_response::{handle_no_response, handle_response, LibwakuResponse};
use crate::general::pubsubtopic::PubsubTopic;
use crate::general::{messagehash::MessageHash, Result, WakuMessage};
use crate::handle_ffi_call;
use crate::node::context::WakuNodeContext;
/// Create a content topic according to [RFC 23](https://rfc.vac.dev/spec/23/)
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_content_topicchar-applicationname-unsigned-int-applicationversion-char-contenttopicname-char-encoding)
pub fn waku_create_content_topic(
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub async fn waku_create_content_topic(
ctx: &WakuNodeContext,
application_name: &str,
application_version: &str,
application_version: u32,
content_topic_name: &str,
encoding: Encoding,
) -> WakuContentTopic {
let application_name_ptr = CString::new(application_name)
.expect("Application name should always transform to CString")
.into_raw();
let application_version_ptr = CString::new(application_version)
.expect("Application version should always transform to CString")
.into_raw();
let content_topic_name_ptr = CString::new(content_topic_name)
.expect("Content topic should always transform to CString")
.into_raw();
let encoding_ptr = CString::new(encoding.to_string())
.expect("Encoding should always transform to CString")
.into_raw();
) -> Result<WakuContentTopic> {
let application_name = CString::new(application_name)
.expect("Application name should always transform to CString");
let content_topic_name =
CString::new(content_topic_name).expect("Content topic should always transform to CString");
let encoding =
CString::new(encoding.to_string()).expect("Encoding should always transform to CString");
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_content_topic(
application_name_ptr,
application_version_ptr,
content_topic_name_ptr,
encoding_ptr,
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(application_name_ptr));
drop(CString::from_raw(application_version_ptr));
drop(CString::from_raw(content_topic_name_ptr));
drop(CString::from_raw(encoding_ptr));
out
};
handle_response::<WakuContentTopic>(code, &result)
.expect("&str from result should always be extracted")
}
/// Default pubsub topic used for exchanging waku messages defined in [RFC 10](https://rfc.vac.dev/spec/10/)
pub fn waku_default_pubsub_topic() -> WakuPubSubTopic {
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
waku_sys::waku_default_pubsub_topic(cb, &mut closure as *mut _ as *mut c_void)
};
handle_response(code, &result).expect("&str from result should always be extracted")
}
/// Get the list of subscribed pubsub topics in Waku Relay.
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_relay_topics)
pub fn waku_relay_topics() -> Result<Vec<String>> {
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
waku_sys::waku_relay_topics(cb, &mut closure as *mut _ as *mut c_void)
};
handle_json_response(code, &result)
handle_ffi_call!(
waku_sys::waku_content_topic,
handle_response,
ctx.get_ptr(),
application_name.as_ptr(),
application_version,
content_topic_name.as_ptr(),
encoding.as_ptr()
)
}
/// Publish a message using Waku Relay
/// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_relay_publishchar-messagejson-char-pubsubtopic-int-timeoutms)
pub fn waku_relay_publish_message(
pub async fn waku_relay_publish_message(
ctx: &WakuNodeContext,
message: &WakuMessage,
pubsub_topic: Option<WakuPubSubTopic>,
pubsub_topic: &PubsubTopic,
timeout: Option<Duration>,
) -> Result<MessageId> {
let pubsub_topic = pubsub_topic
.unwrap_or_else(waku_default_pubsub_topic)
.to_string();
let message_ptr = CString::new(
) -> Result<MessageHash> {
let message = CString::new(
serde_json::to_string(&message)
.expect("WakuMessages should always be able to success serializing"),
)
.expect("CString should build properly from the serialized waku message")
.into_raw();
let pubsub_topic_ptr = CString::new(pubsub_topic)
.expect("CString should build properly from pubsub topic")
.into_raw();
.expect("CString should build properly from the serialized waku message");
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_relay_publish(
message_ptr,
pubsub_topic_ptr,
timeout
.map(|duration| {
duration
.as_millis()
.try_into()
.expect("Duration as milliseconds should fit in a i32")
})
.unwrap_or(0),
cb,
&mut closure as *mut _ as *mut c_void,
);
let pubsub_topic = CString::new(String::from(pubsub_topic))
.expect("CString should build properly from pubsub topic");
drop(CString::from_raw(message_ptr));
drop(CString::from_raw(pubsub_topic_ptr));
out
};
handle_response(code, &result)
}
pub fn waku_enough_peers(pubsub_topic: Option<WakuPubSubTopic>) -> Result<bool> {
let pubsub_topic = pubsub_topic
.unwrap_or_else(waku_default_pubsub_topic)
.to_string();
let pubsub_topic_ptr = CString::new(pubsub_topic)
.expect("CString should build properly from pubsub topic")
.into_raw();
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_relay_enough_peers(
pubsub_topic_ptr,
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(pubsub_topic_ptr));
out
};
handle_response(code, &result)
}
pub fn waku_relay_subscribe(content_filter: &ContentFilter) -> Result<()> {
let content_filter_ptr = CString::new(
serde_json::to_string(content_filter)
.expect("ContentFilter should always succeed to serialize"),
handle_ffi_call!(
waku_sys::waku_relay_publish,
handle_response,
ctx.get_ptr(),
pubsub_topic.as_ptr(),
message.as_ptr(),
timeout
.map(|duration| {
duration
.as_millis()
.try_into()
.expect("Duration as milliseconds should fit in a u32")
})
.unwrap_or(0)
)
.expect("ContentFilter should always be able to be serialized")
.into_raw();
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_relay_subscribe(
content_filter_ptr,
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(content_filter_ptr));
out
};
handle_no_response(code, &error)
}
pub fn waku_relay_unsubscribe(content_filter: &ContentFilter) -> Result<()> {
let content_filter_ptr = CString::new(
serde_json::to_string(content_filter)
.expect("ContentFilter should always succeed to serialize"),
pub async fn waku_relay_subscribe(ctx: &WakuNodeContext, pubsub_topic: &PubsubTopic) -> Result<()> {
let pubsub_topic = CString::new(String::from(pubsub_topic))
.expect("CString should build properly from pubsub topic");
handle_ffi_call!(
waku_sys::waku_relay_subscribe,
handle_no_response,
ctx.get_ptr(),
pubsub_topic.as_ptr()
)
}
pub async fn waku_relay_unsubscribe(
ctx: &WakuNodeContext,
pubsub_topic: &PubsubTopic,
) -> Result<()> {
let pubsub_topic = CString::new(String::from(pubsub_topic))
.expect("CString should build properly from pubsub topic");
handle_ffi_call!(
waku_sys::waku_relay_unsubscribe,
handle_no_response,
ctx.get_ptr(),
pubsub_topic.as_ptr()
)
.expect("ContentFilter should always be able to be serialized")
.into_raw();
let mut error: String = Default::default();
let error_cb = |v: &str| error = v.to_string();
let code = unsafe {
let mut closure = error_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_relay_subscribe(
content_filter_ptr,
cb,
&mut closure as *mut _ as *mut c_void,
);
drop(CString::from_raw(content_filter_ptr));
out
};
handle_no_response(code, &error)
}

View File

@ -1,84 +1,176 @@
//! Waku [store](https://rfc.vac.dev/spec/36/#waku-store) handling methods
//! Waku store protocol related methods
// std
use std::ffi::CString;
use std::time::Duration;
use uuid::Uuid;
// crates
use libc::*;
use tokio::time::Duration;
// internal
use crate::general::{PeerId, Result, StoreQuery, StoreResponse};
use crate::utils::{get_trampoline, handle_json_response};
use crate::general::libwaku_response::{handle_response, LibwakuResponse};
use crate::general::time::get_now_in_nanosecs;
use crate::general::waku_decode::WakuDecode;
use crate::general::{
contenttopic::WakuContentTopic, messagehash::MessageHash, pubsubtopic::PubsubTopic, Result,
WakuStoreRespMessage,
};
use crate::handle_ffi_call;
use crate::node::context::WakuNodeContext;
use multiaddr::Multiaddr;
use serde::{Deserialize, Serialize};
/// Retrieves historical messages on specific content topics. This method may be called with [`PagingOptions`](`crate::general::PagingOptions`),
/// to retrieve historical messages on a per-page basis. If the request included [`PagingOptions`](`crate::general::PagingOptions`),
/// the node must return messages on a per-page basis and include [`PagingOptions`](`crate::general::PagingOptions`) in the response.
/// These [`PagingOptions`](`crate::general::PagingOptions`) must contain a cursor pointing to the Index from which a new page can be requested
pub fn waku_store_query(
query: &StoreQuery,
peer_id: &PeerId,
timeout: Option<Duration>,
// #[derive(Clone, Serialize, Deserialize, Debug)]
// #[serde(rename_all = "camelCase")]
// pub struct PagingOptions {
// pub page_size: usize,
// pub cursor: Option<MessageHash>,
// pub forward: bool,
// }
/// Criteria used to retrieve historical messages
#[derive(Clone, Serialize, Debug)]
pub struct StoreQueryRequest {
/// if true, the store-response will include the full message content. If false,
/// the store-response will only include a list of message hashes.
#[serde(rename = "request_id")]
request_id: String,
#[serde(rename = "include_data")]
include_data: bool,
#[serde(rename = "pubsub_topic", skip_serializing_if = "Option::is_none")]
pubsub_topic: Option<PubsubTopic>,
#[serde(rename = "content_topics")]
content_topics: Vec<WakuContentTopic>,
#[serde(rename = "time_start", skip_serializing_if = "Option::is_none")]
time_start: Option<u64>,
#[serde(rename = "time_end", skip_serializing_if = "Option::is_none")]
time_end: Option<u64>,
#[serde(rename = "message_hashes", skip_serializing_if = "Option::is_none")]
message_hashes: Option<Vec<MessageHash>>,
#[serde(rename = "pagination_cursor", skip_serializing_if = "Option::is_none")]
pagination_cursor: Option<MessageHash>, // Message hash (key) from where to start query (exclusive)
#[serde(rename = "pagination_forward")]
pagination_forward: bool,
#[serde(rename = "pagination_limit", skip_serializing_if = "Option::is_none")]
pagination_limit: Option<u64>,
}
impl StoreQueryRequest {
pub fn new() -> Self {
StoreQueryRequest {
request_id: Uuid::new_v4().to_string(),
include_data: true,
pubsub_topic: None,
content_topics: Vec::new(),
time_start: Some(get_now_in_nanosecs()),
time_end: Some(get_now_in_nanosecs()),
message_hashes: None,
pagination_cursor: None,
pagination_forward: true,
pagination_limit: Some(25),
}
}
pub fn with_include_data(mut self, include_data: bool) -> Self {
self.include_data = include_data;
self
}
pub fn with_pubsub_topic(mut self, pubsub_topic: Option<PubsubTopic>) -> Self {
self.pubsub_topic = pubsub_topic;
self
}
pub fn with_content_topics(mut self, content_topics: Vec<WakuContentTopic>) -> Self {
self.content_topics = content_topics;
self
}
pub fn with_time_start(mut self, time_start: Option<u64>) -> Self {
self.time_start = time_start;
self
}
pub fn with_time_end(mut self, time_end: Option<u64>) -> Self {
self.time_end = time_end;
self
}
#[allow(dead_code)]
pub fn with_message_hashes(mut self, message_hashes: Vec<MessageHash>) -> Self {
self.message_hashes = Some(message_hashes);
self
}
pub fn with_pagination_cursor(mut self, pagination_cursor: Option<MessageHash>) -> Self {
self.pagination_cursor = pagination_cursor;
self
}
pub fn with_pagination_forward(mut self, pagination_forward: bool) -> Self {
self.pagination_forward = pagination_forward;
self
}
}
#[derive(Clone, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct StoreWakuMessageResponse {
pub message_hash: MessageHash,
pub message: Option<WakuStoreRespMessage>, // None if include_data == false
pub pubsub_topic: String,
}
#[derive(Clone, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct StoreResponse {
#[allow(unused)]
pub request_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[allow(unused)]
pub status_code: u32,
#[allow(unused)]
pub status_desc: String,
/// Array of retrieved historical messages in [`WakuMessage`] format
// #[serde(default)]
pub messages: Vec<StoreWakuMessageResponse>,
/// Paging information in [`PagingOptions`] format from which to resume further historical queries
#[serde(skip_serializing_if = "Option::is_none")]
pub pagination_cursor: Option<MessageHash>,
}
// Implement WakuDecode for Vec<Multiaddr>
impl WakuDecode for StoreResponse {
fn decode(input: &str) -> Result<Self> {
Ok(serde_json::from_str(input).expect("could not parse store resp"))
}
}
pub async fn waku_store_query(
ctx: &WakuNodeContext,
query: StoreQueryRequest,
peer_addr: &str,
timeout_millis: Option<Duration>,
) -> Result<StoreResponse> {
let query_ptr = CString::new(
serde_json::to_string(query).expect("StoreQuery should always be able to be serialized"),
let json_query = CString::new(
serde_json::to_string(&query).expect("StoreQuery should always be able to be serialized"),
)
.expect("CString should build properly from the serialized filter subscription")
.into_raw();
let peer_id_ptr = CString::new(peer_id.clone())
.expect("CString should build properly from peer id")
.into_raw();
.expect("CString should build properly from the serialized filter subscription");
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out = waku_sys::waku_store_query(
query_ptr,
peer_id_ptr,
timeout
.map(|timeout| {
timeout
.as_millis()
.try_into()
.expect("Duration as milliseconds should fit in a i32")
})
.unwrap_or(0),
cb,
&mut closure as *mut _ as *mut c_void,
);
peer_addr
.parse::<Multiaddr>()
.expect("correct multiaddress in store query");
let peer_addr = CString::new(peer_addr).expect("peer_addr CString should be created");
drop(CString::from_raw(query_ptr));
drop(CString::from_raw(peer_id_ptr));
let timeout_millis = timeout_millis.unwrap_or(Duration::from_secs(10));
out
};
handle_json_response(code, &result)
}
/// Retrieves locally stored historical messages on specific content topics from the local archive system. This method may be called with [`PagingOptions`](`crate::general::PagingOptions`),
/// to retrieve historical messages on a per-page basis. If the request included [`PagingOptions`](`crate::general::PagingOptions`),
/// the node must return messages on a per-page basis and include [`PagingOptions`](`crate::general::PagingOptions`) in the response.
/// These [`PagingOptions`](`crate::general::PagingOptions`) must contain a cursor pointing to the Index from which a new page can be requested
pub fn waku_local_store_query(query: &StoreQuery) -> Result<StoreResponse> {
let query_ptr = CString::new(
serde_json::to_string(query).expect("StoreQuery should always be able to be serialized"),
handle_ffi_call!(
waku_sys::waku_store_query,
handle_response,
ctx.get_ptr(),
json_query.as_ptr(),
peer_addr.as_ptr(),
timeout_millis.as_millis() as i32
)
.expect("CString should build properly from the serialized filter subscription")
.into_raw();
let mut result: String = Default::default();
let result_cb = |v: &str| result = v.to_string();
let code = unsafe {
let mut closure = result_cb;
let cb = get_trampoline(&closure);
let out =
waku_sys::waku_store_local_query(query_ptr, cb, &mut closure as *mut _ as *mut c_void);
drop(CString::from_raw(query_ptr));
out
};
handle_json_response(code, &result)
}

View File

@ -1,72 +0,0 @@
use crate::general::Result;
use core::str::FromStr;
use serde::de::DeserializeOwned;
use std::ffi::CStr;
use waku_sys::WakuCallBack;
use waku_sys::{RET_ERR, RET_MISSING_CALLBACK, RET_OK};
pub fn decode<T: DeserializeOwned>(input: &str) -> Result<T> {
serde_json::from_str(input)
.map_err(|err| format!("could not deserialize waku response: {}", err))
}
unsafe extern "C" fn trampoline<F>(
_ret_code: ::std::os::raw::c_int,
data: *const ::std::os::raw::c_char,
user_data: *mut ::std::os::raw::c_void,
) where
F: FnMut(&str),
{
let user_data = &mut *(user_data as *mut F);
let response = if data.is_null() {
""
} else {
unsafe { CStr::from_ptr(data) }
.to_str()
.map_err(|err| {
format!(
"could not retrieve response from pointer returned by waku: {}",
err
)
})
.expect("could not retrieve response")
};
user_data(response);
}
pub fn get_trampoline<F>(_closure: &F) -> WakuCallBack
where
F: FnMut(&str),
{
Some(trampoline::<F>)
}
pub fn handle_no_response(code: i32, error: &str) -> Result<()> {
match code {
RET_OK => Ok(()),
RET_ERR => Err(format!("waku error: {}", error)),
RET_MISSING_CALLBACK => Err("missing callback".to_string()),
_ => Err(format!("undefined return code {}", code)),
}
}
pub fn handle_json_response<F: DeserializeOwned>(code: i32, result: &str) -> Result<F> {
match code {
RET_OK => decode(result),
RET_ERR => Err(format!("waku error: {}", result)),
RET_MISSING_CALLBACK => Err("missing callback".to_string()),
_ => Err(format!("undefined return code {}", code)),
}
}
pub fn handle_response<F: FromStr>(code: i32, result: &str) -> Result<F> {
match code {
RET_OK => result
.parse()
.map_err(|_| format!("could not parse value: {}", result)),
RET_ERR => Err(format!("waku error: {}", result)),
RET_MISSING_CALLBACK => Err("missing callback".to_string()),
_ => Err(format!("undefined return code {}", code)),
}
}

View File

@ -1,223 +1,154 @@
use aes_gcm::{Aes256Gcm, KeyInit};
use multiaddr::Multiaddr;
use rand::thread_rng;
use regex::Regex;
use secp256k1::SecretKey;
use serial_test::serial;
use std::net::IpAddr;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::{collections::HashSet, str::from_utf8};
use tokio::sync::mpsc::{self, Sender};
use tokio::time;
use tokio::time::sleep;
use waku_bindings::node::PubsubTopic;
use waku_bindings::{
waku_default_pubsub_topic, waku_new, waku_set_event_callback, ContentFilter, Encoding, Event,
GossipSubParams, Key, MessageId, ProtocolId, Running, WakuContentTopic, WakuLogLevel,
WakuMessage, WakuNodeConfig, WakuNodeHandle, WakuPubSubTopic,
waku_new, Encoding, Initialized, MessageHash, WakuContentTopic, WakuEvent, WakuMessage,
WakuNodeConfig, WakuNodeHandle,
};
const ECHO_TIMEOUT: u64 = 10;
use waku_bindings::{LibwakuResponse, Running};
const ECHO_TIMEOUT: u64 = 1000;
const ECHO_MESSAGE: &str = "Hi from 🦀!";
const TEST_PUBSUBTOPIC: &str = "test";
const NODES: &[&str] = &[
"/dns4/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm",
"/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ",
"/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS"
];
fn try_publish_relay_messages(
async fn try_publish_relay_messages(
node: &WakuNodeHandle<Running>,
msg: &WakuMessage,
) -> Result<HashSet<MessageId>, String> {
Ok(HashSet::from(
[node.relay_publish_message(msg, None, None)?],
))
}
fn try_publish_lightpush_messages(
node: &WakuNodeHandle<Running>,
msg: &WakuMessage,
) -> Result<HashSet<MessageId>, String> {
let peer_id = node
.peers()
.unwrap()
.iter()
.map(|peer| peer.peer_id())
.find(|id| id.as_str() != node.peer_id().unwrap().as_str())
.unwrap()
.clone();
Ok(HashSet::from([
node.lightpush_publish(msg, None, peer_id, None)?
]))
}
#[derive(Debug)]
struct Response {
id: MessageId,
payload: Vec<u8>,
}
fn set_callback(tx: Sender<Response>, sk: SecretKey, ssk: Key<Aes256Gcm>) {
waku_set_event_callback(move |signal| {
if let Event::WakuMessage(message) = signal.event() {
let id = message.message_id();
let message = message.waku_message();
let payload = if let Ok(message) = message
.try_decode_asymmetric(&sk)
.map_err(|e| println!("{e}"))
{
message.data().to_vec()
} else if let Ok(message) = message
.try_decode_symmetric(&ssk)
.map_err(|e| println!("{e}"))
{
message.data().to_vec()
} else {
message.payload().to_vec()
};
futures::executor::block_on(tx.send(Response {
id: id.to_string(),
payload,
}))
.expect("send response to the receiver");
}
});
) -> Result<HashSet<MessageHash>, String> {
Ok(HashSet::from([node
.relay_publish_message(msg, &PubsubTopic::new(TEST_PUBSUBTOPIC), None)
.await?]))
}
async fn test_echo_messages(
node: &WakuNodeHandle<Running>,
node1: WakuNodeHandle<Initialized>,
node2: WakuNodeHandle<Initialized>,
content: &'static str,
content_topic: WakuContentTopic,
sk: SecretKey,
ssk: Key<Aes256Gcm>,
) {
let message = WakuMessage::new(
content,
content_topic,
1,
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis()
.try_into()
.unwrap(),
Vec::new(),
false,
);
) -> Result<(), String> {
// setting a naïve event handler to avoid appearing ERR messages in logs
node1
.set_event_callback(|_| {})
.expect("set event call back working");
let (tx, mut rx) = mpsc::channel(1);
set_callback(tx, sk, ssk);
let rx_waku_message: Arc<Mutex<WakuMessage>> = Arc::new(Mutex::new(WakuMessage::default()));
let mut ids = try_publish_relay_messages(node, &message).expect("send relay messages");
let rx_waku_message_cloned = rx_waku_message.clone();
let closure = move |response| {
if let LibwakuResponse::Success(v) = response {
let event: WakuEvent = serde_json::from_str(v.unwrap().as_str())
.expect("Parsing event to succeed test_echo_messages");
ids.extend(try_publish_lightpush_messages(node, &message).expect("send lightpush messages"));
while let Some(res) = rx.recv().await {
if ids.take(&res.id).is_some() {
let msg = from_utf8(&res.payload).expect("should be valid message");
assert_eq!(content, msg);
match event {
WakuEvent::WakuMessage(evt) => {
if let Ok(mut msg_lock) = rx_waku_message_cloned.lock() {
*msg_lock = evt.waku_message;
}
}
WakuEvent::RelayTopicHealthChange(_evt) => {
// dbg!("Relay topic change evt", evt);
}
WakuEvent::ConnectionChange(_evt) => {
// dbg!("Conn change evt", evt);
}
WakuEvent::Unrecognized(err) => panic!("Unrecognized waku event: {:?}", err),
_ => panic!("event case not expected"),
};
}
if ids.is_empty() {
break;
}
}
}
#[ignore]
#[tokio::test]
#[serial]
async fn discv5_echo() -> Result<(), String> {
let config = WakuNodeConfig {
host: IpAddr::from_str("0.0.0.0").ok(),
log_level: Some(WakuLogLevel::Error),
discv5: Some(true),
discv5_udp_port: Some(9000),
discv5_bootstrap_nodes: Vec::new(),
..Default::default()
};
let node = waku_new(Some(config))?;
let node = node.start()?;
println!("Node peer id: {}", node.peer_id()?);
println!("Before setting event callback");
for node_address in NODES {
let address: Multiaddr = node_address.parse().unwrap();
let peer_id = node.add_peer(&address, ProtocolId::Relay)?;
node.connect_peer_with_id(&peer_id, None)?;
}
node2
.set_event_callback(closure)
.expect("set event call back working"); // Set the event callback with the closure
assert!(node.peers()?.len() >= NODES.len());
assert!(node.peer_count()? >= NODES.len());
let node1 = node1.start().await?;
let node2 = node2.start().await?;
assert!(node.relay_enough_peers(None)?);
let sk = SecretKey::new(&mut thread_rng());
let ssk = Aes256Gcm::generate_key(&mut thread_rng());
node1
.relay_subscribe(&PubsubTopic::new(TEST_PUBSUBTOPIC))
.await
.unwrap();
node2
.relay_subscribe(&PubsubTopic::new(TEST_PUBSUBTOPIC))
.await
.unwrap();
// Subscribe to default channel.
let content_filter = ContentFilter::new(Some(waku_default_pubsub_topic()), vec![]);
node.relay_subscribe(&content_filter)?;
let content_topic = WakuContentTopic::new("toychat", "2", "huilong", Encoding::Proto);
sleep(Duration::from_secs(5)).await;
let topics = node.relay_topics()?;
let default_topic = waku_default_pubsub_topic();
assert!(topics.len() == 1);
let topic: WakuPubSubTopic = topics[0].parse().unwrap();
// Interconnect nodes
// Replace all matches with 127.0.0.1 to avoid issue with NAT or firewall.
let addresses1 = node1.listen_addresses().await.unwrap();
let addresses1 = &addresses1[0].to_string();
assert!(topic == default_topic);
let re = Regex::new(r"\b(?:\d{1,3}\.){3}\d{1,3}\b").unwrap();
let addresses1 = re.replace_all(addresses1, "127.0.0.1").to_string();
let sleep = time::sleep(Duration::from_secs(ECHO_TIMEOUT));
tokio::pin!(sleep);
let addresses1 = addresses1.parse::<Multiaddr>().expect("parse multiaddress");
// Send and receive messages. Waits until all messages received.
let got_all = tokio::select! {
_ = sleep => false,
_ = test_echo_messages(&node, ECHO_MESSAGE, content_topic, sk, ssk) => true,
};
println!("Connecting node1 to node2: {}", addresses1);
node2.connect(&addresses1, None).await.unwrap();
assert!(got_all);
// Wait for mesh to form
sleep(Duration::from_secs(3)).await;
for node_data in node.peers()? {
if node_data.peer_id() != &node.peer_id()? {
node.disconnect_peer_with_id(node_data.peer_id())?;
dbg!("Before publish");
let message = WakuMessage::new(content, content_topic, 1, Vec::new(), false);
let _ids = try_publish_relay_messages(&node1, &message)
.await
.expect("send relay messages");
// Wait for the msg to arrive
let rx_waku_message_cloned = rx_waku_message.clone();
for _ in 0..50 {
let message_received = if let Ok(msg) = rx_waku_message_cloned.lock() {
// dbg!("The waku message value is: {:?}", msg);
let payload = msg.payload.to_vec();
let payload_str = from_utf8(&payload).expect("should be valid message");
payload_str == ECHO_MESSAGE
} else {
false
};
if message_received {
node1.stop().await?;
node2.stop().await?;
return Ok(());
}
sleep(Duration::from_millis(100)).await;
}
node.stop()?;
Ok(())
let node1 = node1.stop().await?;
let node2 = node2.stop().await?;
node1.waku_destroy().await?;
node2.waku_destroy().await?;
Err("Unexpected test ending".to_string())
}
#[ignore]
#[tokio::test]
#[serial]
async fn default_echo() -> Result<(), String> {
let config = WakuNodeConfig {
log_level: Some(WakuLogLevel::Error),
println!("Test default_echo");
let node1 = waku_new(Some(WakuNodeConfig {
tcp_port: Some(60010),
..Default::default()
};
}))
.await?;
let node2 = waku_new(Some(WakuNodeConfig {
tcp_port: Some(60020),
..Default::default()
}))
.await?;
let node = waku_new(Some(config))?;
let node = node.start()?;
println!("Node peer id: {}", node.peer_id()?);
for node_address in NODES {
let address: Multiaddr = node_address.parse().unwrap();
let peer_id = node.add_peer(&address, ProtocolId::Relay)?;
node.connect_peer_with_id(&peer_id, None)?;
}
assert!(node.peers()?.len() >= NODES.len());
assert!(node.peer_count()? >= NODES.len());
assert!(node.relay_enough_peers(None)?);
let sk = SecretKey::new(&mut thread_rng());
let ssk = Aes256Gcm::generate_key(&mut thread_rng());
// subscribe to default channel
let content_filter = ContentFilter::new(Some(waku_default_pubsub_topic()), vec![]);
node.relay_subscribe(&content_filter)?;
let content_topic = WakuContentTopic::new("toychat", "2", "huilong", Encoding::Proto);
let sleep = time::sleep(Duration::from_secs(ECHO_TIMEOUT));
@ -226,106 +157,34 @@ async fn default_echo() -> Result<(), String> {
// Send and receive messages. Waits until all messages received.
let got_all = tokio::select! {
_ = sleep => false,
_ = test_echo_messages(&node, ECHO_MESSAGE, content_topic, sk, ssk) => true,
_ = test_echo_messages(node1, node2, ECHO_MESSAGE, content_topic) => true,
};
assert!(got_all);
for node_data in node.peers()? {
if node_data.peer_id() != &node.peer_id()? {
node.disconnect_peer_with_id(node_data.peer_id())?;
}
}
node.stop()?;
Ok(())
}
#[test]
#[tokio::test]
#[serial]
fn gossipsub_config() -> Result<(), String> {
let params = GossipSubParams {
d: Some(6),
dlo: Some(3),
dhi: Some(12),
dscore: Some(10),
dout: Some(8),
history_length: Some(500),
history_gossip: Some(3),
dlazy: Some(12),
gossip_factor: Some(0.25),
gossip_retransmission: Some(4),
heartbeat_initial_delay_ms: Some(500),
heartbeat_interval_seconds: Some(60),
slow_heartbeat_warning: Some(0.5),
fanout_ttl_seconds: Some(60),
prune_peers: Some(3),
prune_backoff_seconds: Some(900),
unsubscribe_backoff_seconds: Some(60),
connectors: Some(3),
max_pending_connections: Some(50),
connection_timeout_seconds: Some(15),
direct_connect_ticks: Some(5),
direct_connect_initial_delay_seconds: Some(5),
opportunistic_graft_ticks: Some(8),
opportunistic_graft_peers: Some(2),
graft_flood_threshold_seconds: Some(120),
max_ihave_length: Some(32),
max_ihave_messages: Some(8),
iwant_followup_time_seconds: Some(120),
seen_messages_ttl_seconds: Some(120),
};
async fn node_restart() {
let config = WakuNodeConfig {
gossipsub_params: params.into(),
log_level: Some(WakuLogLevel::Error),
node_key: Some(
SecretKey::from_str("05f381866cc21f6c1e2e80e07fa732008e36d942dce3206ad6dcd6793c98d609")
.unwrap(),
),
..Default::default()
};
let node = waku_new(Some(config))?;
let node = node.start()?;
node.stop()?;
Ok(())
}
#[test]
#[serial]
fn loglevel_error() -> Result<(), String> {
let config = WakuNodeConfig {
log_level: Some(WakuLogLevel::Error),
..Default::default()
};
let node = waku_new(Some(config))?;
let node = node.start()?;
node.stop()?;
Ok(())
}
#[test]
#[serial]
fn loglevel_info() -> Result<(), String> {
let config = WakuNodeConfig {
log_level: Some(WakuLogLevel::Info),
..Default::default()
};
let node = waku_new(Some(config))?;
let node = node.start()?;
node.stop()?;
Ok(())
}
#[test]
#[serial]
fn node_restart() {
let config = WakuNodeConfig::default();
for _ in 0..3 {
let node = waku_new(config.clone().into()).expect("default config should be valid");
let node = node.start().expect("node should start with valid config");
assert!(node.peer_id().is_ok());
node.stop().expect("node should stop");
let node = waku_new(config.clone().into())
.await
.expect("default config should be valid");
let node = node
.start()
.await
.expect("node should start with valid config");
let node = node.stop().await.expect("node should stop");
node.waku_destroy().await.expect("free resources");
}
}

View File

@ -1,13 +1,15 @@
[package]
name = "waku-sys"
version = "0.5.0"
version = "1.0.0"
edition = "2021"
authors = [
"Daniel Sanchez Quiros <danielsq@status.im>"
"Daniel Sanchez Quiros <danielsq@status.im>",
"Richard Ramos <richard@waku.org>",
"Ivan Folgueira Bande <ivansete@status.im>"
]
description = "Waku networking library generated bindings"
license = "MIT OR Apache-2.0"
repository = "https://github.com/waku-org/waku-rust-bindings"
repository = "https://github.com/logos-messaging/logos-messaging-rust-bindings"
keywords = ["waku", "peer-to-peer", "libp2p", "networking"]
categories = ["network-programming"]
@ -16,14 +18,12 @@ exclude = [
"vendor/docs/*",
"vendor/coverage/*",
"vendor/pkg/*",
"vendor/scripts/*",
"vendor/tests/*",
"vendor/ci/*",
"vendor/cmd/*",
"**/*.md",
"**/*.lock",
"**/*.nix",
"**/Makefile",
"**/Dockerfile",
]
@ -35,3 +35,4 @@ crate-type = ["rlib"]
[build-dependencies]
bindgen = "0.64"
cc = "1.0.73"

View File

@ -1,9 +1,9 @@
# Waku rust bindgen bindings
[<img alt="github" src="https://img.shields.io/badge/github-Github-red?style=for-the-badge&labelColor=555555&logo=github" height="20">](https://github.com/waku-org/waku-rust-bindings)
[<img alt="github" src="https://img.shields.io/badge/github-Github-red?style=for-the-badge&labelColor=555555&logo=github" height="20">](https://github.com/logos-messaging/logos-messaging-rust-bindings)
[<img alt="crates.io" src="https://img.shields.io/crates/v/waku-bindings.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/waku-sys)
[<img alt="docs.rs" src="https://img.shields.io/badge/doc/waku-bindings-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/waku-sys)
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/waku-org/waku-rust-bindings/main.yml?branch=master" height="20">](https://github.com/waku-org/waku-rust-bindings/actions/workflows/main.yml?query=branch%3Amaster)
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/logos-messaging/logos-messaging-rust-bindings/main.yml?branch=master" height="20">](https://github.com/logos-messaging/logos-messaging-rust-bindings/actions/workflows/main.yml?query=branch%3Amaster)
Rust layer on top of [`go-waku`](https://github.com/status-im/go-waku) [c ffi bindings](https://github.com/status-im/go-waku/blob/v0.2.2/library/README.md).

View File

@ -1,86 +1,123 @@
use std::env;
use std::env::set_current_dir;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn get_go_bin() -> String {
if cfg!(target_family = "unix") {
let output = String::from_utf8(
Command::new("/usr/bin/which")
.arg("go")
.output()
.map_err(|e| println!("cargo:warning=Couldn't find `which` command: {e}"))
.expect("`which` command not found")
.stdout,
)
.expect("which output couldnt be parsed");
extern crate cc;
if output.is_empty() {
println!("cargo:warning=Couldn't find go binary installed, please ensure that it is installed and/or withing the system paths");
panic!("Couldn't find `go` binary installed");
fn submodules_init(project_dir: &Path) {
let mark_file_path = ".submodules-initialized";
// Check if the mark file exists
if !Path::new(mark_file_path).exists() {
// If mark file doesn't exist, initialize submodule
if Command::new("git")
.args(["submodule", "init"])
.status()
.expect("Failed to execute 'git submodule init'")
.success()
&& Command::new("git")
.args(["submodule", "update", "--recursive"])
.status()
.expect("Failed to execute 'git submodule update --recursive'")
.success()
{
// Now, inside nwaku folder, run 'make update' to get nwaku's vendors
let nwaku_path = project_dir.join("vendor");
set_current_dir(nwaku_path).expect("Moving to vendor dir");
if Command::new("make")
.args(["update"])
.status()
.expect("Failed to execute 'make update'")
.success()
{
std::fs::File::create(mark_file_path).expect("Failed to create mark file");
} else {
panic!("Failed to run 'make update' within nwaku folder.");
}
set_current_dir(project_dir).expect("Going back to project dir");
println!("Git submodules initialized and updated successfully.");
} else {
panic!("Failed to initialize or update git submodules.");
}
output.trim().to_string()
} else if cfg!(target_family = "windows") {
"go".into()
} else {
panic!("OS not supported!");
println!("Mark file '{mark_file_path}' exists. Skipping git submodule initialization.");
}
}
fn build_go_waku_lib(go_bin: &str, project_dir: &Path) {
// Build go-waku static lib
// build command taken from waku make file:
// https://github.com/status-im/go-waku/blob/eafbc4c01f94f3096c3201fb1e44f17f907b3068/Makefile#L115
let out_dir: PathBuf = env::var_os("OUT_DIR").unwrap().into();
let vendor_path = project_dir.join("vendor");
set_current_dir(vendor_path).expect("Moving to vendor dir");
let mut cmd = Command::new(go_bin);
cmd.env("CGO_ENABLED", "1")
.arg("build")
.arg("-buildmode=c-archive")
.arg("-tags=gowaku_no_rln")
.arg("-o")
.arg(out_dir.join("libgowaku.a"))
.arg("./library/c");
// Setting `GOCACHE=/tmp/` for crates.io job that builds documentation
// when a crate is being published or updated.
if std::env::var("DOCS_RS").is_ok() {
cmd.env("GOCACHE", "/tmp/");
}
fn build_nwaku_lib(project_dir: &Path) {
let nwaku_path = project_dir.join("vendor");
set_current_dir(nwaku_path).expect("Moving to vendor dir");
let mut cmd = Command::new("make");
cmd.arg("libwaku").arg("STATIC=1");
cmd.status()
.map_err(|e| println!("cargo:warning=go build failed due to: {e}"))
.map_err(|e| println!("cargo:warning=make build failed due to: {e}"))
.unwrap();
set_current_dir(project_dir).expect("Going back to project dir");
}
fn patch_gowaku_lib() {
// Replacing cgo_utils.h as it is only needed when compiling go-waku bindings
let lib_dir: PathBuf = env::var_os("OUT_DIR").unwrap().into();
let file_path = lib_dir.join("libgowaku.h");
let data = fs::read_to_string(&file_path).expect("Unable to read file");
let new_data = data.replace("#include <cgo_utils.h>", "");
fs::write(&file_path, new_data).expect("Unable to write file");
}
fn generate_bindgen_code(project_dir: &Path) {
let nwaku_path = project_dir.join("vendor");
let header_path = nwaku_path.join("library/libwaku.h");
fn generate_bindgen_code() {
let lib_dir: PathBuf = env::var_os("OUT_DIR").unwrap().into();
cc::Build::new()
.object(
nwaku_path
.join("vendor/nim-libbacktrace/libbacktrace_wrapper.o")
.display()
.to_string(),
)
.compile("libbacktrace_wrapper");
println!("cargo:rustc-link-search={}", lib_dir.display());
println!("cargo:rustc-link-lib=static=gowaku");
println!("cargo:rerun-if-changed=libgowaku.h");
println!("cargo:rerun-if-changed={}", header_path.display());
println!(
"cargo:rustc-link-search={}",
nwaku_path.join("build").display()
);
println!("cargo:rustc-link-lib=static=waku");
patch_gowaku_lib();
println!(
"cargo:rustc-link-search={}",
nwaku_path
.join("vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/build")
.display()
);
println!("cargo:rustc-link-lib=static=miniupnpc");
println!(
"cargo:rustc-link-search={}",
nwaku_path
.join("vendor/nim-nat-traversal/vendor/libnatpmp-upstream")
.display()
);
println!("cargo:rustc-link-lib=static=natpmp");
println!("cargo:rustc-link-lib=dl");
println!("cargo:rustc-link-lib=m");
println!(
"cargo:rustc-link-search=native={}",
nwaku_path
.join("vendor/nim-libbacktrace/install/usr/lib")
.display()
);
println!("cargo:rustc-link-lib=static=backtrace");
cc::Build::new()
.file("src/cmd.c") // Compile the C file
.compile("cmditems"); // Compile it as a library
println!("cargo:rustc-link-lib=static=cmditems");
// Generate waku bindings with bindgen
let bindings = bindgen::Builder::default()
// The input header we would like to generate
// bindings for.
.header(format!("{}/{}", lib_dir.display(), "libgowaku.h"))
.header(format!("{}", header_path.display()))
// Tell cargo to invalidate the built crate whenever any of the
// included header files changed.
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
@ -98,10 +135,9 @@ fn generate_bindgen_code() {
#[cfg(not(doc))]
fn main() {
let go_bin = get_go_bin();
let project_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
build_go_waku_lib(&go_bin, &project_dir);
generate_bindgen_code();
submodules_init(&project_dir);
build_nwaku_lib(&project_dir);
generate_bindgen_code(&project_dir);
}

13
waku-sys/src/cmd.c Normal file
View File

@ -0,0 +1,13 @@
/*
This file is needed to avoid errors like the following when linking the waku-sys lib crate:
<<undefined reference to `cmdCount'>>
and
<<undefined reference to `cmdLine'>>
*/
#include <stdio.h>
int cmdCount = 0;
char** cmdLine = NULL;

@ -1 +1 @@
Subproject commit e340337d64622d22cb94a969255efe4e36637df0
Subproject commit 4117449b9af6c0304a6115dd4bc0d1d745159685