use nym_client_core::client::base_client::storage::gateways_storage::{
GatewayDetails, GatewaysDetailsStore, InMemGatewaysDetails,
};
use nym_client_core::client::base_client::storage::{Ephemeral, MixnetClientStorage};
use nym_client_core::client::key_manager::persistence::{InMemEphemeralKeys, KeyStore};
use nym_client_core::client::replies::reply_storage;
use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
use nym_credentials_interface::TicketType;
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
use rand::rngs::OsRng;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
nym_bin_common::logging::setup_tracing_logger();
let sandbox_network = mixnet::NymNetworkDetails::new_from_env();
println!("=== Phase 1: Client-A registers with a gateway ===\n");
let storage_a = Ephemeral::default();
let keys_store_a = storage_a.key_store().clone();
let gw_store_a = storage_a.gateway_details_store().clone();
let client_a_builder = mixnet::MixnetClientBuilder::new_with_storage(storage_a)
.network_details(sandbox_network.clone())
.enable_credentials_mode()
.build()?;
let mnemonic = std::env::var("SANDBOX_MNEMONIC").expect("Set mnemonic in the env variable");
let bandwidth_client_a = client_a_builder
.create_bandwidth_client(mnemonic, TicketType::V1MixnetEntry)
.await?;
bandwidth_client_a.acquire().await?;
let mut client_a = client_a_builder.connect_to_mixnet().await?;
let address_a = *client_a.nym_address();
println!("Client-A address: {address_a}");
let client_keys = keys_store_a.load_keys().await?;
let active_gw = gw_store_a.active_gateway().await?;
let registration = active_gw
.registration
.expect("client-A should have an active gateway registration");
let shared_key_bytes = registration
.details
.shared_key()
.expect("remote gateway should have a shared key")
.to_bytes();
let gateway_id = registration.details.gateway_id();
let published_data = match ®istration.details {
GatewayDetails::Remote(r) => r.published_data.clone(),
_ => panic!("expected remote gateway"),
};
println!("Gateway ID: {}", gateway_id.to_base58_string());
println!(
"Shared key (first 8 bytes): {:02x?}",
&shared_key_bytes[..8]
);
println!("Gateway listener: {}", published_data.listeners.primary);
client_a
.send_plain_message(address_a, "hello from client-A")
.await?;
println!("\nClient-A sent self-ping...");
if let Some(msgs) = client_a.wait_for_messages().await {
for m in &msgs {
println!("Client-A received: {}", String::from_utf8_lossy(&m.message));
}
}
client_a.disconnect().await;
println!("\nClient-A disconnected (gateway keeps registration)\n");
println!("=== Phase 2: Client-B reuses Client-A's identity + registration ===\n");
let shared_key = SharedSymmetricKey::try_from_bytes(&shared_key_bytes)?;
let gw_details = GatewayDetails::new_remote(gateway_id, Arc::new(shared_key), published_data);
let gw_registration = gw_details.into();
let keys_store_b = InMemEphemeralKeys::new(&mut OsRng);
keys_store_b.store_keys(&client_keys).await?;
let gw_store_b = InMemGatewaysDetails::default();
gw_store_b.store_gateway_details(&gw_registration).await?;
gw_store_b
.set_active_gateway(&gateway_id.to_base58_string())
.await?;
let storage_b = PrePopulatedStorage {
key_store: keys_store_b,
reply_store: reply_storage::Empty::default(),
credential_store: EphemeralCredentialStorage::default(),
gateway_details_store: gw_store_b,
};
let client_b_builder = mixnet::MixnetClientBuilder::new_with_storage(storage_b)
.request_gateway(gateway_id.to_base58_string())
.network_details(sandbox_network)
.enable_credentials_mode()
.build()?;
println!("Client-B connecting (should authenticate, not register)...");
let mut client_b = client_b_builder.connect_to_mixnet().await?;
let address_b = *client_b.nym_address();
println!("Client-B address: {address_b}");
if address_a == address_b {
println!("\n*** SUCCESS: Both clients have the SAME Nym address ***");
println!("*** Client-B authenticated using Client-A's shared key ***");
} else {
println!("\n*** UNEXPECTED: Addresses differ ***");
println!(" A: {address_a}");
println!(" B: {address_b}");
}
client_b
.send_plain_message(address_b, "hello from client-B")
.await?;
println!("\nClient-B sent self-ping...");
if let Some(msgs) = client_b.wait_for_messages().await {
for m in &msgs {
println!("Client-B received: {}", String::from_utf8_lossy(&m.message));
}
}
client_b.disconnect().await;
println!("\nClient-B disconnected. Done!");
Ok(())
}
#[derive(Clone)]
struct PrePopulatedStorage {
key_store: InMemEphemeralKeys,
reply_store: reply_storage::Empty,
credential_store: EphemeralCredentialStorage,
gateway_details_store: InMemGatewaysDetails,
}
impl MixnetClientStorage for PrePopulatedStorage {
type KeyStore = InMemEphemeralKeys;
type ReplyStore = reply_storage::Empty;
type CredentialStore = EphemeralCredentialStorage;
type GatewaysDetailsStore = InMemGatewaysDetails;
fn into_runtime_stores(
self,
) -> (
Self::ReplyStore,
Self::CredentialStore,
Self::GatewaysDetailsStore,
) {
(
self.reply_store,
self.credential_store,
self.gateway_details_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
}
}