Merge pull request #6042 from nymtech/release/2025.16-halloumi

Merge release/2025.16-halloumi to master
This commit is contained in:
benedetta davico
2025-09-17 14:20:19 +02:00
committed by GitHub
212 changed files with 8325 additions and 3103 deletions
+7 -7
View File
@@ -38,7 +38,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ arc-ubuntu-22.04, custom-windows-11, custom-macos-15 ]
os: [ arc-linux-latest, custom-windows-11, custom-macos-15 ]
runs-on: ${{ matrix.os }}
env:
CARGO_TERM_COLOR: always
@@ -46,9 +46,9 @@ jobs:
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler cmake
continue-on-error: true
if: contains(matrix.os, 'ubuntu')
if: contains(matrix.os, 'linux')
- name: Check out repository code
uses: actions/checkout@v4
@@ -63,7 +63,7 @@ jobs:
# To avoid running out of disk space, skip generating debug symbols
- name: Set debug to false (unix)
if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'mac')
if: contains(matrix.os, 'linux') || contains(matrix.os, 'mac')
run: |
sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml
git diff
@@ -93,14 +93,14 @@ jobs:
command: build
- name: Build all examples
if: contains(matrix.os, 'ubuntu')
if: contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --examples
- name: Run all tests
if: contains(matrix.os, 'ubuntu')
if: contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
env:
NYM_API: https://sandbox-nym-api1.nymtech.net/api
@@ -109,7 +109,7 @@ jobs:
args: --workspace
- name: Run expensive tests
if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'ubuntu')
if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
with:
command: test
+1 -5
View File
@@ -30,13 +30,11 @@ jobs:
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
client_hash: ${{ steps.binary-hashes.outputs.client_hash }}
nymvisor_hash: ${{ steps.binary-hashes.outputs.nymvisor_hash }}
nymnode_hash: ${{ steps.binary-hashes.outputs.nymnode_hash }}
socks5_hash: ${{ steps.binary-hashes.outputs.socks5_hash }}
netreq_hash: ${{ steps.binary-hashes.outputs.netreq_hash }}
cli_hash: ${{ steps.binary-hashes.outputs.cli_hash }}
client_version: ${{ steps.binary-versions.outputs.client_version }}
nymvisor_version: ${{ steps.binary-versions.outputs.nymvisor_version }}
nymnode_version: ${{ steps.binary-versions.outputs.nymnode_version }}
socks5_version: ${{ steps.binary-versions.outputs.socks5_version }}
netreq_version: ${{ steps.binary-versions.outputs.netreq_version }}
cli_version: ${{ steps.binary-versions.outputs.cli_version }}
@@ -56,7 +54,7 @@ jobs:
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: 1.86.0
toolchain: 1.88.0
override: true
- name: Build all binaries
@@ -76,7 +74,6 @@ jobs:
target/release/nym-network-requester
target/release/nym-cli
target/release/nymvisor
target/release/nym-node
retention-days: 30
- id: create-release
@@ -91,7 +88,6 @@ jobs:
target/release/nym-network-requester
target/release/nym-cli
target/release/nymvisor
target/release/nym-node
push-release-data-client:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
@@ -40,7 +40,8 @@ jobs:
- name: Get version from cargo.toml
id: get_version
run: |
yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: cleanup-gateway-probe-ref
id: cleanup_gateway_probe_ref
@@ -52,13 +53,16 @@ jobs:
- name: Set GIT_TAG variable
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
- name: Set RELEASE_TAG variable
- name: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
- name: Set RELEASE_TAG for release
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
- name: New env vars
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
+42
View File
@@ -4,6 +4,48 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.16-halloumi] (2025-09-16)
- Backport metadata endpoint ([#6010])
- bugfix: make sure tables are removed in correct order to not trigger FK constraint issue ([#5987])
- chore: move authenticator into gateway crate ([#5982])
- Fix the ns api ci workflow ([#5981])
- Remove freshness check on testrun submit ([#5977])
- Update sysinfo to the latest ([#5976])
- bugfix: manually calculate per node work on rewarded set changes ([#5972])
- fixing the ci for ns agent ([#5965])
- Feature/testing utils ([#5963])
- bugfix: fix ci-build for linux (and use updated runner) ([#5958])
- chore: updated refs to cheddar rev of nym repo ([#5955])
- http api client adjustment ([#5953])
- chore: fix rust 1.89 clippy issues ([#5944])
- Wireguard metadata client library ([#5943])
- chore: remove unused import ([#5942])
- feat: introduce additional checks when attempting to send to bounded channels ([#5941])
- Move credential verifier in peer controller ([#5938])
- change PK/FK on expiration date signatures tables ([#5934])
- Wireguard private metadata ([#5915])
[#6010]: https://github.com/nymtech/nym/pull/6010
[#5987]: https://github.com/nymtech/nym/pull/5987
[#5982]: https://github.com/nymtech/nym/pull/5982
[#5981]: https://github.com/nymtech/nym/pull/5981
[#5977]: https://github.com/nymtech/nym/pull/5977
[#5976]: https://github.com/nymtech/nym/pull/5976
[#5972]: https://github.com/nymtech/nym/pull/5972
[#5965]: https://github.com/nymtech/nym/pull/5965
[#5963]: https://github.com/nymtech/nym/pull/5963
[#5958]: https://github.com/nymtech/nym/pull/5958
[#5955]: https://github.com/nymtech/nym/pull/5955
[#5953]: https://github.com/nymtech/nym/pull/5953
[#5944]: https://github.com/nymtech/nym/pull/5944
[#5943]: https://github.com/nymtech/nym/pull/5943
[#5942]: https://github.com/nymtech/nym/pull/5942
[#5941]: https://github.com/nymtech/nym/pull/5941
[#5938]: https://github.com/nymtech/nym/pull/5938
[#5934]: https://github.com/nymtech/nym/pull/5934
[#5915]: https://github.com/nymtech/nym/pull/5915
## [2025.15-gruyere] (2025-08-20)
- Migrate strum to 0.27.2 ([#5960])
Generated
+641 -1404
View File
File diff suppressed because it is too large Load Diff
+10 -8
View File
@@ -92,7 +92,7 @@ members = [
"common/socks5/requests",
"common/statistics",
"common/store-cipher",
"common/task",
"common/task", "common/test-utils",
"common/ticketbooks-merkle",
"common/topology",
"common/tun",
@@ -102,7 +102,12 @@ members = [
"common/wasm/storage",
"common/wasm/utils",
"common/wireguard",
"common/wireguard-types", "common/zulip-client",
"common/wireguard-private-metadata/client",
"common/wireguard-private-metadata/server",
"common/wireguard-private-metadata/shared",
"common/wireguard-private-metadata/tests",
"common/wireguard-types",
"common/zulip-client",
"documentation/autodoc",
"gateway",
"nym-api",
@@ -126,7 +131,6 @@ members = [
"sdk/ffi/go",
"sdk/ffi/shared",
"sdk/rust/nym-sdk",
"service-providers/authenticator",
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
@@ -164,7 +168,6 @@ default-members = [
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
"service-providers/authenticator",
"service-providers/ip-packet-router",
"service-providers/network-requester",
"tools/nymvisor",
@@ -320,11 +323,11 @@ si-scale = "0.2.3"
snow = "0.9.6"
sphinx-packet = "=0.6.0"
sqlx = "0.8.6"
strum = "0.26"
strum_macros = "0.26"
strum = "0.27.2"
strum_macros = "0.27.2"
subtle-encoding = "0.5"
syn = "1"
sysinfo = "0.33.0"
sysinfo = "0.37.0"
tap = "1.0.1"
tar = "0.4.44"
tempfile = "3.20"
@@ -450,4 +453,3 @@ exit = "deny"
panic = "deny"
unimplemented = "deny"
unreachable = "deny"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.61"
version = "1.1.62"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.61"
version = "1.1.62"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+2 -2
View File
@@ -207,7 +207,7 @@ where
<St as Storage>::StorageError: Send + Sync + 'static,
{
if let Some(stored) = storage
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await
.map_err(BandwidthControllerError::credential_storage_error)?
{
@@ -220,7 +220,7 @@ where
ecash_apis,
|api| async move {
api.api_client
.global_expiration_date_signatures(Some(expiration_date))
.global_expiration_date_signatures(Some(expiration_date), Some(epoch_id))
.await
},
format!("aggregated coin index signatures for date {expiration_date}"),
@@ -96,35 +96,59 @@ impl MixTrafficController {
mut mix_packets: Vec<MixPacket>,
) -> Result<(), ErasedGatewayError> {
debug_assert!(!mix_packets.is_empty());
let result = if mix_packets.len() == 1 {
let send_future = if mix_packets.len() == 1 {
// SAFETY: we just checked we have one packet
#[allow(clippy::unwrap_used)]
let mix_packet = mix_packets.pop().unwrap();
self.gateway_transceiver.send_mix_packet(mix_packet).await
self.gateway_transceiver.send_mix_packet(mix_packet)
} else {
self.gateway_transceiver
.batch_send_mix_packets(mix_packets)
.await
self.gateway_transceiver.batch_send_mix_packets(mix_packets)
};
if result.is_err() {
self.consecutive_gateway_failure_count += 1;
} else {
trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
self.consecutive_gateway_failure_count = 0;
}
tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown while handling messages");
Ok(())
}
result = send_future => {
if result.is_err() {
self.consecutive_gateway_failure_count += 1;
} else {
trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
self.consecutive_gateway_failure_count = 0;
}
result
result
}
}
}
async fn on_client_request(&mut self, client_request: ClientRequest) {
tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown while handling client request");
}
result = self.gateway_transceiver.send_client_request(client_request) => {
if let Err(err) = result {
error!("Failed to send client request: {err}")
}
}
}
}
pub fn start(mut self) {
spawn_future!(
async move {
debug!("Started MixTrafficController with graceful shutdown support");
while !self.task_client.is_shutdown() {
tokio::select! {
biased;
_ = self.task_client.recv() => {
tracing::trace!("MixTrafficController: Received shutdown");
break;
}
mix_packets = self.mix_rx.recv() => match mix_packets {
Some(mix_packets) => {
if let Err(err) = self.on_messages(mix_packets).await {
@@ -147,23 +171,15 @@ impl MixTrafficController {
},
client_request = self.client_rx.recv() => match client_request {
Some(client_request) => {
match self.gateway_transceiver.send_client_request(client_request).await {
Ok(_) => (),
Err(e) => error!("Failed to send client request: {e}"),
};
self.on_client_request(client_request).await;
},
None => {
tracing::trace!("MixTrafficController, client request channel closed");
}
},
_ = self.task_client.recv() => {
tracing::trace!("MixTrafficController: Received shutdown");
break;
}
}
}
self.task_client.recv_timeout().await;
tracing::debug!("MixTrafficController: Exiting");
},
"MixTrafficController"
@@ -226,6 +226,11 @@ where
while !self.task_client.is_shutdown() {
tokio::select! {
biased;
_ = self.task_client.recv() => {
tracing::trace!("InputMessageListener: Received shutdown");
break;
}
input_msg = self.input_receiver.recv() => match input_msg {
Some(input_msg) => {
self.on_input_message(input_msg).await;
@@ -235,9 +240,7 @@ where
break;
}
},
_ = self.task_client.recv() => {
tracing::trace!("InputMessageListener: Received shutdown");
}
}
}
self.task_client.recv_timeout().await;
@@ -179,6 +179,11 @@ where
while !self.task_client.is_shutdown() {
tokio::select! {
biased;
_ = self.task_client.recv() => {
tracing::trace!("RetransmissionRequestListener: Received shutdown");
break;
}
timed_out_ack = self.request_receiver.next() => match timed_out_ack {
Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await,
None => {
@@ -186,9 +191,7 @@ where
break;
}
},
_ = self.task_client.recv() => {
tracing::trace!("RetransmissionRequestListener: Received shutdown");
}
}
}
self.task_client.recv_timeout().await;
@@ -548,6 +548,7 @@ where
pending_acks.push(pending_ack);
}
drop(topology_permit);
self.insert_pending_acks(pending_acks);
self.forward_messages(real_messages, lane).await;
@@ -730,17 +731,21 @@ where
// tells real message sender (with the poisson timer) to send this to the mix network
pub(crate) async fn forward_messages(
&self,
&mut self,
messages: Vec<RealMessage>,
transmission_lane: TransmissionLane,
) {
if let Err(err) = self
.real_message_sender
.send((messages, transmission_lane))
.await
{
if !self.task_client.is_shutdown_poll() {
error!("Failed to forward messages to the real message sender: {err}");
tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown while attempting to forward mixnet messages");
}
sending_res = self.real_message_sender.send((messages, transmission_lane)) => {
if sending_res.is_err() {
error!(
"failed to forward mixnet messages due to closed channel (outside of shutdown!)"
);
}
}
}
}
@@ -280,17 +280,33 @@ where
}
};
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
if !self.task_client.is_shutdown_poll() {
tracing::error!("Failed to send: {err}");
let sending_res = tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown signal while attempting to send mix message");
return
}
sending_res = self.mix_tx.send(vec![next_message]) => {
sending_res
}
};
match sending_res {
Err(_) => {
if !self.task_client.is_shutdown_poll() {
tracing::error!(
"failed to send mixnet packet due to closed channel (outside of shutdown!)"
);
}
}
Ok(_) => {
let event = if fragment_id.is_some() {
PacketStatisticsEvent::RealPacketSent(packet_size)
} else {
PacketStatisticsEvent::CoverPacketSent(packet_size)
};
self.stats_tx.report(event.into());
}
} else {
let event = if fragment_id.is_some() {
PacketStatisticsEvent::RealPacketSent(packet_size)
} else {
PacketStatisticsEvent::CoverPacketSent(packet_size)
};
self.stats_tx.report(event.into());
}
// notify ack controller about sending our message only after we actually managed to push it
@@ -126,7 +126,7 @@ impl TopologyAccessor {
.map(|p| p.topology.clone())
}
pub async fn current_route_provider(&self) -> Option<RwLockReadGuard<NymRouteProvider>> {
pub async fn current_route_provider(&self) -> Option<RwLockReadGuard<'_, NymRouteProvider>> {
let provider = self.inner.topology.read().await;
if provider.topology.is_empty() {
None
+1 -1
View File
@@ -148,7 +148,7 @@ async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure)
}
async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<G>, ClientCoreError>
async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<'_, G>, ClientCoreError>
where
G: ConnectableGateway,
{
@@ -201,7 +201,7 @@ impl<C, St> GatewayClient<C, St> {
#[cfg(not(target_arch = "wasm32"))]
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
debug!(
"Attemting to establish connection to gateway at: {}",
"Attempting to establish connection to gateway at: {}",
self.gateway_address
);
let (ws_stream, _) = connect_async(
@@ -337,7 +337,7 @@ impl PartiallyDelegatedHandle {
// check if the split stream didn't error out
let receive_res = stream_receiver
.try_recv()
.expect("stream sender was somehow dropped without sending anything!");
.map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?;
if let Some(res) = receive_res {
let _res = res?;
@@ -719,10 +719,11 @@ impl NymApiClient {
pub async fn partial_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<PartialExpirationDateSignatureResponse, ValidatorClientError> {
Ok(self
.nym_api
.partial_expiration_date_signatures(expiration_date)
.partial_expiration_date_signatures(expiration_date, epoch_id)
.await?)
}
@@ -739,10 +740,11 @@ impl NymApiClient {
pub async fn global_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<AggregatedExpirationDateSignatureResponse, ValidatorClientError> {
Ok(self
.nym_api
.global_expiration_date_signatures(expiration_date)
.global_expiration_date_signatures(expiration_date, epoch_id)
.await?)
}
@@ -1103,8 +1103,9 @@ pub trait NymApiClientExt: ApiClient {
async fn partial_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<PartialExpirationDateSignatureResponse, NymAPIError> {
let params = match expiration_date {
let mut params = match expiration_date {
None => Vec::new(),
Some(exp) => vec![(
ecash::EXPIRATION_DATE_PARAM,
@@ -1112,6 +1113,10 @@ pub trait NymApiClientExt: ApiClient {
)],
};
if let Some(epoch_id) = epoch_id {
params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string()));
}
self.get_json(
&[
routes::V1_API_VERSION,
@@ -1148,8 +1153,9 @@ pub trait NymApiClientExt: ApiClient {
async fn global_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<AggregatedExpirationDateSignatureResponse, NymAPIError> {
let params = match expiration_date {
let mut params = match expiration_date {
None => Vec::new(),
Some(exp) => vec![(
ecash::EXPIRATION_DATE_PARAM,
@@ -1157,6 +1163,10 @@ pub trait NymApiClientExt: ApiClient {
)],
};
if let Some(epoch_id) = epoch_id {
params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string()));
}
self.get_json(
&[
routes::V1_API_VERSION,
+1 -1
View File
@@ -86,7 +86,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> {
anyhow!("ticketbook got incorrectly imported - the master verification key is missing")
})?;
let expiration_signatures = persistent_storage
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await?
.ok_or_else(|| {
anyhow!(
@@ -120,7 +120,7 @@ async fn issue_to_file(args: Args, client: SigningClient) -> anyhow::Result<()>
if args.include_expiration_date_signatures {
let signatures = credentials_store
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await?
.ok_or(anyhow!("missing expiration date signatures!"))?;
@@ -241,7 +241,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) {
let node_id = row.node_id.clone().parse::<u32>().unwrap();
let coins: Vec<Coin> = vec![];
undelegation_msgs.push((ExecuteMsg::Undelegate { node_id }, coins));
undelegation_table.add_row(&[row.node_id.clone()]);
undelegation_table.add_row(std::slice::from_ref(&row.node_id));
if row.amount.amount > 0 {
delegation_msgs
@@ -188,7 +188,7 @@ impl<C> ContractTesterBuilder<C> {
*self.app.api()
}
pub fn querier(&self) -> QuerierWrapper {
pub fn querier(&self) -> QuerierWrapper<'_> {
self.app.wrap()
}
}
@@ -57,7 +57,7 @@ pub trait NodeBond {
fn is_unbonding(&self) -> bool;
fn identity(&self) -> IdentityKeyRef;
fn identity(&self) -> IdentityKeyRef<'_>;
fn original_pledge(&self) -> &Coin;
@@ -125,7 +125,7 @@ impl NodeBond for MixNodeBond {
self.is_unbonding
}
fn identity(&self) -> IdentityKeyRef {
fn identity(&self) -> IdentityKeyRef<'_> {
self.identity()
}
@@ -178,7 +178,7 @@ impl NodeBond for NymNodeBond {
self.is_unbonding
}
fn identity(&self) -> IdentityKeyRef {
fn identity(&self) -> IdentityKeyRef<'_> {
self.identity()
}
@@ -58,7 +58,7 @@ impl<'a> PrimaryKey<'a> for Role {
type Suffix = <u8 as PrimaryKey<'a>>::Suffix;
type SuperSuffix = <u8 as PrimaryKey<'a>>::SuperSuffix;
fn key(&self) -> Vec<Key> {
fn key(&self) -> Vec<Key<'_>> {
// I'm not sure why it wasn't possible to delegate the call to
// `(*self as u8).key()` directly...
// I guess because of the `Key::Ref(&'a [u8])` variant?
@@ -86,6 +86,25 @@ impl IntervalRewardParams {
pub fn to_inline_json(&self) -> String {
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
pub fn active_node_work(&self, standby_node_work: Decimal) -> WorkFactor {
self.active_set_work_factor * standby_node_work
}
pub fn standby_node_work(
&self,
rewarded_set_size: Decimal,
standby_set_size: Decimal,
) -> WorkFactor {
let f = self.active_set_work_factor;
let k = rewarded_set_size;
let one = Decimal::one();
// nodes in reserve
let k_r = standby_set_size;
one / (f * k - (f - one) * k_r)
}
}
/// Parameters used for reward calculation.
@@ -109,18 +128,15 @@ pub struct RewardingParams {
impl RewardingParams {
pub fn active_node_work(&self) -> WorkFactor {
self.interval.active_set_work_factor * self.standby_node_work()
let standby_work = self.standby_node_work();
self.interval.active_node_work(standby_work)
}
pub fn standby_node_work(&self) -> WorkFactor {
let f = self.interval.active_set_work_factor;
let k = self.dec_rewarded_set_size();
let one = Decimal::one();
// nodes in reserve
let k_r = self.dec_standby_set_size();
one / (f * k - (f - one) * k_r)
let rewarded_set_size = self.dec_rewarded_set_size();
let standby_set_size = self.dec_standby_set_size();
self.interval
.standby_node_work(rewarded_set_size, standby_set_size)
}
pub fn rewarded_set_size(&self) -> u32 {
@@ -3,6 +3,7 @@
use crate::config_score::{ConfigScoreParams, OutdatedVersionWeights, VersionScoreFormulaParams};
use crate::nym_node::Role;
use crate::reward_params::RewardedSetParams;
use crate::EpochId;
use contracts_common::Percent;
use cosmwasm_schema::cw_serde;
@@ -85,7 +86,11 @@ impl RewardedSet {
}
pub fn rewarded_set_size(&self) -> usize {
self.active_set_size() + self.standby.len()
self.active_set_size() + self.standby_set_size()
}
pub fn standby_set_size(&self) -> usize {
self.standby.len()
}
pub fn get_role(&self, node_id: NodeId) -> Option<Role> {
@@ -110,6 +115,13 @@ impl RewardedSet {
}
None
}
pub fn matches_parameters(&self, params: RewardedSetParams) -> bool {
self.entry_gateways.len() <= params.entry_gateways as usize
&& self.exit_gateways.len() <= params.exit_gateways as usize
&& self.layer1.len() + self.layer2.len() + self.layer3.len() <= params.mixnodes as usize
&& self.standby.len() <= params.standby as usize
}
}
#[cw_serde]
@@ -0,0 +1,123 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
-- 1. add temporary `epoch_id` column
ALTER TABLE pending_issuance
ADD COLUMN epoch_id INTEGER;
-- 2. populate the value
UPDATE pending_issuance
SET epoch_id = (SELECT epoch_id
FROM expiration_date_signatures
WHERE expiration_date_signatures.expiration_date = pending_issuance.expiration_date);
-- 3. create new expiration_date_signatures table (with changed constraints)
CREATE TABLE expiration_date_signatures_new
(
expiration_date DATE NOT NULL,
epoch_id INTEGER NOT NULL,
serialization_revision INTEGER NOT NULL,
-- combined signatures for all tuples issued for given day
serialised_signatures BLOB NOT NULL,
PRIMARY KEY (epoch_id, expiration_date)
);
-- 4. migrate the data
INSERT INTO expiration_date_signatures_new (expiration_date, epoch_id, serialization_revision, serialised_signatures)
SELECT expiration_date, epoch_id, serialization_revision, serialised_signatures
FROM expiration_date_signatures;
-- 5. drop and recreate the table references (due to new FK)
-- 5.1.
-- (data for ticketbooks that have an associated deposit, but failed to get issued)
CREATE TABLE pending_issuance_new
(
deposit_id INTEGER NOT NULL PRIMARY KEY,
-- introduce a way for us to introduce breaking changes in serialization of data
serialization_revision INTEGER NOT NULL,
pending_ticketbook_data BLOB NOT NULL UNIQUE,
-- for each ticketbook we MUST have corresponding expiration date signatures
expiration_date DATE NOT NULL,
epoch_id INTEGER NOT NULL,
-- for each ticketbook we MUST have corresponding expiration date signatures
FOREIGN KEY (epoch_id, expiration_date) REFERENCES expiration_date_signatures_new (epoch_id, expiration_date)
);
INSERT INTO pending_issuance_new (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date,
epoch_id)
SELECT deposit_id, serialization_revision, pending_ticketbook_data, expiration_date, epoch_id
FROM pending_issuance;
-- 5.2.
CREATE TABLE ecash_ticketbook_new
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-- introduce a way for us to introduce breaking changes in serialization of data
serialization_revision INTEGER NOT NULL,
-- the type of the associated ticketbook
ticketbook_type TEXT NOT NULL,
-- the actual crypto data of the ticketbook (wallet, keys, etc.)
ticketbook_data BLOB NOT NULL UNIQUE,
-- for each ticketbook we MUST have corresponding expiration date signatures
expiration_date DATE NOT NULL,
-- for each ticketbook we MUST have corresponding coin index signatures
epoch_id INTEGER NOT NULL,
-- the initial number of tickets the wallet has been created for
total_tickets INTEGER NOT NULL,
-- how many tickets have been used so far (the `l` value of the wallet)
used_tickets INTEGER NOT NULL,
-- FOREIGN KEYS:
-- for each ticketbook we MUST have corresponding coin index signatures
FOREIGN KEY (epoch_id) REFERENCES coin_indices_signatures (epoch_id),
-- for each ticketbook we MUST have corresponding expiration date signatures
FOREIGN KEY (expiration_date, epoch_id) REFERENCES expiration_date_signatures_new (expiration_date, epoch_id)
);
INSERT INTO ecash_ticketbook_new (id, serialization_revision, ticketbook_type, ticketbook_data, expiration_date,
epoch_id, total_tickets, used_tickets)
SELECT id,
serialization_revision,
ticketbook_type,
ticketbook_data,
expiration_date,
epoch_id,
total_tickets,
used_tickets
FROM ecash_ticketbook;
-- 6. finally swap out the old tables
-- drop old tables
DROP TABLE pending_issuance;
DROP TABLE ecash_ticketbook;
DROP TABLE expiration_date_signatures;
-- rename new tables
ALTER TABLE pending_issuance_new
RENAME TO pending_issuance;
ALTER TABLE ecash_ticketbook_new
RENAME TO ecash_ticketbook;
ALTER TABLE expiration_date_signatures_new
RENAME TO expiration_date_signatures;
@@ -28,7 +28,7 @@ struct EcashCredentialManagerInner {
pending: HashMap<i64, RetrievedPendingTicketbook>,
master_vk: HashMap<u64, VerificationKeyAuth>,
coin_indices_sigs: HashMap<u64, Vec<AnnotatedCoinIndexSignature>>,
expiration_date_sigs: HashMap<Date, Vec<AnnotatedExpirationDateSignature>>,
expiration_date_sigs: HashMap<(u64, Date), Vec<AnnotatedExpirationDateSignature>>,
_next_id: i64,
}
@@ -242,10 +242,14 @@ impl MemoryEcachTicketbookManager {
pub(crate) async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Option<Vec<AnnotatedExpirationDateSignature>> {
let guard = self.inner.read().await;
guard.expiration_date_sigs.get(&expiration_date).cloned()
guard
.expiration_date_sigs
.get(&(epoch_id, expiration_date))
.cloned()
}
pub(crate) async fn insert_expiration_date_signatures(
@@ -254,8 +258,9 @@ impl MemoryEcachTicketbookManager {
) {
let mut guard = self.inner.write().await;
guard
.expiration_date_sigs
.insert(sigs.expiration_date, sigs.signatures.clone());
guard.expiration_date_sigs.insert(
(sigs.epoch_id, sigs.expiration_date),
sigs.signatures.clone(),
);
}
}
@@ -39,7 +39,7 @@ impl SqliteEcashTicketbookManager {
Ok(())
}
pub(crate) async fn begin_storage_tx(&self) -> Result<Transaction<Sqlite>, sqlx::Error> {
pub(crate) async fn begin_storage_tx(&self) -> Result<Transaction<'_, Sqlite>, sqlx::Error> {
self.connection_pool.begin().await
}
@@ -260,15 +260,17 @@ impl SqliteEcashTicketbookManager {
pub(crate) async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: i64,
) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error> {
sqlx::query_as!(
RawExpirationDateSignatures,
r#"
SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8"
SELECT serialised_signatures, serialization_revision as "serialization_revision: u8"
FROM expiration_date_signatures
WHERE expiration_date = ?
WHERE expiration_date = ? AND epoch_id = ?
"#,
expiration_date
expiration_date,
epoch_id
)
.fetch_optional(&*self.connection_pool)
.await
@@ -166,10 +166,11 @@ impl Storage for EphemeralStorage {
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
Ok(self
.storage_manager
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await)
}
-1
View File
@@ -60,7 +60,6 @@ pub struct StoredPendingTicketbook {
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct RawExpirationDateSignatures {
pub epoch_id: u32,
pub serialised_signatures: Vec<u8>,
pub serialization_revision: u8,
}
@@ -325,10 +325,11 @@ impl Storage for PersistentStorage {
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
let Some(raw) = self
.storage_manager
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id as i64)
.await?
else {
return Ok(None);
+1
View File
@@ -92,6 +92,7 @@ pub trait Storage: Clone + Send + Sync {
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError>;
async fn insert_expiration_date_signatures(
@@ -39,7 +39,7 @@ impl traits::EcashManager for EcashManager {
async fn verification_key(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
self.shared_state.verification_key(epoch_id).await
}
@@ -231,7 +231,7 @@ impl traits::EcashManager for MockEcashManager {
async fn verification_key(
&self,
_epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
Ok(self.verfication_key.read().await)
}
@@ -125,7 +125,7 @@ impl SharedState {
async fn set_epoch_data(
&self,
epoch_id: EpochId,
) -> Result<RwLockWriteGuard<BTreeMap<EpochId, EpochState>>, EcashTicketError> {
) -> Result<RwLockWriteGuard<'_, BTreeMap<EpochId, EpochState>>, EcashTicketError> {
let Some(threshold) = self.threshold(epoch_id).await? else {
return Err(EcashTicketError::DKGThresholdUnavailable { epoch_id });
};
@@ -186,7 +186,7 @@ impl SharedState {
pub(crate) async fn api_clients(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<Vec<EcashApiClient>>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, Vec<EcashApiClient>>, EcashTicketError> {
let guard = self.epoch_data.read().await;
// the key was already in the map
@@ -212,7 +212,7 @@ impl SharedState {
pub(crate) async fn verification_key(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
let guard = self.epoch_data.read().await;
// the key was already in the map
@@ -235,11 +235,11 @@ impl SharedState {
}))
}
pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<DirectSigningHttpRpcNyxdClient> {
pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.nyxd_client.write().await
}
pub(crate) async fn start_query(&self) -> RwLockReadGuard<DirectSigningHttpRpcNyxdClient> {
pub(crate) async fn start_query(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.nyxd_client.read().await
}
@@ -12,7 +12,7 @@ pub trait EcashManager {
async fn verification_key(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError>;
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError>;
fn storage(&self) -> Box<dyn BandwidthGatewayStorage + Send + Sync>;
async fn check_payment(
&self,
+16
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::ecash::traits::EcashManager;
use async_trait::async_trait;
use bandwidth_storage_manager::BandwidthStorageManager;
use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime};
use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType};
@@ -139,3 +140,18 @@ impl CredentialVerifier {
.await)
}
}
#[async_trait]
pub trait TicketVerifier {
/// Verify that the ticket is valid and cryptographically correct.
/// If the verification succeeds, also increase the bandwidth with the ticket's
/// amount and return the latest available bandwidth
async fn verify(&mut self) -> Result<i64>;
}
#[async_trait]
impl TicketVerifier for CredentialVerifier {
async fn verify(&mut self) -> Result<i64> {
self.verify().await
}
}
+1
View File
@@ -15,6 +15,7 @@ bls12_381 = { workspace = true, default-features = false }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
strum = { workspace = true, features = ["derive"] }
strum_macros = { workspace = true }
time = { workspace = true, features = ["serde"] }
utoipa = { workspace = true }
rand = { workspace = true }
+3 -3
View File
@@ -229,9 +229,9 @@ impl From<PayInfo> for NymPayInfo {
Serialize,
Deserialize,
Hash,
strum::Display,
strum::EnumString,
strum::EnumIter,
strum_macros::Display,
strum_macros::EnumString,
strum_macros::EnumIter,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
+1 -1
View File
@@ -51,7 +51,7 @@ pub async fn obtain_expiration_date_signatures(
for ecash_api_client in ecash_api_clients.iter() {
match ecash_api_client
.api_client
.partial_expiration_date_signatures(None)
.partial_expiration_date_signatures(None, None)
.await
{
Ok(signature) => {
+3
View File
@@ -47,4 +47,7 @@ workspace = true
default-features = false
[dev-dependencies]
anyhow = { workspace = true }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests
nym-test-utils = { path = "../test-utils" }
tokio = { workspace = true, features = ["full"] }
@@ -109,3 +109,85 @@ GATEWAY -> CLIENT
DONE(status)
*/
#[cfg(test)]
mod tests {
use super::*;
use crate::ClientControlRequest;
use futures::StreamExt;
use nym_test_utils::helpers::u64_seeded_rng;
use nym_test_utils::mocks::stream_sink::mock_streams;
use nym_test_utils::traits::{Leak, Timeboxed, TimeboxedSpawnable};
use tokio::join;
use tungstenite::Message;
#[tokio::test]
async fn basic_handshake() -> anyhow::Result<()> {
use anyhow::Context as _;
// solve the lifetime issue by just leaking the contents of the boxes
// which is perfectly fine in test
let client_rng = u64_seeded_rng(42).leak();
let gateway_rng = u64_seeded_rng(69).leak();
let client_keys = ed25519::KeyPair::new(client_rng).leak();
let gateway_keys = ed25519::KeyPair::new(gateway_rng).leak();
let (client_ws, gateway_ws) = mock_streams::<Message>();
// we need streams that return Result<Message, WsError>
let client_ws = client_ws.map(Ok);
let gateway_ws = gateway_ws.map(Ok);
let client_ws = client_ws.leak();
let gateway_ws = gateway_ws.leak();
let handshake_client = client_handshake(
client_rng,
client_ws,
client_keys,
*gateway_keys.public_key(),
false,
true,
TaskClient::dummy(),
);
let client_fut = handshake_client.spawn_timeboxed();
// we need to receive the first message so that it could be propagated to the gateway side of the handshake
let ClientControlRequest::RegisterHandshakeInitRequest {
protocol_version: _,
data,
} = (gateway_ws.next())
.timeboxed()
.await
.context("timeout")?
.context("no message!")??
.into_text()?
.parse::<ClientControlRequest>()?
else {
panic!("bad message")
};
let init_msg = data;
let handshake_gateway = gateway_handshake(
gateway_rng,
gateway_ws,
gateway_keys,
init_msg,
TaskClient::dummy(),
);
let gateway_fut = handshake_gateway.spawn_timeboxed();
let (client, gateway) = join!(client_fut, gateway_fut);
let client_key = client???;
let gateway_key = gateway???;
// ensure the created keys are the same
assert_eq!(client_key, gateway_key);
Ok(())
}
}
+6
View File
@@ -136,6 +136,7 @@
//! ```
#![warn(missing_docs)]
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
pub use reqwest::StatusCode;
use crate::path::RequestPath;
@@ -173,6 +174,10 @@ mod path;
#[cfg(not(target_arch = "wasm32"))]
pub use dns::{HickoryDnsError, HickoryDnsResolver};
// helper for generating user agent based on binary information
#[doc(hidden)]
pub use nym_bin_common::bin_info;
/// Default HTTP request connection timeout.
///
/// The timeout is relatively high as we are often making requests over the mixnet, where latency is
@@ -608,6 +613,7 @@ impl Client {
current_idx: Arc::new(Default::default()),
reqwest_client: self.reqwest_client.clone(),
#[cfg(feature = "tunneling")]
front: self.front.clone(),
retry_limit: self.retry_limit,
+10
View File
@@ -20,6 +20,16 @@ pub struct UserAgent {
pub git_commit: String,
}
/// Create `UserAgent` based on the caller's crate information
// we can't use normal function as then `application` and `version` would correspond
// of that of `nym-http-api-client` lib
#[macro_export]
macro_rules! generate_user_agent {
() => {
$crate::UserAgent::from($crate::bin_info!())
};
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("invalid user agent string: {0}")]
pub struct UserAgentError(String);
+2 -1
View File
@@ -47,7 +47,8 @@ pub mod nyx {
pub mod wireguard {
use std::net::{Ipv4Addr, Ipv6Addr};
pub const WG_PORT: u16 = 51822;
pub const WG_TUNNEL_PORT: u16 = 51822;
pub const WG_METADATA_PORT: u16 = 51830;
// The interface used to route traffic
pub const WG_TUN_BASE_NAME: &str = "nymwg";
+2 -2
View File
@@ -25,8 +25,8 @@ pub enum NymIdError {
#[error("attempted to import an expired credential (it expired on {expiration})")]
ExpiredCredentialImport { expiration: Date },
#[error("could not import ticketbook expiring at {date} since we do not have corresponding expiration date signatures")]
MissingExpirationDateSignatures { date: Date },
#[error("could not import ticketbook expiring at {date} for epoch {epoch_id} since we do not have corresponding expiration date signatures")]
MissingExpirationDateSignatures { date: Date, epoch_id: u64 },
#[error("could not import ticketbook for epoch {epoch_id} since we do not have corresponding coin index signatures")]
MissingCoinIndexSignatures { epoch_id: u64 },
@@ -99,7 +99,7 @@ where
// in order to import the ticketbook we MUST have the appropriate signatures in the storage already
if credentials_store
.get_expiration_date_signatures(ticketbook.expiration_date())
.get_expiration_date_signatures(ticketbook.expiration_date(), ticketbook.epoch_id())
.await
.map_err(|source| NymIdError::StorageError {
source: Box::new(source),
@@ -108,6 +108,7 @@ where
{
return Err(NymIdError::MissingExpirationDateSignatures {
date: ticketbook.expiration_date(),
epoch_id: ticketbook.epoch_id(),
});
}
+2
View File
@@ -14,6 +14,7 @@ pin-project = { workspace = true }
sha2 = { workspace = true }
snow = { workspace = true }
strum = { workspace = true, features = ["derive"] }
strum_macros = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["net", "io-util", "time"] }
tokio-util = { workspace = true, features = ["codec"] }
@@ -27,6 +28,7 @@ anyhow = { workspace = true }
tokio = { workspace = true, features = ["full"] }
rand_chacha = { workspace = true }
nym-crypto = { path = "../crypto", features = ["rand"] }
nym-test-utils = { path = "../test-utils" }
[lints]
+1 -1
View File
@@ -13,7 +13,7 @@ use nym_crypto::asymmetric::x25519;
use nym_noise_keys::{NoiseVersion, VersionedNoiseKey};
use snow::params::NoiseParams;
use strum::{EnumIter, FromRepr};
use strum_macros::{EnumIter, FromRepr};
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
#[repr(u8)]
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::config::NoisePattern;
use crate::error::NoiseError;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use nym_noise_keys::NoiseVersion;
use strum::FromRepr;
use strum_macros::FromRepr;
#[derive(Debug)]
pub struct NymNoiseFrame {
+9 -117
View File
@@ -411,122 +411,21 @@ where
mod tests {
use super::*;
use nym_crypto::asymmetric::x25519;
use rand_chacha::rand_core::SeedableRng;
use std::io::Error;
use std::mem;
use nym_test_utils::helpers::deterministic_rng;
use nym_test_utils::mocks::async_read_write::mock_io_streams;
use nym_test_utils::traits::{Timeboxed, TimeboxedSpawnable};
use std::sync::Arc;
use std::task::{Context, Waker};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::join;
use tokio::sync::Mutex;
use tokio::time::timeout;
fn mock_streams() -> (MockStream, MockStream) {
let ch1 = Arc::new(Mutex::new(Default::default()));
let ch2 = Arc::new(Mutex::new(Default::default()));
(
MockStream {
inner: MockStreamInner {
tx: ch1.clone(),
rx: ch2.clone(),
},
},
MockStream {
inner: MockStreamInner { tx: ch2, rx: ch1 },
},
)
}
struct MockStream {
inner: MockStreamInner,
}
#[allow(dead_code)]
impl MockStream {
fn unchecked_tx_data(&self) -> Vec<u8> {
self.inner.tx.try_lock().unwrap().data.clone()
}
fn unchecked_rx_data(&self) -> Vec<u8> {
self.inner.rx.try_lock().unwrap().data.clone()
}
}
struct MockStreamInner {
tx: Arc<Mutex<DataWrapper>>,
rx: Arc<Mutex<DataWrapper>>,
}
#[derive(Default)]
struct DataWrapper {
data: Vec<u8>,
waker: Option<Waker>,
}
impl AsyncRead for MockStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let mut inner = self.inner.rx.try_lock().unwrap();
let data = mem::take(&mut inner.data);
if data.is_empty() {
inner.waker = Some(cx.waker().clone());
return Poll::Pending;
}
if let Some(waker) = inner.waker.take() {
waker.wake();
}
buf.put_slice(&data);
Poll::Ready(Ok(()))
}
}
impl AsyncWrite for MockStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, Error>> {
let mut inner = self.inner.tx.try_lock().unwrap();
let len = buf.len();
if !inner.data.is_empty() {
assert!(inner.waker.is_none());
inner.waker = Some(cx.waker().clone());
return Poll::Pending;
}
inner.data.extend_from_slice(buf);
if let Some(waker) = inner.waker.take() {
waker.wake();
}
Poll::Ready(Ok(len))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Poll::Ready(Ok(()))
}
}
#[tokio::test]
async fn noise_handshake() -> anyhow::Result<()> {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let mut rng = deterministic_rng();
let initiator_keys = Arc::new(x25519::KeyPair::new(&mut rng));
let responder_keys = Arc::new(x25519::KeyPair::new(&mut rng));
let (initiator_stream, responder_stream) = mock_streams();
let (initiator_stream, responder_stream) = mock_io_streams();
let psk = generate_psk(*responder_keys.public_key(), NoiseVersion::V1)?;
let pattern = NoisePattern::default();
@@ -547,14 +446,8 @@ mod tests {
*responder_keys.public_key(),
);
let initiator_fut =
tokio::spawn(
async move { timeout(Duration::from_millis(200), stream_initiator).await },
);
let responder_fut =
tokio::spawn(
async move { timeout(Duration::from_millis(200), stream_responder).await },
);
let initiator_fut = stream_initiator.spawn_timeboxed();
let responder_fut = stream_responder.spawn_timeboxed();
let (initiator, responder) = join!(initiator_fut, responder_fut);
@@ -563,14 +456,13 @@ mod tests {
let msg = b"hello there";
// if noise was successful we should be able to write a proper message across
timeout(Duration::from_millis(200), initiator.write_all(msg)).await??;
initiator.write_all(msg).timeboxed().await??;
initiator.inner_stream.flush().await?;
let inner_buf = initiator.inner_stream.get_ref().unchecked_tx_data();
let mut buf = [0u8; 11];
timeout(Duration::from_millis(200), responder.read(&mut buf)).await??;
responder.read(&mut buf).timeboxed().await??;
assert_eq!(&buf[..], msg);
+1
View File
@@ -21,6 +21,7 @@ time = { workspace = true }
tokio = { workspace = true }
si-scale = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
nym-crypto = { path = "../crypto" }
nym-sphinx = { path = "../nymsphinx" }
+2 -2
View File
@@ -7,8 +7,8 @@ use serde::{Deserialize, Serialize};
PartialEq,
Copy,
Clone,
strum::Display,
strum::EnumString,
strum_macros::Display,
strum_macros::EnumString,
Serialize,
Deserialize,
Default,
+2
View File
@@ -10,6 +10,7 @@ where
}
#[cfg(not(target_arch = "wasm32"))]
#[track_caller]
pub fn spawn<F>(future: F)
where
F: Future + Send + 'static,
@@ -18,6 +19,7 @@ where
tokio::spawn(future);
}
#[track_caller]
pub fn spawn_with_report_error<F, T, E>(future: F, mut shutdown: TaskClient)
where
F: Future<Output = Result<T, E>> + Send + 'static,
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "nym-test-utils"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
[dependencies]
anyhow = { workspace = true }
futures = { workspace = true }
rand_chacha = { workspace = true }
tokio = { workspace = true, features = ["sync", "time", "rt"] }
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
[lints]
workspace = true
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::traits::Timeboxed;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use std::future::Future;
use tokio::task::JoinHandle;
use tokio::time::error::Elapsed;
pub fn leak<T>(val: T) -> &'static mut T {
Box::leak(Box::new(val))
}
pub fn spawn_timeboxed<F>(fut: F) -> JoinHandle<Result<F::Output, Elapsed>>
where
F: Future + Send + 'static,
<F as Future>::Output: Send,
{
tokio::spawn(async move { fut.timeboxed().await })
}
pub fn deterministic_rng() -> ChaCha20Rng {
seeded_rng([42u8; 32])
}
pub fn seeded_rng(seed: [u8; 32]) -> ChaCha20Rng {
ChaCha20Rng::from_seed(seed)
}
pub fn u64_seeded_rng(seed: u64) -> ChaCha20Rng {
ChaCha20Rng::seed_from_u64(seed)
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod helpers;
pub mod mocks;
pub mod traits;
@@ -0,0 +1,161 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::mocks::shared::InnerWrapper;
use futures::ready;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
// sending buffer of the first stream is the receiving buffer of the second stream
// and vice versa
pub fn mock_io_streams() -> (MockIOStream, MockIOStream) {
let ch1 = MockIOStream::default();
let ch2 = ch1.make_connection();
(ch1, ch2)
}
#[derive(Default)]
pub struct MockIOStream {
// messages to send
tx: InnerWrapper<Vec<u8>>,
// messages to receive
rx: InnerWrapper<Vec<u8>>,
}
impl MockIOStream {
fn make_connection(&self) -> Self {
MockIOStream {
tx: self.rx.cloned_buffer(),
rx: self.tx.cloned_buffer(),
}
}
// unwrap in test code is fine
#[allow(clippy::unwrap_used)]
pub fn unchecked_tx_data(&self) -> Vec<u8> {
self.tx.buffer.try_lock().unwrap().content.clone()
}
// unwrap in test code is fine
#[allow(clippy::unwrap_used)]
pub fn unchecked_rx_data(&self) -> Vec<u8> {
self.rx.buffer.try_lock().unwrap().content.clone()
}
}
impl AsyncRead for MockIOStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
ready!(Pin::new(&mut self.rx).poll_guard_ready(cx));
// SAFETY: guard is ready
#[allow(clippy::unwrap_used)]
let guard = self.rx.guard().unwrap();
let data = guard.take_content();
if data.is_empty() {
// nothing to retrieve - store the waiter so that the sender could trigger it
guard.waker = Some(cx.waker().clone());
// drop the guard so that the sender could actually put messages in
self.rx.transition_to_idle();
return Poll::Pending;
}
// if let Some(waker) = guard.waker.take() {
// waker.wake();
// }
self.rx.transition_to_idle();
buf.put_slice(&data);
Poll::Ready(Ok(()))
}
}
impl AsyncWrite for MockIOStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
// wait until we transition to the locked state
ready!(Pin::new(&mut self.tx).poll_guard_ready(cx));
// SAFETY: guard is ready
#[allow(clippy::unwrap_used)]
let guard = self.tx.guard().unwrap();
let len = buf.len();
guard.content.extend_from_slice(buf);
// TODO: if we wanted the behaviour of always reading everything before writing anything extra
// if !guard.content.is_empty() {
// // sanity check
// assert!(guard.waker.is_none());
// guard.waker = Some(cx.waker().clone());
// self.tx.transition_to_idle();
// return Poll::Pending;
// }
Poll::Ready(Ok(len))
}
fn poll_flush(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let Some(guard) = self.tx.guard() else {
return Poll::Ready(Err(io::Error::other(
"invalid lock state to send/flush messages",
)));
};
if let Some(waker) = guard.waker.take() {
// notify the receiver if it was waiting for messages
waker.wake();
}
// release the guard
self.tx.transition_to_idle();
Poll::Ready(Ok(()))
}
fn poll_shutdown(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
// make sure our guard is always dropped on close
self.tx.transition_to_idle();
Poll::Ready(Ok(()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::test]
async fn basic() {
let (mut stream1, mut stream2) = mock_io_streams();
stream1.write_all(&[1, 2, 3, 4, 5]).await.unwrap();
stream1.flush().await.unwrap();
let mut buf = [0u8; 5];
let read = stream2.read(&mut buf).await.unwrap();
assert_eq!(read, 5);
assert_eq!(&buf[0..5], &[1, 2, 3, 4, 5]);
let mut buf = [0u8; 5];
stream2.write_all(&[6, 7, 8, 9, 10]).await.unwrap();
stream2.flush().await.unwrap();
let read = stream1.read(&mut buf).await.unwrap();
assert_eq!(read, 5);
assert_eq!(&buf[0..5], &[6, 7, 8, 9, 10]);
}
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod async_read_write;
mod shared;
pub mod stream_sink;
+109
View File
@@ -0,0 +1,109 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::future::BoxFuture;
use futures::{ready, FutureExt};
use std::mem;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll, Waker};
use tokio::sync::{Mutex, OwnedMutexGuard};
#[derive(Default)]
pub(crate) struct InnerWrapper<T: 'static> {
pub(crate) buffer: Arc<Mutex<ContentWrapper<T>>>,
lock_state: LockState<T>,
}
impl<T: Send> InnerWrapper<T> {
pub(crate) fn clone_buffer(&self) -> Arc<Mutex<ContentWrapper<T>>> {
Arc::clone(&self.buffer)
}
pub(crate) fn cloned_buffer(&self) -> Self {
assert!(matches!(self.lock_state, LockState::Idle));
InnerWrapper {
buffer: self.clone_buffer(),
lock_state: LockState::Idle,
}
}
// NOTE: it's responsibility of the caller to ensure the guard is released and state transitions to idle!
pub(crate) fn poll_guard_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
match &mut self.lock_state {
LockState::Idle => {
// 1. first try to obtain the guard without locking
let Ok(guard) = self.buffer.clone().try_lock_owned() else {
// 2. if that fails, create the future for obtaining it
self.lock_state =
LockState::TryingToLock(self.buffer.clone().lock_owned().boxed());
return Poll::Pending;
};
// correctly transition to locked state and poll ourselves again
self.lock_state = LockState::Locked(guard);
cx.waker().wake_by_ref();
Poll::Ready(())
}
LockState::TryingToLock(lock_fut) => {
// see if the guard future has resolved, if so, transition to locked state and schedule for another poll
let guard = ready!(lock_fut.as_mut().poll(cx));
self.lock_state = LockState::Locked(guard);
cx.waker().wake_by_ref();
Poll::Pending
}
LockState::Locked(_) => Poll::Ready(()),
}
}
pub(crate) fn guard(&mut self) -> Option<&mut OwnedMutexGuard<ContentWrapper<T>>> {
match &mut self.lock_state {
LockState::Locked(guard) => Some(guard),
_ => None,
}
}
pub(crate) fn transition_to_idle(&mut self) {
self.lock_state = LockState::Idle
}
}
#[derive(Default)]
pub(crate) enum LockState<T> {
// We havent started locking yet
#[default]
Idle,
// Waiting for the mutex lock future to resolve
TryingToLock(BoxFuture<'static, OwnedMutexGuard<ContentWrapper<T>>>),
// We hold the mutex guard
Locked(OwnedMutexGuard<ContentWrapper<T>>),
}
#[derive(Default)]
pub struct ContentWrapper<T> {
pub(crate) content: T,
pub(crate) waker: Option<Waker>,
}
impl<T> ContentWrapper<T> {
pub fn into_content(self) -> T {
self.content
}
pub fn content(&self) -> &T {
&self.content
}
pub(crate) fn take_content(&mut self) -> T
where
T: Default,
{
mem::take(&mut self.content)
}
}
impl<T> LockState<T> {}
+181
View File
@@ -0,0 +1,181 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::mocks::shared::{ContentWrapper, InnerWrapper};
use anyhow::{anyhow, bail};
use futures::{ready, Sink, Stream};
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tokio::sync::Mutex;
// sending buffer of the first stream is the receiving buffer of the second stream
// and vice versa
pub fn mock_streams<T>() -> (MockStream<T>, MockStream<T>)
where
T: Send,
{
let ch1 = MockStream::default();
let ch2 = ch1.make_connection();
(ch1, ch2)
}
pub struct MockStream<T: 'static> {
// messages to send
tx: InnerWrapper<VecDeque<T>>,
// messages to receive
rx: InnerWrapper<VecDeque<T>>,
}
impl<T> MockStream<T> {
pub fn clone_tx_buffer(&self) -> Arc<Mutex<ContentWrapper<VecDeque<T>>>>
where
T: Send,
{
self.tx.clone_buffer()
}
pub fn clone_rx_buffer(&self) -> Arc<Mutex<ContentWrapper<VecDeque<T>>>>
where
T: Send,
{
self.rx.clone_buffer()
}
fn make_connection(&self) -> Self
where
T: Send,
{
MockStream {
tx: self.rx.cloned_buffer(),
rx: self.tx.cloned_buffer(),
}
}
}
impl<T> Default for MockStream<T> {
fn default() -> Self {
MockStream {
tx: InnerWrapper::default(),
rx: InnerWrapper::default(),
}
}
}
impl<T> Stream for MockStream<T>
where
T: Send,
{
type Item = T;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
ready!(Pin::new(&mut self.rx).poll_guard_ready(cx));
// SAFETY: guard is ready
#[allow(clippy::unwrap_used)]
let guard = self.rx.guard().unwrap();
let Some(next) = guard.content.pop_front() else {
// nothing to retrieve - store the waiter so that the sender could trigger it
guard.waker = Some(cx.waker().clone());
// drop the guard so that the sender could actually put messages in
self.rx.transition_to_idle();
return Poll::Pending;
};
// there are more messages buffered waiting for us to retrieve
// keep the guard!
if !guard.content.is_empty() {
cx.waker().wake_by_ref();
} else {
// no more messages, drop the guard
self.rx.transition_to_idle();
}
Poll::Ready(Some(next))
}
fn size_hint(&self) -> (usize, Option<usize>) {
// that's just a minor optimisation, so don't sweat about it too much,
// if we can obtain the mutex, give precise information, otherwise return default values
let Ok(guard) = self.rx.buffer.try_lock() else {
return (0, None);
};
let items = guard.content.len();
(items, Some(items))
}
}
impl<T> Sink<T> for MockStream<T>
where
T: Send,
{
type Error = anyhow::Error;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
// wait until we transition to the locked state
ready!(Pin::new(&mut self.tx).poll_guard_ready(cx));
Poll::Ready(Ok(()))
}
fn start_send(mut self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> {
let Some(guard) = self.tx.guard() else {
bail!("invalid lock state to send messages");
};
guard.content.push_back(item);
Ok(())
}
fn poll_flush(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
let Some(guard) = self.tx.guard() else {
return Poll::Ready(Err(anyhow!("invalid lock state to send/flush messages")));
};
if let Some(waker) = guard.waker.take() {
// notify the receiver if it was waiting for messages
waker.wake();
}
// release the guard
self.tx.transition_to_idle();
Poll::Ready(Ok(()))
}
fn poll_close(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
// make sure our guard is always dropped on close
self.tx.transition_to_idle();
Poll::Ready(Ok(()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::{SinkExt, StreamExt};
#[tokio::test]
async fn basic() {
let (mut stream1, mut stream2) = mock_streams();
stream1.send("foomp").await.unwrap();
let received = stream2.next().await.unwrap();
assert_eq!(received, "foomp");
stream2.send("bar").await.unwrap();
let received = stream1.next().await.unwrap();
assert_eq!(received, "bar");
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::helpers::{leak, spawn_timeboxed};
use std::future::{Future, IntoFuture};
use std::time::Duration;
use tokio::task::JoinHandle;
use tokio::time::error::Elapsed;
// a helper trait for use in tests to easily convert `T` into `&'static mut T`
pub trait Leak {
fn leak(self) -> &'static mut Self;
}
impl<T> Leak for T {
fn leak(self) -> &'static mut T {
leak(self)
}
}
// those are internal testing traits so we're not concerned about auto traits
#[allow(async_fn_in_trait)]
pub trait Timeboxed: IntoFuture + Sized {
async fn timeboxed(self) -> Result<Self::Output, Elapsed> {
self.execute_with_deadline(Duration::from_millis(200)).await
}
async fn execute_with_deadline(self, timeout: Duration) -> Result<Self::Output, Elapsed> {
tokio::time::timeout(timeout, self).await
}
}
impl<T> Timeboxed for T where T: IntoFuture + Sized {}
// those are internal testing traits so we're not concerned about auto traits
#[allow(async_fn_in_trait)]
pub trait Spawnable: Future + Sized + Send + 'static {
fn spawn(self) -> JoinHandle<Self::Output>
where
<Self as Future>::Output: Send + 'static,
{
tokio::spawn(self)
}
}
impl<T> Spawnable for T where T: Future + Sized + Send + 'static {}
pub trait TimeboxedSpawnable: Timeboxed + Spawnable {
fn spawn_timeboxed(self) -> JoinHandle<Result<<Self as Future>::Output, Elapsed>>
where
<Self as Future>::Output: Send,
{
spawn_timeboxed(self)
}
}
impl<T> TimeboxedSpawnable for T where T: Spawnable + Future + Send {}
+1
View File
@@ -19,6 +19,7 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
strum = { workspace = true, features = ["derive"] }
strum_macros = { workspace = true }
thiserror = { workspace = true }
ts-rs = { workspace = true }
url = { workspace = true }
+1 -1
View File
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use strum::{Display, EnumString, VariantNames};
use strum_macros::{Display, EnumString, VariantNames};
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
@@ -0,0 +1,18 @@
[package]
name = "nym-wireguard-private-metadata-client"
version = "1.0.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
async-trait = { workspace = true }
tracing = { workspace = true }
nym-http-api-client = { path = "../../http-api-client" }
nym-wireguard-private-metadata-shared = { path = "../shared" }
[lints]
workspace = true
@@ -0,0 +1,58 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use tracing::instrument;
use nym_http_api_client::{ApiClient, Client, HttpClientError, NO_PARAMS};
use nym_wireguard_private_metadata_shared::{
routes, Version, {ErrorResponse, Request, Response},
};
pub type WireguardMetadataApiClientError = HttpClientError<ErrorResponse>;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait WireguardMetadataApiClient: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn version(&self) -> Result<Version, WireguardMetadataApiClientError> {
let version: u64 = self
.get_json(
&[routes::V1_API_VERSION, routes::BANDWIDTH, routes::VERSION],
NO_PARAMS,
)
.await?;
Ok(version.into())
}
#[instrument(level = "debug", skip(self))]
async fn available_bandwidth(
&self,
request_body: &Request,
) -> Result<Response, WireguardMetadataApiClientError> {
self.post_json(
&[routes::V1_API_VERSION, routes::BANDWIDTH, routes::AVAILABLE],
NO_PARAMS,
request_body,
)
.await
}
#[instrument(level = "debug", skip(self, request_body))]
async fn topup_bandwidth(
&self,
request_body: &Request,
) -> Result<Response, WireguardMetadataApiClientError> {
self.post_json(
&[routes::V1_API_VERSION, routes::BANDWIDTH, routes::TOPUP],
NO_PARAMS,
request_body,
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl WireguardMetadataApiClient for Client {}
@@ -0,0 +1,43 @@
[package]
name = "nym-wireguard-private-metadata-server"
version = "1.0.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
anyhow = { workspace = true }
axum = { workspace = true, features = ["tokio", "macros"] }
futures = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] }
tokio-util = { workspace = true }
tower-http = { workspace = true, features = [
"cors",
"trace",
"compression-br",
"compression-deflate",
"compression-gzip",
"compression-zstd",
] }
utoipa = { workspace = true, features = ["axum_extras", "time"] }
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
nym-credentials-interface = { path = "../../credentials-interface" }
nym-credential-verification = { path = "../../credential-verification" }
nym-http-api-common = { path = "../../http-api-common", features = [
"middleware",
"utoipa",
"output",
] }
nym-wireguard = { path = "../../wireguard" }
nym-wireguard-private-metadata-shared = { path = "../shared" }
[dev-dependencies]
async-trait = { workspace = true }
[lints]
workspace = true
@@ -0,0 +1,46 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::sync::Arc;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use nym_wireguard::WgApiWrapper;
pub(crate) mod openapi;
pub(crate) mod router;
pub(crate) mod state;
/// Shutdown goes 2 directions:
/// 1. signal background tasks to gracefully finish
/// 2. signal server itself
///
/// These are done through separate shutdown handles. Of course, shut down server
/// AFTER you have shut down BG tasks (or past their grace period).
#[allow(unused)]
pub struct ShutdownHandles {
axum_shutdown_button: CancellationToken,
/// Tokio JoinHandle for axum server's task
axum_join_handle: AxumJoinHandle,
/// Wireguard API for kernel interactions
wg_api: Arc<WgApiWrapper>,
}
impl ShutdownHandles {
/// Cancellation token is given to Axum server constructor. When the token
/// receives a shutdown signal, Axum server will shut down gracefully.
pub fn new(
axum_join_handle: AxumJoinHandle,
wg_api: Arc<WgApiWrapper>,
axum_shutdown_button: CancellationToken,
) -> Self {
Self {
axum_shutdown_button,
axum_join_handle,
wg_api,
}
}
}
type AxumJoinHandle = JoinHandle<std::io::Result<()>>;
@@ -0,0 +1,14 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use utoipa::OpenApi;
use nym_wireguard_private_metadata_shared::{Request, Response};
#[derive(OpenApi)]
#[openapi(
info(title = "Nym Wireguard Private Metadata"),
tags(),
components(schemas(Request, Response))
)]
pub(crate) struct ApiDoc;
@@ -0,0 +1,101 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use anyhow::anyhow;
use axum::response::Redirect;
use axum::routing::get;
use axum::Router;
use core::net::SocketAddr;
use nym_http_api_common::middleware::logging::log_request_info;
use tokio::net::TcpListener;
use tokio_util::sync::WaitForCancellationFutureOwned;
use tower_http::cors::CorsLayer;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use crate::http::openapi::ApiDoc;
use crate::http::state::AppState;
use crate::network::bandwidth_routes;
/// Wrapper around `axum::Router` which ensures correct [order of layers][order].
/// Add new routes as if you were working directly with `axum`.
///
/// Why? Middleware like logger, CORS, TLS which need to handle request before other
/// layers should be added last. Using this builder pattern ensures that.
///
/// [order]: https://docs.rs/axum/latest/axum/middleware/index.html#ordering
pub struct RouterBuilder {
unfinished_router: Router<AppState>,
}
impl RouterBuilder {
/// All routes should be, if possible, added here. Exceptions are e.g.
/// routes which are added conditionally in other places based on some `if`.
pub fn with_default_routes() -> Self {
let default_routes = Router::new()
.merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi()))
.route("/", get(|| async { Redirect::to("/swagger") }))
.nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes()));
Self {
unfinished_router: default_routes,
}
}
/// Invoke this as late as possible before constructing HTTP server
/// (after all routes were added).
pub fn with_state(self, state: AppState) -> RouterWithState {
RouterWithState {
router: self.finalize_routes().with_state(state),
}
}
/// Middleware added here intercepts the request before it gets to other routes.
fn finalize_routes(self) -> Router<AppState> {
self.unfinished_router
.layer(setup_cors())
.layer(axum::middleware::from_fn(log_request_info))
}
}
fn setup_cors() -> CorsLayer {
CorsLayer::new()
.allow_origin(tower_http::cors::Any)
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
.allow_headers(tower_http::cors::Any)
.allow_credentials(false)
}
pub struct RouterWithState {
pub router: Router,
}
impl RouterWithState {
pub async fn build_server(self, bind_address: &SocketAddr) -> anyhow::Result<ApiHttpServer> {
let listener = tokio::net::TcpListener::bind(bind_address)
.await
.map_err(|err| anyhow!("Couldn't bind to address {} due to {}", bind_address, err))?;
Ok(ApiHttpServer {
router: self.router,
listener,
})
}
}
pub struct ApiHttpServer {
router: Router,
listener: TcpListener,
}
impl ApiHttpServer {
pub async fn run(self, receiver: WaitForCancellationFutureOwned) -> Result<(), std::io::Error> {
// into_make_service_with_connect_info allows us to see client ip address
axum::serve(
self.listener,
self.router
.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(receiver)
.await
}
}
@@ -0,0 +1,35 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::net::IpAddr;
use nym_credentials_interface::CredentialSpendingData;
use crate::transceiver::PeerControllerTransceiver;
use nym_wireguard_private_metadata_shared::error::MetadataError;
#[derive(Clone, axum::extract::FromRef)]
pub struct AppState {
transceiver: PeerControllerTransceiver,
}
impl AppState {
pub fn new(transceiver: PeerControllerTransceiver) -> Self {
Self { transceiver }
}
pub async fn available_bandwidth(&self, ip: IpAddr) -> Result<i64, MetadataError> {
self.transceiver.query_bandwidth(ip).await
}
// Top up with a credential and return the afterwards available bandwidth
pub async fn topup_bandwidth(
&self,
ip: IpAddr,
credential: CredentialSpendingData,
) -> Result<i64, MetadataError> {
self.transceiver
.topup_bandwidth(ip, Box::new(credential))
.await
}
}
@@ -0,0 +1,13 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
mod http;
mod network;
mod transceiver;
pub use http::{
router::{ApiHttpServer, RouterBuilder, RouterWithState},
state::AppState,
ShutdownHandles,
};
pub use transceiver::PeerControllerTransceiver;
@@ -0,0 +1,111 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::net::SocketAddr;
use axum::{
extract::{ConnectInfo, Query, State},
Json, Router,
};
use nym_http_api_common::{FormattedResponse, OutputParams};
use nym_wireguard_private_metadata_shared::{
interface::{RequestData, ResponseData},
latest, AxumErrorResponse, AxumResult, Construct, Extract, Request, Response,
};
use tower_http::compression::CompressionLayer;
use crate::http::state::AppState;
pub(crate) fn bandwidth_routes() -> Router<AppState> {
Router::new()
.route("/version", axum::routing::get(version))
.route("/available", axum::routing::post(available_bandwidth))
.route("/topup", axum::routing::post(topup_bandwidth))
.layer(CompressionLayer::new())
}
#[utoipa::path(
tag = "bandwidth",
get,
path = "/v1/bandwidth/version",
responses(
(status = 200, content(
(Response = "application/bincode")
))
),
)]
async fn version(Query(output): Query<OutputParams>) -> AxumResult<FormattedResponse<u64>> {
let output = output.output.unwrap_or_default();
Ok(output.to_response(latest::VERSION.into()))
}
#[utoipa::path(
tag = "bandwidth",
post,
request_body = Request,
path = "/v1/bandwidth/available",
responses(
(status = 200, content(
(Response = "application/bincode")
))
),
)]
async fn available_bandwidth(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
Json(request): Json<Request>,
) -> AxumResult<FormattedResponse<Response>> {
let output = output.output.unwrap_or_default();
let (RequestData::AvailableBandwidth(_), version) =
request.extract().map_err(AxumErrorResponse::bad_request)?
else {
return Err(AxumErrorResponse::bad_request("incorrect request type"));
};
let available_bandwidth = state
.available_bandwidth(addr.ip())
.await
.map_err(AxumErrorResponse::bad_request)?;
let response = Response::construct(
ResponseData::AvailableBandwidth(available_bandwidth),
version,
)
.map_err(AxumErrorResponse::bad_request)?;
Ok(output.to_response(response))
}
#[utoipa::path(
tag = "bandwidth",
post,
request_body = Request,
path = "/v1/bandwidth/topup",
responses(
(status = 200, content(
(Response = "application/bincode")
))
),
)]
async fn topup_bandwidth(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
Json(request): Json<Request>,
) -> AxumResult<FormattedResponse<Response>> {
let output = output.output.unwrap_or_default();
let (RequestData::TopUpBandwidth(credential), version) =
request.extract().map_err(AxumErrorResponse::bad_request)?
else {
return Err(AxumErrorResponse::bad_request("incorrect request type"));
};
let available_bandwidth = state
.topup_bandwidth(addr.ip(), *credential)
.await
.map_err(AxumErrorResponse::bad_request)?;
let response = Response::construct(ResponseData::TopUpBandwidth(available_bandwidth), version)
.map_err(AxumErrorResponse::bad_request)?;
Ok(output.to_response(response))
}
@@ -0,0 +1,307 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::net::IpAddr;
use futures::channel::oneshot;
use tokio::sync::mpsc;
use nym_credential_verification::ClientBandwidth;
use nym_credentials_interface::CredentialSpendingData;
use nym_wireguard::peer_controller::PeerControlRequest;
use nym_wireguard_private_metadata_shared::error::MetadataError;
#[derive(Clone)]
pub struct PeerControllerTransceiver {
request_tx: mpsc::Sender<PeerControlRequest>,
}
impl PeerControllerTransceiver {
pub fn new(request_tx: mpsc::Sender<PeerControlRequest>) -> Self {
Self { request_tx }
}
async fn get_client_bandwidth(&self, ip: IpAddr) -> Result<ClientBandwidth, MetadataError> {
let (response_tx, response_rx) = oneshot::channel();
let msg = PeerControlRequest::GetClientBandwidthByIp { ip, response_tx };
self.request_tx
.send(msg)
.await
.map_err(|_| MetadataError::PeerInteractionStopped)?;
response_rx
.await
.map_err(|_| MetadataError::NoResponse)?
.map_err(|err| MetadataError::Unsuccessful {
reason: err.to_string(),
})
}
pub(crate) async fn query_bandwidth(&self, ip: IpAddr) -> Result<i64, MetadataError> {
Ok(self.get_client_bandwidth(ip).await?.available().await)
}
// Top up with a credential and return the afterwards available bandwidth
pub(crate) async fn topup_bandwidth(
&self,
ip: IpAddr,
credential: Box<CredentialSpendingData>,
) -> Result<i64, MetadataError> {
let (response_tx, response_rx) = oneshot::channel();
let msg = PeerControlRequest::GetVerifierByIp {
ip,
credential,
response_tx,
};
self.request_tx
.send(msg)
.await
.map_err(|_| MetadataError::PeerInteractionStopped)?;
let mut verifier = response_rx
.await
.map_err(|_| MetadataError::NoResponse)?
.map_err(|err| MetadataError::Unsuccessful {
reason: err.to_string(),
})?;
let available_bandwidth =
verifier
.verify()
.await
.map_err(|err| MetadataError::CredentialVerification {
message: err.to_string(),
})?;
Ok(available_bandwidth)
}
}
#[cfg(test)]
pub(crate) mod tests {
use nym_credential_verification::TicketVerifier;
use nym_wireguard::CONTROL_CHANNEL_SIZE;
use super::*;
pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [
0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254,
16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139,
154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123,
35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1,
151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43,
90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193,
39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81,
184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195,
196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48,
92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48,
166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186,
19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38,
228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85,
88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241,
85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112,
48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31,
97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203,
71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107,
213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180,
178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1,
96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166,
30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212,
207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71,
223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22,
119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131,
59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119,
240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202,
58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144,
189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33,
30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150,
74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6,
227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85,
15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38,
161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20,
47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48,
135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170,
150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109,
55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249,
87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144,
178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32,
203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184,
96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169,
24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61,
130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82,
108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1,
32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234,
150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241,
203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217,
160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52,
147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81,
70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233,
178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98,
110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69,
131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251,
197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233,
162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106,
83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145,
192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124,
55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0,
0, 0, 0, 0, 0, 1,
];
pub(crate) struct MockVerifier {
ret: i64,
}
impl MockVerifier {
pub(crate) fn new(ret: i64) -> MockVerifier {
Self { ret }
}
}
#[async_trait::async_trait]
impl TicketVerifier for MockVerifier {
async fn verify(&mut self) -> nym_credential_verification::Result<i64> {
Ok(self.ret)
}
}
#[tokio::test]
async fn get_bandwidth() {
let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE);
let transceiver = PeerControllerTransceiver::new(request_tx);
tokio::spawn(async move {
match request_rx.recv().await.unwrap() {
PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => {
response_tx
.send(Ok(ClientBandwidth::new(Default::default())))
.ok();
}
_ => panic!("Not expected"),
}
});
let bw = transceiver
.query_bandwidth("10.0.0.42".parse().unwrap())
.await
.unwrap();
assert_eq!(bw, 0);
}
#[tokio::test]
async fn stop_peer() {
let (request_tx, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE);
let transceiver = PeerControllerTransceiver::new(request_tx);
drop(request_rx);
let err = transceiver
.query_bandwidth("10.0.0.42".parse().unwrap())
.await
.unwrap_err();
assert_eq!(err, MetadataError::PeerInteractionStopped);
}
#[tokio::test]
async fn unresponsive_peer() {
let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE);
let transceiver = PeerControllerTransceiver::new(request_tx);
tokio::spawn(async move {
match request_rx.recv().await.unwrap() {
PeerControlRequest::GetClientBandwidthByIp {
ip: _,
response_tx: _,
} => {}
_ => panic!("Not expected"),
}
});
let err = transceiver
.query_bandwidth("10.0.0.42".parse().unwrap())
.await
.unwrap_err();
assert_eq!(err, MetadataError::NoResponse);
}
#[tokio::test]
async fn unsuccessful_query_bandwidth() {
let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE);
let transceiver = PeerControllerTransceiver::new(request_tx);
tokio::spawn(async move {
match request_rx.recv().await.unwrap() {
PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => {
response_tx
.send(Err(nym_wireguard::error::Error::Internal(
"testing".to_owned(),
)))
.ok();
}
_ => panic!("Not expected"),
}
});
let ret = transceiver
.query_bandwidth("10.0.0.42".parse().unwrap())
.await;
assert!(ret.is_err());
}
#[tokio::test]
async fn topup() {
let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE);
let transceiver = PeerControllerTransceiver::new(request_tx);
let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap();
let verifier_bw = 42;
tokio::spawn(async move {
match request_rx.recv().await.unwrap() {
PeerControlRequest::GetVerifierByIp {
ip: _,
credential: _,
response_tx,
} => {
response_tx
.send(Ok(Box::new(MockVerifier::new(verifier_bw))))
.ok();
}
_ => panic!("Not expected"),
}
});
let bw = transceiver
.topup_bandwidth("10.0.0.42".parse().unwrap(), Box::new(credential))
.await
.unwrap();
assert_eq!(bw, verifier_bw);
}
#[tokio::test]
async fn unsuccessful_topup() {
let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE);
let transceiver = PeerControllerTransceiver::new(request_tx);
let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap();
tokio::spawn(async move {
match request_rx.recv().await.unwrap() {
PeerControlRequest::GetVerifierByIp {
ip: _,
credential: _,
response_tx,
} => {
response_tx
.send(Err(nym_wireguard::error::Error::Internal(
"testing".to_owned(),
)))
.ok();
}
_ => panic!("Not expected"),
}
});
let ret = transceiver
.topup_bandwidth("10.0.0.42".parse().unwrap(), Box::new(credential))
.await;
assert!(ret.is_err());
}
}
@@ -0,0 +1,25 @@
[package]
name = "nym-wireguard-private-metadata-shared"
version = "1.0.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
axum = { workspace = true }
bincode = { workspace = true }
schemars = { workspace = true, features = ["preserve_order"] }
serde = { workspace = true }
thiserror = { workspace = true }
utoipa = { workspace = true }
nym-credentials-interface = { path = "../../credentials-interface" }
[features]
testing = []
[lints]
workspace = true
@@ -0,0 +1,28 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub enum MetadataError {
#[error("peers can't be interacted with anymore")]
PeerInteractionStopped,
#[error("no response received")]
NoResponse,
#[error("query was not successful: {reason}")]
Unsuccessful { reason: String },
#[error("Models error: {message}")]
Models { message: String },
#[error("Credential verification error: {message}")]
CredentialVerification { message: String },
}
impl From<crate::models::error::Error> for MetadataError {
fn from(value: crate::models::error::Error) -> Self {
Self::Models {
message: value.to_string(),
}
}
}
@@ -0,0 +1,20 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod error;
mod models;
pub mod routes;
#[cfg(feature = "testing")]
pub use models::v0;
pub use models::{
error::Error as ModelError, interface, latest, v1, AxumErrorResponse, AxumResult, Construct,
ErrorResponse, Extract, Request, Response, Version,
};
fn make_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
@@ -0,0 +1,30 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::Version;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Bincode(#[from] bincode::Error),
#[error("trying to deserialize from version {source_version:?} into {target_version:?}")]
InvalidVersion {
source_version: Version,
target_version: Version,
},
#[error(
"trying to deserialize from query type {source_query_type} query type {target_query_type}"
)]
InvalidQueryType {
source_query_type: String,
target_query_type: String,
},
#[error("update not possible from {from:?} to {to:?}")]
UpdateNotPossible { from: Version, to: Version },
#[error("downgrade not possible from {from:?} to {to:?}")]
DowngradeNotPossible { from: Version, to: Version },
}
@@ -0,0 +1,145 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credentials_interface::CredentialSpendingData;
#[cfg(feature = "testing")]
use crate::models::v0;
use crate::models::{v1, Construct, Extract, Request, Response, Version};
pub enum RequestData {
AvailableBandwidth(()),
TopUpBandwidth(Box<CredentialSpendingData>),
}
impl From<super::latest::interface::RequestData> for RequestData {
fn from(value: super::latest::interface::RequestData) -> Self {
match value {
super::latest::interface::RequestData::AvailableBandwidth(inner) => {
Self::AvailableBandwidth(inner)
}
super::latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => {
Self::TopUpBandwidth(credential_spending_data)
}
}
}
}
impl From<RequestData> for super::latest::interface::RequestData {
fn from(value: RequestData) -> Self {
match value {
RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner),
RequestData::TopUpBandwidth(credential_spending_data) => {
Self::TopUpBandwidth(credential_spending_data)
}
}
}
}
impl From<super::latest::interface::ResponseData> for ResponseData {
fn from(value: super::latest::interface::ResponseData) -> Self {
match value {
super::latest::interface::ResponseData::AvailableBandwidth(inner) => {
Self::AvailableBandwidth(inner)
}
super::latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => {
Self::TopUpBandwidth(credential_spending_data)
}
}
}
}
impl From<ResponseData> for super::latest::interface::ResponseData {
fn from(value: ResponseData) -> Self {
match value {
ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner),
ResponseData::TopUpBandwidth(credential_spending_data) => {
Self::TopUpBandwidth(credential_spending_data)
}
}
}
}
impl Construct<RequestData> for Request {
fn construct(info: RequestData, version: Version) -> Result<Self, super::error::Error> {
match version {
#[cfg(feature = "testing")]
Version::V0 => {
let translate_info = super::latest::interface::RequestData::from(info);
let downgrade_info = v0::interface::RequestData::try_from(translate_info)?;
let versioned_request = v0::VersionedRequest::construct(downgrade_info, version)?;
Ok(versioned_request.try_into()?)
}
Version::V1 => {
let versioned_request = v1::VersionedRequest::construct(info.into(), version)?;
Ok(versioned_request.try_into()?)
}
}
}
}
impl Extract<RequestData> for Request {
fn extract(&self) -> Result<(RequestData, Version), crate::models::Error> {
match self.version {
#[cfg(feature = "testing")]
super::Version::V0 => {
let versioned_request = v0::VersionedRequest::try_from(self.clone())?;
let (request, version) = versioned_request.extract()?;
let upgrade_request = super::latest::interface::RequestData::try_from(request)?;
Ok((upgrade_request.into(), version))
}
super::Version::V1 => {
let versioned_request = v1::VersionedRequest::try_from(self.clone())?;
let (extracted, version) = versioned_request.extract()?;
Ok((extracted.into(), version))
}
}
}
}
pub enum ResponseData {
AvailableBandwidth(i64),
TopUpBandwidth(i64),
}
impl Construct<ResponseData> for Response {
fn construct(info: ResponseData, version: Version) -> Result<Self, super::error::Error> {
match version {
#[cfg(feature = "testing")]
super::Version::V0 => {
let translate_response = super::latest::interface::ResponseData::from(info);
let downgrade_response = v0::interface::ResponseData::try_from(translate_response)?;
let versioned_response =
v0::VersionedResponse::construct(downgrade_response, version)?;
Ok(versioned_response.try_into()?)
}
Version::V1 => {
let versioned_response = v1::VersionedResponse::construct(info.into(), version)?;
Ok(versioned_response.try_into()?)
}
}
}
}
impl Extract<ResponseData> for Response {
fn extract(&self) -> Result<(ResponseData, Version), super::error::Error> {
match self.version {
#[cfg(feature = "testing")]
super::Version::V0 => {
let versioned_response = v0::VersionedResponse::try_from(self.clone())?;
let (response, version) = versioned_response.extract()?;
let upgrade_response = super::latest::interface::ResponseData::try_from(response)?;
Ok((upgrade_response.into(), version))
}
super::Version::V1 => {
let versioned_response = v1::VersionedResponse::try_from(self.clone())?;
let (extracted, version) = versioned_response.extract()?;
Ok((extracted.into(), version))
}
}
}
}
@@ -0,0 +1,175 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::{Display, Formatter};
use axum::http::StatusCode;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
pub(crate) mod error;
pub mod interface;
#[cfg(feature = "testing")]
pub mod v0; // dummy version, only for filling boilerplate code for update/downgrade and testing
pub mod v1;
pub use v1 as latest;
use crate::models::error::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
pub enum Version {
#[cfg(feature = "testing")]
/// only used for testing purposes, don't include it in your matching arms
V0,
V1,
}
impl From<u64> for Version {
fn from(value: u64) -> Self {
#[cfg(feature = "testing")]
let zero_version = Version::V0;
#[cfg(not(feature = "testing"))]
let zero_version = latest::VERSION;
match value {
0 => zero_version,
1 => Version::V1,
_ => latest::VERSION, // if unknown, it means we're behind, so we can use the latest we know about
}
}
}
impl From<Version> for u64 {
fn from(value: Version) -> Self {
// remember to modify the above match if you're bumping the version
match value {
#[cfg(feature = "testing")]
Version::V0 => 0,
Version::V1 => 1,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct Request {
pub version: Version,
pub(crate) inner: Vec<u8>,
}
#[derive(Clone, Serialize, Deserialize, ToSchema)]
pub struct Response {
pub version: Version,
pub(crate) inner: Vec<u8>,
}
pub trait Extract<T> {
fn extract(&self) -> Result<(T, Version), Error>;
}
pub trait Construct<T>: Sized {
fn construct(info: T, version: Version) -> Result<Self, Error>;
}
pub type AxumResult<T> = Result<T, AxumErrorResponse>;
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
pub struct ErrorResponse {
pub message: String,
}
impl Display for ErrorResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.message.fmt(f)
}
}
pub struct AxumErrorResponse {
message: ErrorResponse,
status: StatusCode,
}
impl AxumErrorResponse {
pub fn bad_request(msg: impl Display) -> Self {
Self {
message: ErrorResponse {
message: msg.to_string(),
},
status: StatusCode::BAD_REQUEST,
}
}
}
impl axum::response::IntoResponse for AxumErrorResponse {
fn into_response(self) -> axum::response::Response {
(self.status, self.message.message.to_string()).into_response()
}
}
mod tests {
#[allow(dead_code)]
pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [
0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254,
16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139,
154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123,
35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1,
151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43,
90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193,
39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81,
184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195,
196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48,
92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48,
166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186,
19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38,
228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85,
88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241,
85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112,
48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31,
97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203,
71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107,
213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180,
178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1,
96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166,
30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212,
207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71,
223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22,
119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131,
59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119,
240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202,
58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144,
189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33,
30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150,
74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6,
227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85,
15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38,
161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20,
47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48,
135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170,
150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109,
55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249,
87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144,
178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32,
203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184,
96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169,
24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61,
130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82,
108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1,
32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234,
150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241,
203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217,
160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52,
147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81,
70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233,
178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98,
110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69,
131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251,
197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233,
162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106,
83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145,
192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124,
55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0,
0, 0, 0, 0, 0, 1,
];
}
@@ -0,0 +1,5 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod request;
pub mod response;
@@ -0,0 +1,86 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bincode::Options;
use serde::{Deserialize, Serialize};
use crate::{make_bincode_serializer, models::Request};
use super::super::{Error, QueryType, VersionedRequest};
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InnerAvailableBandwidthRequest {}
impl TryFrom<VersionedRequest> for InnerAvailableBandwidthRequest {
type Error = Error;
fn try_from(value: VersionedRequest) -> Result<Self, Self::Error> {
match value.query_type {
QueryType::AvailableBandwidth => {
Ok(make_bincode_serializer().deserialize(&value.inner)?)
}
QueryType::TopupBandwidth => Err(Error::InvalidQueryType {
source_query_type: value.query_type.to_string(),
target_query_type: QueryType::AvailableBandwidth.to_string(),
}),
}
}
}
impl TryFrom<InnerAvailableBandwidthRequest> for VersionedRequest {
type Error = Error;
fn try_from(value: InnerAvailableBandwidthRequest) -> Result<Self, Self::Error> {
Ok(Self {
query_type: QueryType::AvailableBandwidth,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Request> for InnerAvailableBandwidthRequest {
type Error = crate::error::MetadataError;
fn try_from(value: Request) -> Result<Self, Self::Error> {
VersionedRequest::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
impl TryFrom<InnerAvailableBandwidthRequest> for Request {
type Error = crate::error::MetadataError;
fn try_from(value: InnerAvailableBandwidthRequest) -> Result<Self, Self::Error> {
VersionedRequest::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde() {
let req = InnerAvailableBandwidthRequest {};
let ser = VersionedRequest::try_from(req).unwrap();
assert_eq!(QueryType::AvailableBandwidth, ser.query_type);
let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap();
assert_eq!(req, de);
}
#[test]
fn empty_content() {
let future_req = VersionedRequest {
query_type: QueryType::AvailableBandwidth,
inner: vec![],
};
assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok());
}
}
@@ -0,0 +1,86 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bincode::Options;
use serde::{Deserialize, Serialize};
use crate::{make_bincode_serializer, models::Response};
use super::super::{Error, QueryType, VersionedResponse};
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InnerAvailableBandwidthResponse {}
impl TryFrom<VersionedResponse> for InnerAvailableBandwidthResponse {
type Error = Error;
fn try_from(value: VersionedResponse) -> Result<Self, Self::Error> {
match value.query_type {
QueryType::AvailableBandwidth => {
Ok(make_bincode_serializer().deserialize(&value.inner)?)
}
QueryType::TopupBandwidth => Err(Error::InvalidQueryType {
source_query_type: value.query_type.to_string(),
target_query_type: QueryType::AvailableBandwidth.to_string(),
}),
}
}
}
impl TryFrom<InnerAvailableBandwidthResponse> for VersionedResponse {
type Error = Error;
fn try_from(value: InnerAvailableBandwidthResponse) -> Result<Self, Self::Error> {
Ok(Self {
query_type: QueryType::AvailableBandwidth,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Response> for InnerAvailableBandwidthResponse {
type Error = crate::error::MetadataError;
fn try_from(value: Response) -> Result<Self, Self::Error> {
VersionedResponse::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
impl TryFrom<InnerAvailableBandwidthResponse> for Response {
type Error = crate::error::MetadataError;
fn try_from(value: InnerAvailableBandwidthResponse) -> Result<Self, Self::Error> {
VersionedResponse::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde() {
let resp = InnerAvailableBandwidthResponse {};
let ser = VersionedResponse::try_from(resp).unwrap();
assert_eq!(QueryType::AvailableBandwidth, ser.query_type);
let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap();
assert_eq!(resp, de);
}
#[test]
fn empty_content() {
let future_resp = VersionedResponse {
query_type: QueryType::AvailableBandwidth,
inner: vec![],
};
assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_ok());
}
}
@@ -0,0 +1,73 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::{
available_bandwidth::{
request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse,
},
topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse},
QueryType, VersionedRequest, VersionedResponse, VERSION,
};
use crate::models::{error::Error, Construct, Extract, Version};
#[derive(Debug, Clone, PartialEq)]
pub enum RequestData {
AvailableBandwidth(()),
TopUpBandwidth(()),
}
#[derive(Debug, Clone, PartialEq)]
pub enum ResponseData {
AvailableBandwidth(()),
TopUpBandwidth(()),
}
impl Construct<RequestData> for VersionedRequest {
fn construct(info: RequestData, _version: Version) -> Result<Self, Error> {
match info {
RequestData::AvailableBandwidth(_) => Ok(InnerAvailableBandwidthRequest {}.try_into()?),
RequestData::TopUpBandwidth(_) => Ok(InnerTopUpRequest {}.try_into()?),
}
}
}
impl Extract<RequestData> for VersionedRequest {
fn extract(&self) -> Result<(RequestData, Version), Error> {
match self.query_type {
QueryType::AvailableBandwidth => {
let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?;
Ok((RequestData::AvailableBandwidth(()), VERSION))
}
QueryType::TopupBandwidth => {
let _req = InnerTopUpRequest::try_from(self.clone())?;
Ok((RequestData::TopUpBandwidth(()), VERSION))
}
}
}
}
impl Construct<ResponseData> for VersionedResponse {
fn construct(info: ResponseData, _version: Version) -> Result<Self, Error> {
match info {
ResponseData::AvailableBandwidth(()) => {
Ok(InnerAvailableBandwidthResponse {}.try_into()?)
}
ResponseData::TopUpBandwidth(()) => Ok(InnerTopUpResponse {}.try_into()?),
}
}
}
impl Extract<ResponseData> for VersionedResponse {
fn extract(&self) -> Result<(ResponseData, Version), Error> {
match self.query_type {
QueryType::AvailableBandwidth => {
let _resp = InnerAvailableBandwidthResponse::try_from(self.clone())?;
Ok((ResponseData::AvailableBandwidth(()), VERSION))
}
QueryType::TopupBandwidth => {
let _resp = InnerTopUpResponse::try_from(self.clone())?;
Ok((ResponseData::TopUpBandwidth(()), VERSION))
}
}
}
}
@@ -0,0 +1,175 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Display;
use bincode::Options;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use super::error::Error;
use crate::{
make_bincode_serializer,
models::{Request, Response, Version},
};
pub(crate) mod available_bandwidth;
pub mod interface;
pub(crate) mod topup_bandwidth;
pub const VERSION: Version = Version::V0;
pub use available_bandwidth::{
request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest,
response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse,
};
pub use topup_bandwidth::{
request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
pub enum QueryType {
AvailableBandwidth,
TopupBandwidth,
}
impl Display for QueryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct VersionedRequest {
query_type: QueryType,
inner: Vec<u8>,
}
impl TryFrom<VersionedRequest> for Request {
type Error = Error;
fn try_from(value: VersionedRequest) -> Result<Self, Self::Error> {
Ok(Request {
version: VERSION,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Request> for VersionedRequest {
type Error = Error;
fn try_from(value: Request) -> Result<Self, Self::Error> {
if value.version != VERSION {
return Err(Error::InvalidVersion {
source_version: value.version,
target_version: VERSION,
});
}
Ok(make_bincode_serializer().deserialize(&value.inner)?)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct VersionedResponse {
query_type: QueryType,
inner: Vec<u8>,
}
impl TryFrom<VersionedResponse> for Response {
type Error = Error;
fn try_from(value: VersionedResponse) -> Result<Self, Self::Error> {
Ok(Response {
version: VERSION,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Response> for VersionedResponse {
type Error = Error;
fn try_from(value: Response) -> Result<Self, Self::Error> {
if value.version != VERSION {
return Err(Error::InvalidVersion {
source_version: value.version,
target_version: VERSION,
});
}
Ok(make_bincode_serializer().deserialize(&value.inner)?)
}
}
#[cfg(test)]
mod tests {
use self::{
available_bandwidth::{
request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse,
},
topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse},
};
use super::*;
#[test]
fn serde_request_av_bw() {
let req = VersionedRequest {
query_type: QueryType::AvailableBandwidth,
inner: make_bincode_serializer()
.serialize(&InnerAvailableBandwidthRequest {})
.unwrap(),
};
let ser = Request::try_from(req.clone()).unwrap();
assert_eq!(VERSION, ser.version);
let de = VersionedRequest::try_from(ser).unwrap();
assert_eq!(req, de);
}
#[test]
fn serde_response_av_bw() {
let resp = VersionedResponse {
query_type: QueryType::AvailableBandwidth,
inner: make_bincode_serializer()
.serialize(&InnerAvailableBandwidthResponse {})
.unwrap(),
};
let ser = Response::try_from(resp.clone()).unwrap();
assert_eq!(VERSION, ser.version);
let de = VersionedResponse::try_from(ser).unwrap();
assert_eq!(resp, de);
}
#[test]
fn serde_request_topup() {
let req = VersionedRequest {
query_type: QueryType::TopupBandwidth,
inner: make_bincode_serializer()
.serialize(&InnerTopUpRequest {})
.unwrap(),
};
let ser = Request::try_from(req.clone()).unwrap();
assert_eq!(VERSION, ser.version);
let de = VersionedRequest::try_from(ser).unwrap();
assert_eq!(req, de);
}
#[test]
fn serde_response_topup() {
let resp = VersionedResponse {
query_type: QueryType::TopupBandwidth,
inner: make_bincode_serializer()
.serialize(&InnerTopUpResponse {})
.unwrap(),
};
let ser = Response::try_from(resp.clone()).unwrap();
assert_eq!(VERSION, ser.version);
let de = VersionedResponse::try_from(ser).unwrap();
assert_eq!(resp, de);
}
}
@@ -0,0 +1,5 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod request;
pub mod response;
@@ -0,0 +1,84 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bincode::Options;
use serde::{Deserialize, Serialize};
use crate::{make_bincode_serializer, models::Request};
use super::super::{Error, QueryType, VersionedRequest};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct InnerTopUpRequest {}
impl TryFrom<VersionedRequest> for InnerTopUpRequest {
type Error = Error;
fn try_from(value: VersionedRequest) -> Result<Self, Self::Error> {
match value.query_type {
QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?),
QueryType::AvailableBandwidth => Err(Error::InvalidQueryType {
source_query_type: value.query_type.to_string(),
target_query_type: QueryType::TopupBandwidth.to_string(),
}),
}
}
}
impl TryFrom<InnerTopUpRequest> for VersionedRequest {
type Error = Error;
fn try_from(value: InnerTopUpRequest) -> Result<Self, Self::Error> {
Ok(Self {
query_type: QueryType::TopupBandwidth,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Request> for InnerTopUpRequest {
type Error = crate::error::MetadataError;
fn try_from(value: Request) -> Result<Self, Self::Error> {
VersionedRequest::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
impl TryFrom<InnerTopUpRequest> for Request {
type Error = crate::error::MetadataError;
fn try_from(value: InnerTopUpRequest) -> Result<Self, Self::Error> {
VersionedRequest::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde() {
let req = InnerTopUpRequest {};
let ser = VersionedRequest::try_from(req.clone()).unwrap();
assert_eq!(QueryType::TopupBandwidth, ser.query_type);
let de = InnerTopUpRequest::try_from(ser).unwrap();
assert_eq!(req, de);
}
#[test]
fn empty_content() {
let future_req = VersionedRequest {
query_type: QueryType::TopupBandwidth,
inner: vec![],
};
assert!(InnerTopUpRequest::try_from(future_req).is_ok());
}
}
@@ -0,0 +1,84 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bincode::Options;
use serde::{Deserialize, Serialize};
use crate::{make_bincode_serializer, models::Response};
use super::super::{Error, QueryType, VersionedResponse};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct InnerTopUpResponse {}
impl TryFrom<VersionedResponse> for InnerTopUpResponse {
type Error = Error;
fn try_from(value: VersionedResponse) -> Result<Self, Self::Error> {
match value.query_type {
QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?),
QueryType::AvailableBandwidth => Err(Error::InvalidQueryType {
source_query_type: value.query_type.to_string(),
target_query_type: QueryType::TopupBandwidth.to_string(),
}),
}
}
}
impl TryFrom<InnerTopUpResponse> for VersionedResponse {
type Error = Error;
fn try_from(value: InnerTopUpResponse) -> Result<Self, Self::Error> {
Ok(Self {
query_type: QueryType::TopupBandwidth,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Response> for InnerTopUpResponse {
type Error = crate::error::MetadataError;
fn try_from(value: Response) -> Result<Self, Self::Error> {
VersionedResponse::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
impl TryFrom<InnerTopUpResponse> for Response {
type Error = crate::error::MetadataError;
fn try_from(value: InnerTopUpResponse) -> Result<Self, Self::Error> {
VersionedResponse::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde() {
let resp = InnerTopUpResponse {};
let ser = VersionedResponse::try_from(resp.clone()).unwrap();
assert_eq!(QueryType::TopupBandwidth, ser.query_type);
let de = InnerTopUpResponse::try_from(ser).unwrap();
assert_eq!(resp, de);
}
#[test]
fn empty_content() {
let future_resp = VersionedResponse {
query_type: QueryType::TopupBandwidth,
inner: vec![],
};
assert!(InnerTopUpResponse::try_from(future_resp).is_ok());
}
}
@@ -0,0 +1,5 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod request;
pub mod response;
@@ -0,0 +1,86 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bincode::Options;
use serde::{Deserialize, Serialize};
use crate::{make_bincode_serializer, models::Request};
use super::super::{Error, QueryType, VersionedRequest};
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InnerAvailableBandwidthRequest {}
impl TryFrom<VersionedRequest> for InnerAvailableBandwidthRequest {
type Error = Error;
fn try_from(value: VersionedRequest) -> Result<Self, Self::Error> {
match value.query_type {
QueryType::AvailableBandwidth => {
Ok(make_bincode_serializer().deserialize(&value.inner)?)
}
QueryType::TopupBandwidth => Err(Error::InvalidQueryType {
source_query_type: value.query_type.to_string(),
target_query_type: QueryType::AvailableBandwidth.to_string(),
}),
}
}
}
impl TryFrom<InnerAvailableBandwidthRequest> for VersionedRequest {
type Error = Error;
fn try_from(value: InnerAvailableBandwidthRequest) -> Result<Self, Self::Error> {
Ok(Self {
query_type: QueryType::AvailableBandwidth,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Request> for InnerAvailableBandwidthRequest {
type Error = crate::error::MetadataError;
fn try_from(value: Request) -> Result<Self, Self::Error> {
VersionedRequest::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
impl TryFrom<InnerAvailableBandwidthRequest> for Request {
type Error = crate::error::MetadataError;
fn try_from(value: InnerAvailableBandwidthRequest) -> Result<Self, Self::Error> {
VersionedRequest::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde() {
let req = InnerAvailableBandwidthRequest {};
let ser = VersionedRequest::try_from(req).unwrap();
assert_eq!(QueryType::AvailableBandwidth, ser.query_type);
let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap();
assert_eq!(req, de);
}
#[test]
fn empty_content() {
let future_req = VersionedRequest {
query_type: QueryType::AvailableBandwidth,
inner: vec![],
};
assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok());
}
}
@@ -0,0 +1,90 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bincode::Options;
use serde::{Deserialize, Serialize};
use crate::{make_bincode_serializer, models::Response};
use super::super::{Error, QueryType, VersionedResponse};
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InnerAvailableBandwidthResponse {
pub available_bandwidth: i64,
}
impl TryFrom<VersionedResponse> for InnerAvailableBandwidthResponse {
type Error = Error;
fn try_from(value: VersionedResponse) -> Result<Self, Self::Error> {
match value.query_type {
QueryType::AvailableBandwidth => {
Ok(make_bincode_serializer().deserialize(&value.inner)?)
}
QueryType::TopupBandwidth => Err(Error::InvalidQueryType {
source_query_type: value.query_type.to_string(),
target_query_type: QueryType::AvailableBandwidth.to_string(),
}),
}
}
}
impl TryFrom<InnerAvailableBandwidthResponse> for VersionedResponse {
type Error = Error;
fn try_from(value: InnerAvailableBandwidthResponse) -> Result<Self, Self::Error> {
Ok(Self {
query_type: QueryType::AvailableBandwidth,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Response> for InnerAvailableBandwidthResponse {
type Error = crate::error::MetadataError;
fn try_from(value: Response) -> Result<Self, Self::Error> {
VersionedResponse::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
impl TryFrom<InnerAvailableBandwidthResponse> for Response {
type Error = crate::error::MetadataError;
fn try_from(value: InnerAvailableBandwidthResponse) -> Result<Self, Self::Error> {
VersionedResponse::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde() {
let resp = InnerAvailableBandwidthResponse {
available_bandwidth: 42,
};
let ser = VersionedResponse::try_from(resp).unwrap();
assert_eq!(QueryType::AvailableBandwidth, ser.query_type);
let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap();
assert_eq!(resp, de);
}
#[test]
fn invalid_content() {
let future_resp = VersionedResponse {
query_type: QueryType::AvailableBandwidth,
inner: vec![],
};
assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_err());
}
}
@@ -0,0 +1,224 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credentials_interface::CredentialSpendingData;
#[cfg(feature = "testing")]
use super::super::v0 as previous;
use super::{
available_bandwidth::{
request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse,
},
topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse},
QueryType, VersionedRequest, VersionedResponse, VERSION,
};
use crate::models::{error::Error, Construct, Extract, Version};
#[derive(Debug, Clone, PartialEq)]
pub enum RequestData {
AvailableBandwidth(()),
TopUpBandwidth(Box<CredentialSpendingData>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum ResponseData {
AvailableBandwidth(i64),
TopUpBandwidth(i64),
}
impl Construct<RequestData> for VersionedRequest {
fn construct(info: RequestData, _version: Version) -> Result<Self, Error> {
match info {
RequestData::AvailableBandwidth(_) => Ok(InnerAvailableBandwidthRequest {}.try_into()?),
RequestData::TopUpBandwidth(credential) => Ok(InnerTopUpRequest {
credential: *credential,
}
.try_into()?),
}
}
}
impl Extract<RequestData> for VersionedRequest {
fn extract(&self) -> Result<(RequestData, Version), Error> {
match self.query_type {
QueryType::AvailableBandwidth => {
let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?;
Ok((RequestData::AvailableBandwidth(()), VERSION))
}
QueryType::TopupBandwidth => {
let req = InnerTopUpRequest::try_from(self.clone())?;
Ok((
RequestData::TopUpBandwidth(Box::new(req.credential)),
VERSION,
))
}
}
}
}
impl Construct<ResponseData> for VersionedResponse {
fn construct(info: ResponseData, _version: Version) -> Result<Self, Error> {
match info {
ResponseData::AvailableBandwidth(available_bandwidth) => {
Ok(InnerAvailableBandwidthResponse {
available_bandwidth,
}
.try_into()?)
}
ResponseData::TopUpBandwidth(available_bandwidth) => Ok(InnerTopUpResponse {
available_bandwidth,
}
.try_into()?),
}
}
}
impl Extract<ResponseData> for VersionedResponse {
fn extract(&self) -> Result<(ResponseData, Version), Error> {
match self.query_type {
QueryType::AvailableBandwidth => {
let resp = InnerAvailableBandwidthResponse::try_from(self.clone())?;
Ok((
ResponseData::AvailableBandwidth(resp.available_bandwidth),
VERSION,
))
}
QueryType::TopupBandwidth => {
let resp = InnerTopUpResponse::try_from(self.clone())?;
Ok((
ResponseData::TopUpBandwidth(resp.available_bandwidth),
VERSION,
))
}
}
}
}
// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions
#[cfg(feature = "testing")]
impl TryFrom<previous::interface::RequestData> for RequestData {
type Error = super::Error;
fn try_from(value: previous::interface::RequestData) -> Result<Self, Self::Error> {
match value {
previous::interface::RequestData::AvailableBandwidth(inner) => {
Ok(Self::AvailableBandwidth(inner))
}
previous::interface::RequestData::TopUpBandwidth(_) => {
Err(super::Error::UpdateNotPossible {
from: previous::VERSION,
to: VERSION,
})
}
}
}
}
// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions
#[cfg(feature = "testing")]
impl TryFrom<RequestData> for previous::interface::RequestData {
type Error = super::Error;
fn try_from(value: RequestData) -> Result<Self, Self::Error> {
match value {
RequestData::AvailableBandwidth(inner) => Ok(Self::AvailableBandwidth(inner)),
RequestData::TopUpBandwidth(_) => Ok(Self::TopUpBandwidth(())),
}
}
}
// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions
#[cfg(feature = "testing")]
impl TryFrom<previous::interface::ResponseData> for ResponseData {
type Error = super::Error;
fn try_from(value: previous::interface::ResponseData) -> Result<Self, Self::Error> {
match value {
previous::interface::ResponseData::AvailableBandwidth(_) => {
Err(super::Error::UpdateNotPossible {
from: previous::VERSION,
to: VERSION,
})
}
previous::interface::ResponseData::TopUpBandwidth(_) => {
Err(super::Error::UpdateNotPossible {
from: previous::VERSION,
to: VERSION,
})
}
}
}
}
// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions
#[cfg(feature = "testing")]
impl TryFrom<ResponseData> for previous::interface::ResponseData {
type Error = super::Error;
fn try_from(value: ResponseData) -> Result<Self, Self::Error> {
match value {
ResponseData::AvailableBandwidth(_) => Ok(Self::AvailableBandwidth(())),
ResponseData::TopUpBandwidth(_) => Ok(Self::TopUpBandwidth(())),
}
}
}
#[cfg(test)]
mod test {
use crate::models::tests::CREDENTIAL_BYTES;
use super::*;
#[test]
fn request_upgrade() {
assert_eq!(
RequestData::try_from(previous::interface::RequestData::AvailableBandwidth(()))
.unwrap(),
RequestData::AvailableBandwidth(())
);
assert!(
RequestData::try_from(previous::interface::RequestData::TopUpBandwidth(())).is_err(),
);
}
#[test]
fn response_upgrade() {
assert!(
ResponseData::try_from(previous::interface::ResponseData::AvailableBandwidth(()))
.is_err()
);
assert!(
ResponseData::try_from(previous::interface::ResponseData::TopUpBandwidth(())).is_err()
);
}
#[test]
fn request_downgrade() {
assert_eq!(
previous::interface::RequestData::try_from(RequestData::AvailableBandwidth(()))
.unwrap(),
previous::interface::RequestData::AvailableBandwidth(())
);
assert_eq!(
previous::interface::RequestData::try_from(RequestData::TopUpBandwidth(Box::new(
CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap()
)))
.unwrap(),
previous::interface::RequestData::TopUpBandwidth(())
);
}
#[test]
fn response_downgrade() {
assert_eq!(
previous::interface::ResponseData::try_from(ResponseData::AvailableBandwidth(42))
.unwrap(),
previous::interface::ResponseData::AvailableBandwidth(())
);
assert_eq!(
previous::interface::ResponseData::try_from(ResponseData::TopUpBandwidth(42)).unwrap(),
previous::interface::ResponseData::TopUpBandwidth(())
);
}
}
@@ -0,0 +1,224 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Display;
use bincode::Options;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use super::error::Error;
use crate::{
make_bincode_serializer,
models::{Request, Response, Version},
};
pub use available_bandwidth::{
request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest,
response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse,
};
pub use topup_bandwidth::{
request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse,
};
pub(crate) mod available_bandwidth;
pub mod interface;
pub(crate) mod topup_bandwidth;
pub const VERSION: Version = Version::V1;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
pub enum QueryType {
AvailableBandwidth,
TopupBandwidth,
}
impl Display for QueryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct VersionedRequest {
query_type: QueryType,
inner: Vec<u8>,
}
impl TryFrom<VersionedRequest> for Request {
type Error = Error;
fn try_from(value: VersionedRequest) -> Result<Self, Self::Error> {
Ok(Request {
version: VERSION,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Request> for VersionedRequest {
type Error = Error;
fn try_from(value: Request) -> Result<Self, Self::Error> {
if value.version != VERSION {
return Err(Error::InvalidVersion {
source_version: value.version,
target_version: VERSION,
});
}
Ok(make_bincode_serializer().deserialize(&value.inner)?)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct VersionedResponse {
query_type: QueryType,
inner: Vec<u8>,
}
impl TryFrom<VersionedResponse> for Response {
type Error = Error;
fn try_from(value: VersionedResponse) -> Result<Self, Self::Error> {
Ok(Response {
version: VERSION,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Response> for VersionedResponse {
type Error = Error;
fn try_from(value: Response) -> Result<Self, Self::Error> {
if value.version != VERSION {
return Err(Error::InvalidVersion {
source_version: value.version,
target_version: VERSION,
});
}
Ok(make_bincode_serializer().deserialize(&value.inner)?)
}
}
#[cfg(test)]
mod tests {
use nym_credentials_interface::CredentialSpendingData;
use crate::models::tests::CREDENTIAL_BYTES;
use self::{
available_bandwidth::{
request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse,
},
topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse},
};
use super::*;
#[test]
fn mismatched_request_version() {
let version = Version::V0;
let future_bw = Request {
version,
inner: vec![],
};
if let Err(Error::InvalidVersion {
source_version,
target_version,
}) = VersionedRequest::try_from(future_bw)
{
assert_eq!(source_version, version);
assert_eq!(target_version, VERSION);
} else {
panic!("failed");
};
}
#[test]
fn mismatched_response_version() {
let version = Version::V0;
let future_bw = Response {
version,
inner: vec![],
};
if let Err(Error::InvalidVersion {
source_version,
target_version,
}) = VersionedResponse::try_from(future_bw)
{
assert_eq!(source_version, version);
assert_eq!(target_version, VERSION);
} else {
panic!("failed");
};
}
#[test]
fn serde_request_av_bw() {
let req = VersionedRequest {
query_type: QueryType::AvailableBandwidth,
inner: make_bincode_serializer()
.serialize(&InnerAvailableBandwidthResponse {
available_bandwidth: 42,
})
.unwrap(),
};
let ser = Request::try_from(req.clone()).unwrap();
assert_eq!(VERSION, ser.version);
let de = VersionedRequest::try_from(ser).unwrap();
assert_eq!(req, de);
}
#[test]
fn serde_response_av_bw() {
let resp = VersionedResponse {
query_type: QueryType::AvailableBandwidth,
inner: make_bincode_serializer()
.serialize(&InnerAvailableBandwidthRequest {})
.unwrap(),
};
let ser = Response::try_from(resp.clone()).unwrap();
assert_eq!(VERSION, ser.version);
let de = VersionedResponse::try_from(ser).unwrap();
assert_eq!(resp, de);
}
#[test]
fn serde_request_topup() {
let req = VersionedRequest {
query_type: QueryType::TopupBandwidth,
inner: make_bincode_serializer()
.serialize(&InnerTopUpRequest {
credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(),
})
.unwrap(),
};
let ser = Request::try_from(req.clone()).unwrap();
assert_eq!(VERSION, ser.version);
let de = VersionedRequest::try_from(ser).unwrap();
assert_eq!(req, de);
}
#[test]
fn serde_response_topup() {
let resp = VersionedResponse {
query_type: QueryType::TopupBandwidth,
inner: make_bincode_serializer()
.serialize(&InnerTopUpResponse {
available_bandwidth: 42,
})
.unwrap(),
};
let ser = Response::try_from(resp.clone()).unwrap();
assert_eq!(VERSION, ser.version);
let de = VersionedResponse::try_from(ser).unwrap();
assert_eq!(resp, de);
}
}
@@ -0,0 +1,5 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod request;
pub mod response;
@@ -0,0 +1,92 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bincode::Options;
use nym_credentials_interface::CredentialSpendingData;
use serde::{Deserialize, Serialize};
use crate::{make_bincode_serializer, models::Request};
use super::super::{Error, QueryType, VersionedRequest};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct InnerTopUpRequest {
/// Ecash credential
pub credential: CredentialSpendingData,
}
impl TryFrom<VersionedRequest> for InnerTopUpRequest {
type Error = Error;
fn try_from(value: VersionedRequest) -> Result<Self, Self::Error> {
match value.query_type {
QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?),
QueryType::AvailableBandwidth => Err(Error::InvalidQueryType {
source_query_type: value.query_type.to_string(),
target_query_type: QueryType::TopupBandwidth.to_string(),
}),
}
}
}
impl TryFrom<InnerTopUpRequest> for VersionedRequest {
type Error = Error;
fn try_from(value: InnerTopUpRequest) -> Result<Self, Self::Error> {
Ok(Self {
query_type: QueryType::TopupBandwidth,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Request> for InnerTopUpRequest {
type Error = crate::error::MetadataError;
fn try_from(value: Request) -> Result<Self, Self::Error> {
VersionedRequest::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
impl TryFrom<InnerTopUpRequest> for Request {
type Error = crate::error::MetadataError;
fn try_from(value: InnerTopUpRequest) -> Result<Self, Self::Error> {
VersionedRequest::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use crate::models::tests::CREDENTIAL_BYTES;
use super::*;
#[test]
fn serde() {
let req = InnerTopUpRequest {
credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(),
};
let ser = VersionedRequest::try_from(req.clone()).unwrap();
assert_eq!(QueryType::TopupBandwidth, ser.query_type);
let de = InnerTopUpRequest::try_from(ser).unwrap();
assert_eq!(req, de);
}
#[test]
fn invalid_content() {
let future_req = VersionedRequest {
query_type: QueryType::TopupBandwidth,
inner: vec![],
};
assert!(InnerTopUpRequest::try_from(future_req).is_err());
}
}
@@ -0,0 +1,88 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bincode::Options;
use serde::{Deserialize, Serialize};
use crate::{make_bincode_serializer, models::Response};
use super::super::{Error, QueryType, VersionedResponse};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct InnerTopUpResponse {
pub available_bandwidth: i64,
}
impl TryFrom<VersionedResponse> for InnerTopUpResponse {
type Error = Error;
fn try_from(value: VersionedResponse) -> Result<Self, Self::Error> {
match value.query_type {
QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?),
QueryType::AvailableBandwidth => Err(Error::InvalidQueryType {
source_query_type: value.query_type.to_string(),
target_query_type: QueryType::TopupBandwidth.to_string(),
}),
}
}
}
impl TryFrom<InnerTopUpResponse> for VersionedResponse {
type Error = Error;
fn try_from(value: InnerTopUpResponse) -> Result<Self, Self::Error> {
Ok(Self {
query_type: QueryType::TopupBandwidth,
inner: make_bincode_serializer().serialize(&value)?,
})
}
}
impl TryFrom<Response> for InnerTopUpResponse {
type Error = crate::error::MetadataError;
fn try_from(value: Response) -> Result<Self, Self::Error> {
VersionedResponse::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
impl TryFrom<InnerTopUpResponse> for Response {
type Error = crate::error::MetadataError;
fn try_from(value: InnerTopUpResponse) -> Result<Self, Self::Error> {
VersionedResponse::try_from(value)?
.try_into()
.map_err(|err: Error| crate::error::MetadataError::Models {
message: err.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde() {
let resp = InnerTopUpResponse {
available_bandwidth: 42,
};
let ser = VersionedResponse::try_from(resp.clone()).unwrap();
assert_eq!(QueryType::TopupBandwidth, ser.query_type);
let de = InnerTopUpResponse::try_from(ser).unwrap();
assert_eq!(resp, de);
}
#[test]
fn invalid_content() {
let future_resp = VersionedResponse {
query_type: QueryType::TopupBandwidth,
inner: vec![],
};
assert!(InnerTopUpResponse::try_from(future_resp).is_err());
}
}
@@ -0,0 +1,10 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub const V1_API_VERSION: &str = "v1";
pub const BANDWIDTH: &str = "bandwidth";
pub const VERSION: &str = "version";
pub const AVAILABLE: &str = "available";
pub const TOPUP: &str = "topup";
@@ -0,0 +1,41 @@
[package]
name = "nym-wireguard-private-metadata-tests"
version = "1.0.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
async-trait = { workspace = true }
axum = { workspace = true, features = ["tokio", "macros"] }
nym-credential-verification = { path = "../../credential-verification" }
nym-credentials-interface = { path = "../../credentials-interface" }
nym-http-api-client = { path = "../../http-api-client" }
nym-http-api-common = { path = "../../http-api-common", features = [
"middleware",
"utoipa",
"output",
] }
nym-wireguard = { path = "../../wireguard" }
tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] }
tower-http = { workspace = true, features = [
"cors",
"trace",
"compression-br",
"compression-deflate",
"compression-gzip",
"compression-zstd",
] }
utoipa = { workspace = true, features = ["axum_extras", "time"] }
nym-wireguard-private-metadata-client = { path = "../client" }
nym-wireguard-private-metadata-shared = { path = "../shared", features = [
"testing",
] }
nym-wireguard-private-metadata-server = { path = "../server" }
[lints]
workspace = true

Some files were not shown because too many files have changed in this diff Show More