first pass rust sdk

This commit is contained in:
mfahampshire
2024-08-26 17:14:41 +02:00
parent 7b986b6b09
commit dc800aee08
22 changed files with 758 additions and 111 deletions
@@ -1,7 +1,8 @@
{
"index": "Introduction",
"concepts": "Core Concepts",
"examples": "Basic Examples",
"message-helpers": "Message Helpers",
"message-types": "Message Types",
"examples": "Examples",
"troubleshooting": "Troubleshooting"
}
@@ -0,0 +1,5 @@
{
"overview": "Overview",
"messages": "Message Based Paradigm",
"abstractions": "Connection Abstractions"
}
@@ -1,12 +1,9 @@
# Examples
All the following examples can be found in the `nym-sdk` [examples directory](https://github.com/nymtech/nym/tree/master/sdk/rust/nym-sdk/examples) in the monorepo. Just navigate to `nym/sdk/rust/nym-sdk/examples/` and run the files from there with:
All the following examples can be found in the `nym-sdk` [examples directory](https://github.com/nymtech/nym/tree/master/sdk/rust/nym-sdk/examples) in the monorepo. Just navigate to `nym/sdk/rust/nym-sdk/examples/` and run the files from there with:
```sh
```sh
cargo run --example <NAME_OF_FILE>
```
If you wish to run these outside of the workspace - such as if you want to use one as the basis for your own project - then make sure to import the `sdk`, `tokio`, and `nym_bin_common` crates.
An example `Cargo.toml` file can be found [here](examples/cargo.md).
If you wish to run these outside of the workspace - such as if you want to use one as the basis for your own project - then make sure to import the `sdk`, `tokio`, and `nym_bin_common` crates.
@@ -0,0 +1,9 @@
{
"simple": "Simple Send",
"builders": "Builder Patterns",
"custom-topology": "Custom Network Topologies",
"split-send": "Concurrent Send & Receive",
"socks": "Socks Proxy",
"storage": "Manually Handle Storage",
"surbs": "Anonymous Replies"
}
@@ -0,0 +1,4 @@
{
"builder": "Ephemeral",
"builder-with-storage": "With Storage"
}
@@ -0,0 +1,72 @@
# Mixnet Client Builder with Storage
The previous example involves ephemeral keys - if we want to create and then maintain a client identity over time, our code becomes a little more complex as we need to create, store, and conditionally load these keys.
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/builder_with_storage.rs).
```rust
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
use std::path::PathBuf;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Specify some config options
let config_dir = PathBuf::from("/tmp/mixnet-client");
let storage_paths = mixnet::StoragePaths::new_from_dir(&config_dir).unwrap();
// Create the client with a storage backend, and enable it by giving it some paths. If keys
// exists at these paths, they will be loaded, otherwise they will be generated.
let client = mixnet::MixnetClientBuilder::new_with_default_storage(storage_paths)
.await
.unwrap()
.build()
.unwrap();
// Now we connect to the mixnet, using keys now stored in the paths provided.
let mut client = client.connect_to_mixnet().await.unwrap();
// Be able to get our client address
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send a message throught the mixnet to ourselves
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message");
if let Some(received) = client.wait_for_messages().await {
for r in received {
println!("Received: {}", String::from_utf8_lossy(&r.message));
}
}
client.disconnect().await;
}
```
As seen in the example above, the `mixnet::MixnetClientBuilder::new()` function handles checking for keys in a storage location, loading them if present, or creating them and storing them if not, making client key management very simple.
Assuming our client config is stored in `/tmp/mixnet-client`, the following files are generated:
```
$ tree /tmp/mixnet-client
mixnet-client
├── ack_key.pem
├── db.sqlite
├── db.sqlite-shm
├── db.sqlite-wal
├── gateway_details.json
├── gateway_shared.pem
├── persistent_reply_store.sqlite
├── private_encryption.pem
├── private_identity.pem
├── public_encryption.pem
└── public_identity.pem
1 directory, 11 files
```
@@ -0,0 +1,41 @@
# Mixnet Client Builder
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/builder.rs).
```rust
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Create client builder, including ephemeral keys. The builder can be usable in the context
// where you don't want to connect just yet.
let client = mixnet::MixnetClientBuilder::new_ephemeral()
.build()
.unwrap();
// Now we connect to the mixnet, using ephemeral keys already created
let mut client = client.connect_to_mixnet().await.unwrap();
// Be able to get our client address
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send a message through the mixnet to ourselves
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message");
if let Some(received) = client.wait_for_messages().await {
for r in received {
println!("Received: {}", String::from_utf8_lossy(&r.message));
}
}
client.disconnect().await;
}
```
@@ -1,35 +0,0 @@
# Example Cargo File
This file imports the basic requirements for running these pieces of example code, and can be used as the basis for your own cargo project.
```toml
[package]
name = "your_app"
version = "x.y.z"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# Async runtime
tokio = { version = "1.24.1", features = ["rt-multi-thread", "macros"] }
# Used for (de)serialising incoming and outgoing messages
serde = "1.0.152"
serde_json = "1.0.91"
# Nym clients, addressing, etc
nym-sdk = { git = "https://github.com/nymtech/nym", branch = "master" }
nym-sphinx-addressing = { git = "https://github.com/nymtech/nym", branch = "master" }
nym-bin-common = { git = "https://github.com/nymtech/nym", branch = "master" }
nym-sphinx-anonymous-replies = { git = "https://github.com/nymtech/nym", branch = "master" }
# Additional dependencies if you're interacting with Nyx or another Cosmos SDK blockchain
cosmrs = "=0.14.0"
nym-validator-client = { git = "https://github.com/nymtech/nym", branch = "master" }
# If you're building an app with a client and server / serivce this might be a useful structure for your repo
[[bin]]
name = "client"
path = "bin/client.rs"
[[bin]]
name = "service"
path = "bin/service.rs"
```
@@ -1,8 +0,0 @@
# Coconut credential generation
The following code shows how you can use the SDK to create and use a credential representing paid bandwidth on the Sandbox testnet.
```rust,noplayground
{{#include ../../../../../../sdk/rust/nym-sdk/examples/bandwidth.rs}}
```
You can read more about Coconut credentials (also referred to as `zk-Nym`) [here](https://nymtech.net/docs/coconut.html).
@@ -0,0 +1,5 @@
{
"custom-network": "Different Options",
"custom-provider": "Custom Topology Provider",
"manual-topology": "Manually Overwrite Topology"
}
@@ -1,18 +1,11 @@
# Importing and using a custom network topology
If you want to send traffic through a sub-set of nodes (for instance, ones you control, or a small test setup) when developing, debugging, or performing research, you will need to import these nodes as a custom network topology, instead of grabbing it from the [`Mainnet Nym-API`](https://validator.nymtech.net/api/swagger/index.html) (`examples/custom_topology_provider.rs`).
If you want to send traffic through a sub-set of nodes (for instance, ones you control, or a small test setup) when developing, debugging, or performing research, you will need to import these nodes as a custom network topology, instead of grabbing it from the [`Mainnet Nym-API`](https://validator.nymtech.net/api/swagger/index.html).
There are two ways to do this:
## Import a custom Nym API endpoint
If you are also running a Validator and Nym API for your network, you can specify that endpoint as such and interact with it as clients usually do (under the hood):
```rust,noplayground
{{#include ../../../../../../sdk/rust/nym-sdk/examples/custom_topology_provider.rs}}
```
## Custom Topology Provider
If you are also running a Validator and Nym API for your network, you can specify that endpoint. Clients will then use this endpoint to grab a network topology on startup. You can also use this to specify using a testnet.
## Import a specific topology manually
If you aren't running a Validator and Nym API, and just want to import a specific sub-set of mix nodes, you can simply overwrite the grabbed topology manually:
```rust,noplayground
{{#include ../../../../../../sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs}}
```
If you aren't running a Validator and Nym API, and just want to import a specific sub-set of mix nodes, you can also overwrite the grabbed topology manually.
@@ -0,0 +1,89 @@
# Custom Topology Provider
If you are also running a Validator and Nym API for your network, you can specify that endpoint as such and interact with it as clients usually do (under the hood).
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/custom_topology_provider.rs)
```rust
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
use nym_topology::provider_trait::{async_trait, TopologyProvider};
use nym_topology::{nym_topology_from_detailed, NymTopology};
use url::Url;
struct MyTopologyProvider {
validator_client: nym_validator_client::client::NymApiClient,
}
impl MyTopologyProvider {
fn new(nym_api_url: Url) -> MyTopologyProvider {
MyTopologyProvider {
validator_client: nym_validator_client::client::NymApiClient::new(nym_api_url),
}
}
async fn get_topology(&self) -> NymTopology {
let mixnodes = self
.validator_client
.get_cached_active_mixnodes()
.await
.unwrap();
// in our topology provider only use mixnodes that have mix_id divisible by 3
// and have more than 100k nym (i.e. 100'000'000'000 unym) in stake
// why? because this is just an example to showcase arbitrary uses and capabilities of this trait
let filtered_mixnodes = mixnodes
.into_iter()
.filter(|mix| {
mix.mix_id() % 3 == 0 && mix.total_stake() > "100000000000".parse().unwrap()
})
.collect::<Vec<_>>();
let gateways = self.validator_client.get_cached_gateways().await.unwrap();
nym_topology_from_detailed(filtered_mixnodes, gateways)
}
}
#[async_trait]
impl TopologyProvider for MyTopologyProvider {
// this will be manually refreshed on a timer specified inside mixnet client config
async fn get_new_topology(&mut self) -> Option<NymTopology> {
Some(self.get_topology().await)
}
}
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
let nym_api = "https://validator.nymtech.net/api/".parse().unwrap();
let my_topology_provider = MyTopologyProvider::new(nym_api);
// Passing no config makes the client fire up an ephemeral session and figure things out on its own
let mut client = mixnet::MixnetClientBuilder::new_ephemeral()
.custom_topology_provider(Box::new(my_topology_provider))
.build()
.unwrap()
.connect_to_mixnet()
.await
.unwrap();
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send a message through the mixnet to ourselves
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message (ctrl-c to exit)");
client
.on_messages(|msg| println!("Received: {}", String::from_utf8_lossy(&msg.message)))
.await;
}
```
@@ -0,0 +1,102 @@
# Manually Overwrite Topology
If you aren't running a Validator and Nym API, and just want to import a specific sub-set of mix nodes, you can simply overwrite the grabbed topology manually.
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs)
```rust
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
use nym_topology::mix::Layer;
use nym_topology::{mix, NymTopology};
use std::collections::BTreeMap;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Passing no config makes the client fire up an ephemeral session and figure shit out on its own
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
let starting_topology = client.read_current_topology().await.unwrap();
// but we don't like our default topology, we want to use only those very specific, hardcoded, nodes:
let mut mixnodes = BTreeMap::new();
mixnodes.insert(
1,
vec![mix::Node {
mix_id: 63,
owner: None,
host: "172.105.92.48".parse().unwrap(),
mix_host: "172.105.92.48:1789".parse().unwrap(),
identity_key: "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK"
.parse()
.unwrap(),
sphinx_key: "CBmYewWf43iarBq349KhbfYMc9ys2ebXWd4Vp4CLQ5Rq"
.parse()
.unwrap(),
layer: Layer::One,
version: "1.1.0".into(),
}],
);
mixnodes.insert(
2,
vec![mix::Node {
mix_id: 23,
owner: None,
host: "178.79.143.65".parse().unwrap(),
mix_host: "178.79.143.65:1789".parse().unwrap(),
identity_key: "4Yr4qmEHd9sgsuQ83191FR2hD88RfsbMmB4tzhhZWriz"
.parse()
.unwrap(),
sphinx_key: "8ndjk5oZ6HxUZNScLJJ7hk39XtUqGexdKgW7hSX6kpWG"
.parse()
.unwrap(),
layer: Layer::Two,
version: "1.1.0".into(),
}],
);
mixnodes.insert(
3,
vec![mix::Node {
mix_id: 66,
owner: None,
host: "139.162.247.97".parse().unwrap(),
mix_host: "139.162.247.97:1789".parse().unwrap(),
identity_key: "66UngapebhJRni3Nj52EW1qcNsWYiuonjkWJzHFsmyYY"
.parse()
.unwrap(),
sphinx_key: "7KyZh8Z8KxuVunqytAJ2eXFuZkCS7BLTZSzujHJZsGa2"
.parse()
.unwrap(),
layer: Layer::Three,
version: "1.1.0".into(),
}],
);
// but we like the available gateways, so keep using them!
// (we like them because the author of this example is too lazy to use the same hardcoded gateway
// during client initialisation to make sure we are able to send to ourselves : ) )
let custom_topology = NymTopology::new(mixnodes, starting_topology.gateways().to_vec());
client.manually_overwrite_topology(custom_topology).await;
// and everything we send now should only ever go via those nodes
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send a message through the mixnet to ourselves
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message (ctrl-c to exit)");
client
.on_messages(|msg| println!("Received: {}", String::from_utf8_lossy(&msg.message)))
.await;
}
```
@@ -1,28 +0,0 @@
# Key Creation and Use
The previous example involves ephemeral keys - if we want to create and then maintain a client identity over time, our code becomes a little more complex as we need to create, store, and conditionally load these keys (`examples/builder_with_storage`):
```rust,noplayground
{{#include ../../../../../../sdk/rust/nym-sdk/examples/builder_with_storage.rs}}
```
As seen in the example above, the `mixnet::MixnetClientBuilder::new()` function handles checking for keys in a storage location, loading them if present, or creating them and storing them if not, making client key management very simple.
Assuming our client config is stored in `/tmp/mixnet-client`, the following files are generated:
```
$ tree /tmp/mixnet-client
mixnet-client
├── ack_key.pem
├── db.sqlite
├── db.sqlite-shm
├── db.sqlite-wal
├── gateway_details.json
├── gateway_shared.pem
├── persistent_reply_store.sqlite
├── private_encryption.pem
├── private_identity.pem
├── public_encryption.pem
└── public_identity.pem
1 directory, 11 files
```
@@ -1,8 +1,34 @@
# Simple Send
Lets look at a very simple example of how you can import and use the websocket client in a piece of Rust code (`examples/simple.rs`).
# Simple Send
Lets look at a very simple example of how you can import and use the websocket client in a piece of Rust code.
Simply importing the `nym_sdk` crate into your project allows you to create a client and send traffic through the mixnet.
```rust,noplayground
{{#include ../../../../../../sdk/rust/nym-sdk/examples/simple.rs}}
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/simple.rs)
```rust
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Passing no config makes the client fire up an ephemeral session and figure shit out on its own
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
// Be able to get our client address
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send a message through the mixnet to ourselves
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message (ctrl-c to exit)");
client
.on_messages(|msg| println!("Received: {}", String::from_utf8_lossy(&msg.message)))
.await;
}
```
@@ -1,10 +1,48 @@
# Socks Proxy
There is also the option to embed the [`socks5-client`](../../../clients/socks5-client.md) into your app code (`examples/socks5.rs`):
# Socks Proxy
```admonish info
If you are looking at implementing Nym as a transport layer for a crypto wallet or desktop app, this is probably the best place to start if they can speak SOCKS5, 4a, or 4.
```
```rust,noplayground
{{#include ../../../../../../sdk/rust/nym-sdk/examples/socks5.rs}}
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/socks5.rs)
```rust
use nym_sdk::mixnet;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
println!("Connecting receiver");
let mut receiving_client = mixnet::MixnetClient::connect_new().await.unwrap();
let socks5_config = mixnet::Socks5::new(receiving_client.nym_address().to_string());
let sending_client = mixnet::MixnetClientBuilder::new_ephemeral()
.socks5_config(socks5_config)
.build()
.unwrap();
println!("Connecting sender");
let sending_client = sending_client.connect_to_mixnet_via_socks5().await.unwrap();
let proxy = reqwest::Proxy::all(sending_client.socks5_url()).unwrap();
let reqwest_client = reqwest::Client::builder().proxy(proxy).build().unwrap();
tokio::spawn(async move {
println!("Sending socks5-wrapped http request");
// Message should be sent through the mixnet, via socks5
// We don't expect to get anything, as there is no network requester on the other end
reqwest_client.get("https://nymtech.net").send().await.ok()
});
println!("Waiting for message");
if let Some(received) = receiving_client.wait_for_messages().await {
for r in received {
println!(
"Received socks5 message requesting for endpoint: {}",
String::from_utf8_lossy(&r.message[10..27])
);
}
}
receiving_client.disconnect().await;
sending_client.disconnect().await;
}
```
@@ -1,6 +1,49 @@
# Send and Receive in Different Tasks
If you need to split the different actions of your client across different tasks, you can do so like this:
If you need to split the different actions of your client across different tasks, you can do so like this. You can think of this analogously to spliting a Tcp Stream into read/write. This functionality is also useful for embedding a sending and receiving client into different tasks.
```rust, noplayground
{{#include ../../../../../../sdk/rust/nym-sdk/examples/parallel_sending_and_receiving.rs}}
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/parallel_sending_and_receiving.rs)
```rust
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::StreamExt;
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Passing no config makes the client fire up an ephemeral session and figure stuff out on its own
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
// Be able to get our client address
let our_address = *client.nym_address();
println!("Our client nym address is: {our_address}");
let sender = client.split_sender();
// receiving task
let receiving_task_handle = tokio::spawn(async move {
if let Some(received) = client.next().await {
println!("Received: {}", String::from_utf8_lossy(&received.message));
}
client.disconnect().await;
});
// sending task
let sending_task_handle = tokio::spawn(async move {
sender
.send_plain_message(our_address, "hello from a different task!")
.await
.unwrap();
});
// wait for both tasks to be done
println!("waiting for shutdown");
sending_task_handle.await.unwrap();
receiving_task_handle.await.unwrap();
}
```
@@ -1,6 +1,226 @@
# Manually Handled Storage
If you're integrating mixnet functionality into an existing app and want to integrate saving client configs and keys into your existing storage logic, you can manually perform the actions taken automatically above (`examples/manually_handle_keys_and_config.rs`)
# Manually Handle Storage
If you're integrating mixnet functionality into an existing app and want to integrate saving client configs and keys into your existing storage logic, you can manually perform these actions.
```rust,noplayground
{{#include ../../../../../../sdk/rust/nym-sdk/examples/manually_handle_storage.rs}}
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/manually_handle_storage.rs)
```rust
use nym_sdk::mixnet::{
self, ActiveGateway, BadGateway, ClientKeys, EmptyReplyStorage, EphemeralCredentialStorage,
GatewayRegistration, GatewaysDetailsStore, KeyStore, MixnetClientStorage, MixnetMessageSender,
};
use nym_topology::provider_trait::async_trait;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Just some plain data to pretend we have some external storage that the application
// implementer is using.
let mock_storage = MockClientStorage::empty();
let mut client = mixnet::MixnetClientBuilder::new_with_storage(mock_storage)
.build()
.unwrap()
.connect_to_mixnet()
.await
.unwrap();
// Be able to get our client address
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send important info up the pipe to a buddy
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message");
if let Some(received) = client.wait_for_messages().await {
for r in received {
println!("Received: {}", String::from_utf8_lossy(&r.message));
}
}
client.disconnect().await;
}
#[allow(unused)]
struct MockClientStorage {
pub key_store: MockKeyStore,
pub gateway_details_store: MockGatewayDetailsStore,
pub reply_store: EmptyReplyStorage,
pub credential_store: EphemeralCredentialStorage,
}
impl MockClientStorage {
fn empty() -> Self {
Self {
key_store: MockKeyStore,
gateway_details_store: MockGatewayDetailsStore,
reply_store: EmptyReplyStorage::default(),
credential_store: EphemeralCredentialStorage::default(),
}
}
}
impl MixnetClientStorage for MockClientStorage {
type KeyStore = MockKeyStore;
type ReplyStore = EmptyReplyStorage;
type CredentialStore = EphemeralCredentialStorage;
type GatewaysDetailsStore = MockGatewayDetailsStore;
fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) {
(self.reply_store, self.credential_store)
}
fn key_store(&self) -> &Self::KeyStore {
&self.key_store
}
fn reply_store(&self) -> &Self::ReplyStore {
&self.reply_store
}
fn credential_store(&self) -> &Self::CredentialStore {
&self.credential_store
}
fn gateway_details_store(&self) -> &Self::GatewaysDetailsStore {
&self.gateway_details_store
}
}
struct MockKeyStore;
#[async_trait]
impl KeyStore for MockKeyStore {
type StorageError = MyError;
async fn load_keys(&self) -> Result<ClientKeys, Self::StorageError> {
println!("loading stored keys");
Err(MyError)
}
async fn store_keys(&self, _keys: &ClientKeys) -> Result<(), Self::StorageError> {
println!("storing keys");
Ok(())
}
}
struct MockGatewayDetailsStore;
#[async_trait]
impl GatewaysDetailsStore for MockGatewayDetailsStore {
type StorageError = MyError;
async fn active_gateway(&self) -> Result<ActiveGateway, Self::StorageError> {
println!("getting active gateway");
Err(MyError)
}
async fn set_active_gateway(&self, _gateway_id: &str) -> Result<(), Self::StorageError> {
println!("setting active gateway");
Ok(())
}
async fn all_gateways(&self) -> Result<Vec<GatewayRegistration>, Self::StorageError> {
println!("getting all registered gateways");
Err(MyError)
}
async fn has_gateway_details(&self, _gateway_id: &str) -> Result<bool, Self::StorageError> {
println!("checking for gateway details");
Err(MyError)
}
async fn load_gateway_details(
&self,
_gateway_id: &str,
) -> Result<GatewayRegistration, Self::StorageError> {
println!("loading gateway details");
Err(MyError)
}
async fn store_gateway_details(
&self,
_details: &GatewayRegistration,
) -> Result<(), Self::StorageError> {
println!("storing gateway details");
Ok(())
}
async fn remove_gateway_details(&self, _gateway_id: &str) -> Result<(), Self::StorageError> {
println!("removing gateway details");
Ok(())
}
}
//
// struct MockReplyStore;
//
// #[async_trait]
// impl ReplyStorageBackend for MockReplyStore {
// type StorageError = MyError;
//
// async fn flush_surb_storage(
// &mut self,
// _storage: &CombinedReplyStorage,
// ) -> Result<(), Self::StorageError> {
// todo!()
// }
//
// async fn init_fresh(&mut self, _fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError> {
// todo!()
// }
//
// async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError> {
// todo!()
// }
// }
//
// struct MockCredentialStore;
//
// #[async_trait]
// impl CredentialStorage for MockCredentialStore {
// type StorageError = MyError;
//
// async fn insert_coconut_credential(
// &self,
// _voucher_value: String,
// _voucher_info: String,
// _serial_number: String,
// _binding_number: String,
// _signature: String,
// _epoch_id: String,
// ) -> Result<(), Self::StorageError> {
// todo!()
// }
//
// async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, Self::StorageError> {
// todo!()
// }
//
// async fn consume_coconut_credential(&self, id: i64) -> Result<(), Self::StorageError> {
// todo!()
// }
// }
#[derive(thiserror::Error, Debug)]
#[error("foobar")]
struct MyError;
impl From<BadGateway> for MyError {
fn from(_: BadGateway) -> Self {
MyError
}
}
```
@@ -1,10 +1,83 @@
# Anonymous Replies with SURBs (Single Use Reply Blocks)
Both functions used to send messages through the mixnet (`send_message` and `send_plain_message`) send a pre-determined number of SURBs along with their messages by default.
You can read more about how SURBs function under the hood [here](https://nymtech.net/docs/architecture/traffic-flow.html#private-replies-using-surbs).
You can read more about how SURBs function under the hood [here](https://nymtech.net/docs/architecture/traffic-flow.html#private-replies-using-surbs). **TODO change link**
In order to reply to an incoming message using SURBs, you can construct a `recipient` from the `sender_tag` sent along with the message you wish to reply to:
In order to reply to an incoming message using SURBs, you can construct a `recipient` from the `sender_tag` sent along with the message you wish to reply to.
```rust,noplayground
{{#include ../../../../../../sdk/rust/nym-sdk/examples/surb_reply.rs}}
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/surb_reply.rs)
```rust
use nym_sdk::mixnet::{
AnonymousSenderTag, MixnetClientBuilder, MixnetMessageSender, ReconstructedMessage,
StoragePaths,
};
use std::path::PathBuf;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Specify some config options
let config_dir = PathBuf::from("/tmp/surb-example");
let storage_paths = StoragePaths::new_from_dir(&config_dir).unwrap();
// Create the client with a storage backend, and enable it by giving it some paths. If keys
// exists at these paths, they will be loaded, otherwise they will be generated.
let client = MixnetClientBuilder::new_with_default_storage(storage_paths)
.await
.unwrap()
.build()
.unwrap();
// Now we connect to the mixnet, using keys now stored in the paths provided.
let mut client = client.connect_to_mixnet().await.unwrap();
// Be able to get our client address
let our_address = client.nym_address();
println!("\nOur client nym address is: {our_address}");
// Send a message through the mixnet to ourselves using our nym address
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
// we're going to parse the sender_tag (AnonymousSenderTag) from the incoming message and use it to 'reply' to ourselves instead of our Nym address.
// we know there will be a sender_tag since the sdk sends SURBs along with messages by default.
println!("Waiting for message\n");
// get the actual message - discard the empty vec sent along with a potential SURB topup request
let mut message: Vec<ReconstructedMessage> = Vec::new();
while let Some(new_message) = client.wait_for_messages().await {
if new_message.is_empty() {
continue;
}
message = new_message;
break;
}
let mut parsed = String::new();
if let Some(r) = message.first() {
parsed = String::from_utf8(r.message.clone()).unwrap();
}
// parse sender_tag: we will use this to reply to sender without needing their Nym address
let return_recipient: AnonymousSenderTag = message[0].sender_tag.unwrap();
println!(
"\nReceived the following message: {} \nfrom sender with surb bucket {}",
parsed, return_recipient
);
// reply to self with it: note we use `send_str_reply` instead of `send_str`
println!("Replying with using SURBs");
client
.send_reply(return_recipient, "hi an0n!")
.await
.unwrap();
println!("Waiting for message (once you see it, ctrl-c to exit)\n");
client
.on_messages(|msg| println!("\nReceived: {}", String::from_utf8_lossy(&msg.message)))
.await;
}
```