Max/docs-diataxis-ify (#6494)

* Diatixisify!

* First pass at Typedoc generation for TS SDK

* Remove overview pages

* Fix typos and remove codebase references from docs

Fix typos across network and developer docs: Quorum, available,
cryptosystem, transaction, proportional, Standalone. Remove TODO
placeholder from dVPN protocol page. Strip GitHub source links
from network docs to decouple documentation from repo structure.

* Expand thin landing pages across network and developer docs

- Add intro content to network overview, infrastructure, and reference landing pages
- Expand developer index with "where to start" guide
- Add usage instructions and explanations to all five TS playground pages
- Expand WebSocket client page with setup and message format examples

* Restructure Rust SDK developer docs

- Delete redundant mixnet example, message-helpers, and message-types subpages
- Delete client-pool architecture and example subpages (content folded into landing)
- Delete tcpproxy troubleshooting (folded into landing page)
- Add deprecation notices to TcpProxy pages, pointing to Stream module
- Add stream module docs: landing page, architecture, tutorial, and 4 example pages
- Add mixnet and client-pool tutorials
- Add SDK tour page
- Update navigation and landing pages with docs.rs links

* Restructure TS SDK developer docs

- Merge overview, installation, and getting started into TS SDK landing page
- Fold FAQ content into bundling/troubleshooting section
- Delete redundant overview, installation, start, and FAQ pages
- Update internal links in browsers.mdx and native.mdx
- Update navigation and example page imports

* Flatten and expand APIs section

- Collapse nested API subpages into single pages with inline Redoc embeds
- Rewrite introduction as landing page with decision table
- Add endpoint categories, quick curl examples to each API page
- Mark Explorer API as deprecated
- Move NS API deployment guide to operators/performance-and-testing
- Fix dangling /apis/nym-api/mainnet link in network-components
- Remove sandbox endpoints from all API pages

* Add redirects for moved and deleted pages

- Add 25 redirects covering TS SDK, Rust SDK, APIs, and network sections
- Fix dangling /developers/typescript/start link in operators changelog

* Replace individual example doc pages with GitHub-linked tables, expand tutorials

- replace individual example doc pages with GitHub-linked tables
- expand mixnet tutorial with persistent identity and split_sender sections
- add tcpproxy tutorial
- rename "API Reference" to "TypeDoc Reference" in TS SDK sidebar
- rename "Misc" to "Extras" in developer sidebar, move VPN CLI up
- remove echo server from tools
- update message-queue callout to reference actual modules
- fix mixnet/examples redirect collision

* Add SEO frontmatter, validate encryption standards, clean up URLs

- add title/description/schemaType/section/lastUpdated frontmatter to 48
  pages across developers, network, and APIs sections
- remove network/.archive/ directory (compare against develop instead)
- update nymtech.net → nym.com for website/blog links (keep infra URLs)
- add native proxy "in progress" callout for Rust/C/Go

* API-scraper update (#6598)

* read nodes and locations

* update python-prebuild.sh

* Address PR #6494 review feedback
- Use "mode" consistently instead of "role" on nym-nodes page
- Replace "staking" with "bonding" for NYM token collateral
- Wire up auto-scraped node counts via TimeNow + nodes-count.json
- Fix broken licensing images: download CC icons locally, replace inline HTML
- Fix 9 stale redirects pointing through deleted /network/architecture path

* Fix linkcheck errors
- Fix stale cross-links: /network/concepts/ → /network/mixnet-mode/
- Replace README.md references with globals.md in TypeDoc output
- Add entryFileName: globals to typedoc.json configs to prevent recurrence

* Fix remaining stale /network/architecture links
- zk-nym-overview: architecture/nyx#nym-api → /network/infrastructure/nyx#nym-api
- setup: network/architecture → /network/overview

* Remove accidentally re-included architecture.md file from rebase

* Standardize tutorials, document examples, add llms.txt, apply tone fixes

- Expand Rust SDK tutorials with step-by-step structure; document all SDK examples across mixnet, client-pool, and tcpproxy pages
- Add llms.txt generation script, wire into build and CI workflows
- Apply tone/style fixes: deduplicate callouts, vary sentence structure, standardize voice consistency across changed pages

* Consolidate redundant network overview docs

* Trim dev docs: git-first imports, stream notice, collapse TcpProxy

* Update tutorial

* Refresh auto-generated API and command outputs

* Update network section docs

* Update developer and API docs: reusable components, stream protocol, conventions, tutorial fixes

* Fix Rust SDK tutorial bugs: setup_env, port conflicts, logging,
open_stream race condition

* Update stream.mdx

* Remove docs.rs link from Stream overview for the moment

* add llms.txt and llms-full.txt note to readme

---------

Co-authored-by: import this <97586125+serinko@users.noreply.github.com>
This commit is contained in:
mfahampshire
2026-04-09 15:25:31 +00:00
committed by GitHub
parent 4fb78c3737
commit f648349e82
239 changed files with 29789 additions and 3702 deletions
+2
View File
@@ -39,6 +39,8 @@ jobs:
- name: Install project dependencies
run: pnpm i
- name: Generate llms-full.txt
run: pnpm run generate:llms
- name: Build project
run: pnpm run build
- name: Generate sitemap
+21
View File
@@ -6,6 +6,8 @@ on:
branches-ignore: [master]
paths:
- "documentation/docs/**"
- "sdk/typescript/packages/sdk/src/**"
- "sdk/typescript/packages/mix-fetch/src/**"
- ".github/workflows/ci-docs.yml"
jobs:
@@ -42,8 +44,27 @@ jobs:
command: build
args: --workspace --release
- name: Check if TypeScript SDK source changed
id: check-ts-sdk
run: |
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-fetch)/src/'; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
working-directory: ${{ github.workspace }}
- name: Regenerate TypeDoc API reference
if: steps.check-ts-sdk.outputs.changed == 'true'
run: |
npm install -g typedoc@0.25.13 typedoc-plugin-markdown@4.0.3
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
- name: Install project dependencies
run: pnpm i
- name: Generate llms-full.txt
run: pnpm run generate:llms
- name: Build project
run: pnpm run build
- name: Generate sitemap
+8 -1
View File
@@ -20,7 +20,7 @@ Our `prebuild` script relies on the following:
- [`tabulate`](https://pypi.org/project/tabulate/)
- `jq`
Otherwise make sure to have `node` installed.
Otherwise make sure to have `node` and `rust` installed.
### Link checking (optional)
We use [lychee](https://github.com/lycheeverse/lychee) to check for broken links. Install via your package manager or `cargo install lychee`, then run:
@@ -89,6 +89,13 @@ NEXT_PUBLIC_SITE_URL=https://nym.com/docs
| HowTo | Step-by-step install/setup guides |
| FAQPage | Question-answer pages |
## LLM-readability
Two files are generated in the deployment workflow: `llms.txt` and `llms-full.txt`. These files follow [Cloudflare's approach](https://developers.cloudflare.com/style-guide/how-we-docs/ai-consumability/) to generation and use.
When running locally can you find these at `http://localhost:3000/docs/llms.txt` and `http://localhost:3000/docs/llms-full.txt`.
When deployed to production, these can be found at [https://nym.com/docs/llms.txt](https://nym.com/docs/llms.txt) and [https://nym.com/docs/llms-full.txt](https://nym.com/docs/llms-full.txt).
## Licensing and copyright information
This is a monorepo and components that make up Nym as a system are licensed individually, so for accurate information, please check individual files.
@@ -0,0 +1,24 @@
import { Callout } from "nextra/components";
const COMMIT_SHORT = "4077717";
const COMMIT_FULL = "4077717d3";
const EXAMPLES_URL =
"https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/examples";
export const CodeVerified = () => (
<Callout type="info">
Code verified against commit{" "}
<a
href={`https://github.com/nymtech/nym/commit/${COMMIT_FULL}`}
target="_blank"
rel="noopener noreferrer"
>
<code>{COMMIT_SHORT}</code>
</a>
. If the API has changed since then, check the{" "}
<a href={EXAMPLES_URL} target="_blank" rel="noopener noreferrer">
examples in the repo
</a>{" "}
for the latest usage.
</Callout>
);
@@ -0,0 +1,13 @@
import { Callout } from "nextra/components";
const CRATES_VERSION = "1.20.4";
const INSTALL_PATH = "/developers/rust/importing";
export const CratesPaused = () => (
<Callout type="warning">
<strong>Crate publication is paused.</strong> The crates.io release (v
{CRATES_VERSION}) doesn't include the Stream module or other recent work.
Publication resumes with the Lewes Protocol. Import from Git for now see{" "}
<a href={INSTALL_PATH}>Installation</a>.
</Callout>
);
@@ -0,0 +1,6 @@
{
"nodes": 753,
"locations": 76,
"mixnodes": 272,
"exit_gateways": 472
}
@@ -11,7 +11,7 @@ options:
--no_routing_history Display node stats without routing history
--no_verloc_metrics Display node stats without verloc metrics
-m, --markdown Display results in markdown format
-o [OUTPUT], --output [OUTPUT]
-o, --output [OUTPUT]
Save results to file (in current dir or supply with
path without filename)
```
@@ -8,12 +8,9 @@ Commands:
help Print this message or the help of the given subcommand(s)
Options:
-c, --config-env-file <CONFIG_ENV_FILE>
Path pointing to an env file that configures the Nym API [env: NYMAPI_CONFIG_ENV_FILE_ARG=]
--no-banner
A no-op flag included for consistency with other binaries (and compatibility with nymvisor, oops) [env: NYMAPI_NO_BANNER_ARG=]
-h, --help
Print help
-V, --version
Print version
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the Nym API [env: NYMAPI_CONFIG_ENV_FILE_ARG=]
--no-banner A no-op flag included for consistency with other binaries (and compatibility with nymvisor, oops)
[env: NYMAPI_NO_BANNER_ARG=]
-h, --help Print help
-V, --version Print version
```
@@ -12,8 +12,7 @@ usage: nym-node-cli install [-h] [-V] [-d BRANCH] [-v]
options:
-h, --help show this help message and exit
-V, --version show program's version number and exit
-d BRANCH, --dev BRANCH
Define github branch (default: develop)
-d, --dev BRANCH Define github branch (default: develop)
-v, --verbose Show full error tracebacks
--mode {mixnode,entry-gateway,exit-gateway}
Node mode: 'mixnode', 'entry-gateway', or 'exit-
@@ -12,12 +12,9 @@ Commands:
help Print this message or the help of the given subcommand(s)
Options:
-c, --config-env-file <CONFIG_ENV_FILE>
Path pointing to an env file that configures the nym-node and overrides any preconfigured values [env: NYMNODE_CONFIG_ENV_FILE_ARG=]
--no-banner
Flag used for disabling the printed banner in tty [env: NYMNODE_NO_BANNER=]
-h, --help
Print help
-V, --version
Print version
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the nym-node and overrides any preconfigured values
[env: NYMNODE_CONFIG_ENV_FILE_ARG=]
--no-banner Flag used for disabling the printed banner in tty [env: NYMNODE_NO_BANNER=]
-h, --help Print help
-V, --version Print version
```
@@ -9,93 +9,124 @@ Options:
--config-file <CONFIG_FILE>
Path to a configuration file of this node [env: NYMNODE_CONFIG=]
--accept-operator-terms-and-conditions
Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env: NYMNODE_ACCEPT_OPERATOR_TERMS=]
Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at
<https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env: NYMNODE_ACCEPT_OPERATOR_TERMS=]
--deny-init
Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist [env: NYMNODE_DENY_INIT=]
Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist [env:
NYMNODE_DENY_INIT=]
--init-only
If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env: NYMNODE_INIT_ONLY=]
If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env:
NYMNODE_INIT_ONLY=]
--local
Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
--mode [<MODE>...]
Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway,
exit-providers-only]
--modes <MODES>
Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway,
exit-gateway, exit-providers-only]
-w, --write-changes
If this node has been initialised before, specify whether to write any new changes to the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=]
If this node has been initialised before, specify whether to write any new changes to the config file [env:
NYMNODE_WRITE_CONFIG_CHANGES=]
--bonding-information-output <BONDING_INFORMATION_OUTPUT>
Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding information is still a subject to change and this argument should be treated only as a preview of future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=]
Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding information is still
a subject to change and this argument should be treated only as a preview of future features [env:
NYMNODE_BONDING_INFORMATION_OUTPUT=]
-o, --output <OUTPUT>
Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible values: text, json]
Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible values:
text, json]
--public-ips <PUBLIC_IPS>
Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In nearly all circumstances, it's going to be identical to the address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=]
Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In nearly all
circumstances, it's going to be identical to the address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=]
--hostname <HOSTNAME>
Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients [env: NYMNODE_HOSTNAME=]
Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients [env:
NYMNODE_HOSTNAME=]
--location <LOCATION>
Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'),
three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
--http-bind-address <HTTP_BIND_ADDRESS>
Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH>
Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
--http-access-token <HTTP_ACCESS_TOKEN>
An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=]
An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env:
NYMNODE_HTTP_ACCESS_TOKEN=]
--expose-system-info <EXPOSE_SYSTEM_INFO>
Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values: true, false]
Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values:
true, false]
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE>
Specify whether basic system hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
Specify whether basic system hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=]
[possible values: true, false]
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE>
Specify whether detailed system crypto hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
Specify whether detailed system crypto hardware information should be exposed. default: true [env:
NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
--mixnet-bind-address <MIXNET_BIND_ADDRESS>
Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT>
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind
a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
--nym-api-urls <NYM_API_URLS>
Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
--nyxd-urls <NYXD_URLS>
Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
--enable-console-logging <ENABLE_CONSOLE_LOGGING>
Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false]
Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible
values: true, false]
--wireguard-enabled <WIREGUARD_ENABLED>
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS>
Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT>
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is
behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for
IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
--wireguard-userspace <WIREGUARD_USERSPACE>
Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] [possible values: true, false]
Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without
kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] [possible values: true, false]
--verloc-bind-address <VERLOC_BIND_ADDRESS>
Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
--verloc-announce-port <VERLOC_ANNOUNCE_PORT>
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind
a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
--entry-bind-address <ENTRY_BIND_ADDRESS>
Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=]
--announce-ws-port <ANNOUNCE_WS_PORT>
Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used
instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
--announce-wss-port <ANNOUNCE_WSS_PORT>
If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
--enforce-zk-nyms <ENFORCE_ZK_NYMS>
Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, false]
Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying
clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, false]
--mnemonic <MNEMONIC>
Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=]
Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env:
NYMNODE_MNEMONIC=]
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL>
Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local
networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY>
Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local
networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL>
Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
--open-proxy <OPEN_PROXY>
Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible values: true, false]
Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it receives [env:
NYMNODE_OPEN_PROXY=] [possible values: true, false]
--lp-control-bind-address <LP_CONTROL_BIND_ADDRESS>
Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=]
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT>
Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the `lp_control_bind_address` will be used instead [env: NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the `lp_control_bind_address`
will be used instead [env: NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
--lp-data-bind-address <LP_DATA_BIND_ADDRESS>
Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=]
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT>
Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the `lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the `lp_data_bind_address` will be
used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
--lp-use-mock-ecash <LP_USE_MOCK_ECASH>
Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP listener will accept any credential without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP
listener will accept any credential without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
-h, --help
Print help
```
@@ -11,10 +11,7 @@ Commands:
help Print this message or the help of the given subcommand(s)
Options:
-c, --config-env-file <CONFIG_ENV_FILE>
Path pointing to an env file that configures the nymvisor and overrides any preconfigured values
-h, --help
Print help
-V, --version
Print version
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the nymvisor and overrides any preconfigured values
-h, --help Print help
-V, --version Print version
```
+201 -9
View File
@@ -64,19 +64,19 @@ const config = {
},
{
source: "/docs/architecture/nym-vs-others.html",
destination: "/docs/network/architecture/nym-vs-others",
destination: "/docs/network/overview/comparisons",
permanent: true,
basePath: false,
},
{
source: "/docs/architecture/traffic-flow.html",
destination: "/docs/network/traffic", // testing difference
destination: "/docs/network/mixnet-mode/traffic-flow",
permanent: true,
basePath: false,
},
{
source: "/docs/architecture/addressing-system.html",
destination: "/docs/network/traffic/addressing-system",
destination: "/docs/network/reference/addressing",
permanent: true,
basePath: false,
},
@@ -100,7 +100,7 @@ const config = {
},
{
source: "/docs/nodes/overview.html ",
destination: "/docs/network/architecture/mixnet#nym-nodes",
destination: "/docs/network/infrastructure/nym-nodes",
permanent: true,
basePath: false,
},
@@ -132,19 +132,19 @@ const config = {
},
{
source: "/docs/nyx/smart-contracts.html",
destination: "/docs/network/architecture/nyx#smart-contracts",
destination: "/docs/network/infrastructure/nyx#smart-contracts",
permanent: true,
basePath: false,
},
{
source: "/docs/nyx/mixnet-contract.html",
destination: "/docs/network/architecture/nyx#mixnet-contract",
destination: "/docs/network/infrastructure/nyx#mixnet-contract",
permanent: true,
basePath: false,
},
{
source: "/docs/nyx/vesting-contract.html",
destination: "/docs/network/architecture/nyx#vesting-contract",
destination: "/docs/network/infrastructure/nyx#vesting-contract",
permanent: true,
basePath: false,
},
@@ -616,7 +616,7 @@ const config = {
},
{
source: "/docs/architecture/network-overview.html",
destination: "/docs/network/architecture",
destination: "/docs/network/overview",
permanent: true,
basePath: false,
},
@@ -634,7 +634,7 @@ const config = {
},
{
source: "/docs/network/architecture/nyx/smart-contracts/ecash",
destination: "/docs/network/architecture/nyx#zk-nym-contract",
destination: "/docs/network/infrastructure/nyx#zk-nym-contract",
permanent: true,
basePath: false,
},
@@ -1073,6 +1073,198 @@ const config = {
// destination: "https://www.<TODO_EDIT_DESTINATION_BASE>/developers/typescript/FAQ",
// permanent: true,
// },
// ==========================================
// Docs rework redirects (2026)
// ==========================================
// --- Network overview: deleted pages ---
{
source: "/docs/network/overview/two-modes",
destination: "/docs/network/overview/choosing-a-mode",
permanent: true,
basePath: false,
},
{
source: "/docs/network/overview/network-components",
destination: "/docs/network/infrastructure",
permanent: true,
basePath: false,
},
// --- TypeScript SDK: merged pages ---
{
source: "/docs/developers/typescript/overview",
destination: "/docs/developers/typescript",
permanent: true,
basePath: false,
},
{
source: "/docs/developers/typescript/installation",
destination: "/docs/developers/typescript",
permanent: true,
basePath: false,
},
{
source: "/docs/developers/typescript/start",
destination: "/docs/developers/typescript",
permanent: true,
basePath: false,
},
{
source: "/docs/developers/typescript/FAQ",
destination: "/docs/developers/typescript/bundling/bundling",
permanent: true,
basePath: false,
},
// --- Rust SDK: deleted mixnet example subpages ---
{
source: "/docs/developers/rust/mixnet/examples/:path*",
destination: "/docs/developers/rust/mixnet/examples",
permanent: true,
basePath: false,
},
{
source: "/docs/developers/rust/mixnet/message-helpers",
destination: "/docs/developers/rust/mixnet",
permanent: true,
basePath: false,
},
{
source: "/docs/developers/rust/mixnet/message-types",
destination: "/docs/developers/rust/mixnet",
permanent: true,
basePath: false,
},
// --- Rust SDK: deleted client-pool subpages ---
{
source: "/docs/developers/rust/client-pool/architecture",
destination: "/docs/developers/rust/client-pool",
permanent: true,
basePath: false,
},
{
source: "/docs/developers/rust/client-pool/example",
destination: "/docs/developers/rust/client-pool",
permanent: true,
basePath: false,
},
// --- Rust SDK: collapsed TcpProxy subpages ---
{
source: "/docs/developers/rust/tcpproxy/troubleshooting",
destination: "/docs/developers/rust/tcpproxy",
permanent: true,
basePath: false,
},
{
source: "/docs/developers/rust/tcpproxy/tutorial",
destination: "/docs/developers/rust/tcpproxy",
permanent: true,
basePath: false,
},
{
source: "/docs/developers/rust/tcpproxy/architecture",
destination: "/docs/developers/rust/tcpproxy",
permanent: true,
basePath: false,
},
{
source: "/docs/developers/rust/tcpproxy/examples",
destination: "/docs/developers/rust/tcpproxy",
permanent: true,
basePath: false,
},
// --- APIs: flattened structure ---
{
source: "/docs/apis/ns-api/mainnet",
destination: "/docs/apis/ns-api",
permanent: true,
basePath: false,
},
{
source: "/docs/apis/ns-api/sandbox",
destination: "/docs/apis/ns-api",
permanent: true,
basePath: false,
},
{
source: "/docs/apis/ns-api/ns-api-run-deploy",
destination: "/docs/operators/performance-and-testing/ns-api-deployment",
permanent: true,
basePath: false,
},
{
source: "/docs/apis/nym-api/mainnet",
destination: "/docs/apis/nym-api",
permanent: true,
basePath: false,
},
{
source: "/docs/apis/explorer-api/mainnet",
destination: "/docs/apis/explorer-api",
permanent: true,
basePath: false,
},
{
source: "/docs/apis/explorer-api/sandbox",
destination: "/docs/apis/explorer-api",
permanent: true,
basePath: false,
},
{
source: "/docs/apis/cosmos-sdk-nyx/mainnet",
destination: "/docs/apis/cosmos-sdk-nyx",
permanent: true,
basePath: false,
},
{
source: "/docs/apis/cosmos-sdk-nyx/sandbox",
destination: "/docs/apis/cosmos-sdk-nyx",
permanent: true,
basePath: false,
},
// --- Network: archived sections ---
{
source: "/docs/network/architecture",
destination: "/docs/network/overview",
permanent: true,
basePath: false,
},
{
source: "/docs/network/architecture/:path*",
destination: "/docs/network/overview",
permanent: true,
basePath: false,
},
{
source: "/docs/network/concepts",
destination: "/docs/network/overview",
permanent: true,
basePath: false,
},
{
source: "/docs/network/concepts/:path*",
destination: "/docs/network/overview",
permanent: true,
basePath: false,
},
{
source: "/docs/network/traffic",
destination: "/docs/network/overview",
permanent: true,
basePath: false,
},
{
source: "/docs/network/traffic/:path*",
destination: "/docs/network/overview",
permanent: true,
basePath: false,
},
];
},
images: {
+2 -1
View File
@@ -8,7 +8,8 @@
"generate:commands": "../scripts/next-scripts/autodoc.sh",
"generate:tables": "../scripts/next-scripts/python-prebuild.sh",
"predev": "../scripts/next-scripts/python-prebuild.sh",
"build": "next build && next-sitemap",
"generate:llms": "node ../scripts/next-scripts/generate-llms-txt.mjs",
"build": "node ../scripts/next-scripts/generate-llms-txt.mjs && next build && next-sitemap",
"dev": " next dev",
"lint": "next lint",
"lint:fix": "next lint --fix",
+2 -2
View File
@@ -1,9 +1,9 @@
{
"introduction": "Introduction",
"ns-api": "Node Status API",
"nym-api": "NymAPI Validator Sidecar",
"explorer-api": "Explorer API",
"nym-api": "NymAPI",
"cosmos-sdk-nyx": "Validator REST API",
"explorer-api": "Explorer API (Deprecated)",
"---": {
"type": "separator"
},
@@ -1,3 +1,57 @@
---
title: "Nyx Validator REST API - Cosmos SDK Endpoints"
description: "Reference for the Nyx Validator REST API, providing Cosmos SDK endpoints for account balances, staking, governance, and CosmWasm smart contract queries."
schemaType: "TechArticle"
section: "APIs"
lastUpdated: "2026-03-15"
---
import { RedocStandalone } from 'redoc'
# Validator REST API
Since the [Nyx validators](/operators/nodes/validator-setup) are built with the Cosmos SDK, they by default expose a [REST API](https://docs.cosmos.network/api) which can be used to query the state of the chain.
Nyx Validators are built with the [Cosmos SDK](https://docs.cosmos.network/) and expose a standard [REST API](https://docs.cosmos.network/api) for querying chain state directly.
**Key endpoint categories:**
- **Accounts**: balances, transaction history
- **Staking**: validators, delegations, rewards
- **Governance**: proposals, votes
- **CosmWasm**: smart contract queries
For validator setup instructions, see the [Nyx Validator Setup Guide](/operators/nodes/validator-setup).
Other validator endpoints are listed at [cosmos.directory/nyx](https://cosmos.directory/nyx).
## Quick examples
**Query an account balance:**
```bash
curl https://api.nymtech.net/cosmos/bank/v1beta1/balances/n1...
```
**List active validators:**
```bash
curl https://api.nymtech.net/cosmos/staking/v1beta1/validators?status=BOND_STATUS_BONDED
```
## Mainnet endpoints
- **OpenAPI spec:** [api.nymtech.net/swagger/swagger.yaml](https://api.nymtech.net/swagger/swagger.yaml)
- **Swagger UI:** [api.nymtech.net/swagger/](https://api.nymtech.net/swagger/)
## Full API reference
<br />
<RedocStandalone
specUrl="https://api.nymtech.net/swagger/swagger.yaml"
options={{
nativeScrollbars: true,
theme: {
sidebar: {
backgroundColor: '#273239',
textColor: '#FCFDFE'
}
}
}}
/>
@@ -1,5 +0,0 @@
{
"mainnet":"Mainnet Endpoints",
"sandbox":"Sandbox Endpoints"
}
@@ -1,20 +0,0 @@
import { RedocStandalone } from 'redoc';
The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://api.nymtech.net/swagger/swagger.yaml](https://api.nymtech.net/swagger/swagger.yaml) which is also used to generate the Swagger docs deployed at [https://api.nymtech.net/swagger/](https://api.nymtech.net/swagger/).
There is also an overview of other Validator endpoints at [https://cosmos.directory/nyx](https://cosmos.directory/nyx).
<br /><br />
<RedocStandalone
specUrl="https://api.nymtech.net/swagger/swagger.yaml"
options={{
nativeScrollbars: true,
theme: {
sidebar: {
backgroundColor: '#273239',
textColor: '#FCFDFE'
}
}
}}
/>
@@ -1,18 +0,0 @@
import { RedocStandalone } from 'redoc';
The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://api.sandbox.nymtech.net/swagger/swagger.yaml](https://api.sandbox.nymtech.net/swagger/swagger.yaml).
<br /><br />
<RedocStandalone
specUrl="https://api.sandbox.nymtech.net/swagger/swagger.yaml"
options={{
nativeScrollbars: true,
theme: {
sidebar: {
backgroundColor: '#273239',
textColor: '#FCFDFE'
}
}
}}
/>
+34 -3
View File
@@ -1,8 +1,39 @@
---
title: "Explorer API (Deprecated)"
description: "Legacy Explorer API reference for the Nym Mixnet Explorer. Deprecated in favor of the Node Status API."
schemaType: "TechArticle"
section: "APIs"
lastUpdated: "2026-03-15"
---
import { RedocStandalone } from 'redoc'
import { Callout } from 'nextra/components'
# Explorer API
The Explorer API is the backend for the [Mixnet Explorer](https://nym.com/explorer).
<Callout type="warning">
The Explorer API is deprecated. Use the [Node Status API](/apis/ns-api) instead, which provides the same data and more.
</Callout>
**This will soon be deprecated in favour of the [Node Status API](ns-api.mdx).**
The Explorer API is the legacy backend for the [Mixnet Explorer](https://nym.com/explorer).
## Mainnet endpoints
- **OpenAPI spec:** [explorer.nymtech.net/api/v1/openapi.json](https://explorer.nymtech.net/api/v1/openapi.json)
- **Swagger UI:** [explorer.nymtech.net/api/swagger/index.html](https://explorer.nymtech.net/api/swagger/index.html)
<br />
<RedocStandalone
specUrl="https://explorer.nymtech.net/api/v1/openapi.json"
options={{
nativeScrollbars: true,
theme: {
sidebar: {
backgroundColor: '#273239',
textColor: '#FCFDFE'
}
}
}}
/>
The code for this service can be found [in our monorepo](https://github.com/nymtech/nym/tree/develop/explorer).
@@ -1,5 +0,0 @@
{
"mainnet":"Mainnet Endpoints",
"sandbox":"Sandbox Endpoints"
}
@@ -1,19 +0,0 @@
import { RedocStandalone } from 'redoc';
The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://explorer.nymtech.net/api/v1/openapi.json](https://explorer.nymtech.net/api/v1/openapi.json) which is also used to generate the Swagger docs deployed at [https://explorer.nymtech.net/api/swagger/index.html](https://explorer.nymtech.net/api/swagger/index.html).
<br /><br />
<RedocStandalone
specUrl="https://sandbox-explorer.nymtech.net/api/v1/openapi.json"
options={{
nativeScrollbars: true,
theme: {
sidebar: {
backgroundColor: '#273239',
textColor: '#FCFDFE'
}
}
}}
/>
@@ -1,18 +0,0 @@
import { RedocStandalone } from 'redoc';
The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://sandbox-explorer.nymtech.net/api/v1/openapi.json](https://sandbox-explorer.nymtech.net/api/v1/openapi.json) which is also used to generate the Swagger docs deployed at [https://sandbox-explorer.nymtech.net/api/swagger/index.html](https://sandbox-explorer.nymtech.net/api/swagger/index.html).
<br /><br />
<RedocStandalone
specUrl="https://sandbox-explorer.nymtech.net/api/v1/openapi.json"
options={{
nativeScrollbars: true,
theme: {
sidebar: {
backgroundColor: '#273239',
textColor: '#FCFDFE'
}
}
}}
/>
+51 -6
View File
@@ -1,13 +1,58 @@
---
title: "Nym API Reference: Network Infrastructure"
description: "Interactive API documentation for Nym network infrastructure. Query node status, network topology, blockchain state & mixnet performance programmatically."
title: "Nym Network APIs Overview"
description: "Overview of Nym HTTP APIs for querying node performance, token supply, credential data, and blockchain state, with guidance on which API to use."
schemaType: "TechArticle"
section: "APIs"
lastUpdated: "2026-02-01"
lastUpdated: "2026-03-15"
---
# Introduction
import { Callout } from 'nextra/components'
This site contains interactive APIs generated from the OpenAPI specs of various API endpoints offered by bits of Nym infrastructure run both by Nym and community operators for both Mainnet and the Sandbox testnet.
# APIs
You can find links to the generated specs for each API on their respective pages as well as their different uses for operators and developers.
The Nym network exposes several HTTP APIs for querying network state, node performance, blockchain data, and credential information. Each API serves a different purpose.
## Which API should I use?
| I want to... | API |
|---|---|
| Query node performance, roles, and mixnet statistics | [Node Status API](/apis/ns-api) |
| Query circulating NYM supply or zk-nym credential data | [NymAPI](/apis/nym-api) |
| Query blockchain state, account balances, or transactions | [Validator REST API](/apis/cosmos-sdk-nyx) |
| Query legacy explorer data | [Explorer API](/apis/explorer-api) *(deprecated)* |
## Node Status API
The primary API for querying node information. It serves data about individual `nym-node` instances: roles, statistics, Network Requester services, and overall mixnet summaries. If you're building an explorer, analytics dashboard, or monitoring tool, start here.
<Callout type="info">
If you're building a service that makes heavy use of the Node Status API, consider [running your own instance](/operators/performance-and-testing/ns-api-deployment) to distribute load across the network.
</Callout>
**Endpoints:** [OpenAPI spec](https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json) · [Swagger](https://mainnet-node-status-api.nymtech.cc/swagger/)
## NymAPI
A sidecar binary operated by Nyx Validators. It caches smart contract data and exposes endpoints for circulating NYM supply, zk-nym credential data, and ticketbook information. Use this when you need token economics or credential data.
**Endpoints:** [OpenAPI spec](https://validator.nymtech.net/api-docs/openapi.json) · [Swagger](https://validator.nymtech.net/api/swagger/index.html)
Other NymAPI instances are listed at [cosmos.directory/nyx](https://cosmos.directory/nyx).
## Validator REST API
The standard Cosmos SDK REST API exposed by Nyx Validators. Use this for direct chain queries: account balances, transaction history, governance proposals, and staking information.
**Endpoints:** [OpenAPI spec](https://api.nymtech.net/swagger/swagger.yaml) · [Swagger](https://api.nymtech.net/swagger/)
Other validator endpoints are listed at [cosmos.directory/nyx](https://cosmos.directory/nyx).
## Explorer API
<Callout type="warning">
The Explorer API is deprecated. Use the [Node Status API](/apis/ns-api) instead.
</Callout>
The legacy backend for the [Mixnet Explorer](https://nym.com/explorer). This API is being replaced by the Node Status API, which provides the same data and more.
**Endpoints:** [OpenAPI spec](https://explorer.nymtech.net/api/v1/openapi.json) · [Swagger](https://explorer.nymtech.net/api/swagger/index.html)
+51 -2
View File
@@ -1,8 +1,57 @@
---
title: "Node Status API - Nym Node Performance and Mixnet Stats"
description: "Reference for the Node Status API, which provides nym-node identity, performance scores, role assignments, and mixnet health summaries."
schemaType: "TechArticle"
section: "APIs"
lastUpdated: "2026-03-15"
---
import { RedocStandalone } from 'redoc'
import { Callout } from 'nextra/components'
# Node Status API
The Node Status API serves information about individual `nym-nodes` in the Mixnet, such as which role they are operating in, statistics about them, services such as Network Requesters, as well as summaries of the state of the Mixnet.
The Node Status API serves information about individual `nym-node` instances in the Nym network: which role they are operating in, performance statistics, Network Requester services, and summaries of the overall mixnet state. It is the primary API for anyone building explorers, analytics dashboards, or monitoring tools.
**Key endpoint categories:**
- **Node information**: identity, bonding status, declared roles, build version, host details
- **Performance scores**: routing reliability, configuration scores, probe results
- **Mixnet summaries**: active set composition, role distribution, network health
<Callout type="info">
We recommend that developers building applications such as explorers or analytics interfaces about the Mixnet run their own instance of the API, in order to promote a robust network of downstream services, and spread the load of API calls amongst as many endpoints as possible.
If you're building a service that makes heavy use of this API, consider [running your own instance](/operators/performance-and-testing/ns-api-deployment) to distribute load and promote a robust network of downstream services.
</Callout>
## Quick examples
**Get a summary of all gateways:**
```bash
curl https://mainnet-node-status-api.nymtech.cc/api/v1/gateways
```
**Get details for a specific node by identity key:**
```bash
curl https://mainnet-node-status-api.nymtech.cc/api/v1/gateways/23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb
```
## Mainnet endpoints
- **OpenAPI spec:** [mainnet-node-status-api.nymtech.cc/api-docs/openapi.json](https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json)
- **Swagger UI:** [mainnet-node-status-api.nymtech.cc/swagger/](https://mainnet-node-status-api.nymtech.cc/swagger/)
## Full API reference
<br />
<RedocStandalone
specUrl="https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json"
options={{
nativeScrollbars: true,
theme: {
sidebar: {
backgroundColor: '#273239',
textColor: '#FCFDFE'
}
}
}}
/>
@@ -1,5 +0,0 @@
{
"ns-api-run-deploy":"Run Instance",
"mainnet":"Mainnet Endpoints",
"sandbox":"Sandbox Endpoints"
}
@@ -1,20 +0,0 @@
import { RedocStandalone } from 'redoc';
import { Callout } from 'nextra/components'
The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json](https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json) which is also used to generate the Swagger docs deployed at [https://mainnet-node-status-api.nymtech.cc/swagger/](https://mainnet-node-status-api.nymtech.cc/swagger/).
<br /><br />
<RedocStandalone
specUrl="https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json"
options={{
nativeScrollbars: true,
theme: {
sidebar: {
backgroundColor: '#273239',
textColor: '#FCFDFE'
}
}
}}
/>
@@ -1,18 +0,0 @@
import { RedocStandalone } from 'redoc';
The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://sandbox-node-status-api.nymte.ch/api-docs/openapi.json](https://sandbox-node-status-api.nymte.ch/api-docs/openapi.json) which is also used to generate the Swagger docs deployed at [https://sandbox-node-status-api.nymte.ch/swagger/](https://sandbox-node-status-api.nymte.ch/swagger/).
<br /><br />
<RedocStandalone
specUrl="https://sandbox-node-status-api.nymte.ch/api-docs/openapi.json"
options={{
nativeScrollbars: true,
theme: {
sidebar: {
backgroundColor: '#273239',
textColor: '#FCFDFE'
}
}
}}
/>
+60 -2
View File
@@ -1,5 +1,63 @@
---
title: "NymAPI - Token Supply and Credential Endpoints"
description: "Reference for the NymAPI, a validator sidecar that caches Nyx blockchain data and exposes endpoints for circulating NYM supply, zk-nym credentials, and network topology."
schemaType: "TechArticle"
section: "APIs"
lastUpdated: "2026-03-15"
---
import APITable from 'components/api-table.tsx'
import { RedocStandalone } from 'redoc'
import { Callout } from 'nextra/components'
# NymAPI
The [NymAPI](/operators/nodes/validator-setup/nym-api) is a sidecar binary operated by members of the Nym Validator set. Amongst other things, the API offers endpoints to query regarding circulating `NYM` supply, and global and ticketbook-specific [zk-nym](/network/cryptography/zk-nym) data. This is all information contained in various smart contracts on the Nyx blockchain; the NymAPI caches this information periodically to make queries faster and more scalable.
The NymAPI is a sidecar binary operated by members of the Nyx Validator set. It caches smart contract data from the Nyx blockchain and exposes it via HTTP endpoints, making queries faster and more scalable than querying the chain directly.
The code for this service can be found [in our monorepo](https://github.com/nymtech/nym/tree/develop/nym-api).
**Key endpoint categories:**
- **Token economics:** circulating NYM supply, staking information
- **Credential data:** zk-nym ticketbook information, global and per-ticketbook data
- **Network topology:** cached node and mixnet data from on-chain contracts
For operator setup instructions, see the [NymAPI Operator Guide](/operators/nodes/validator-setup/nym-api).
## Quick examples
**Get the circulating NYM supply:**
```bash
curl https://validator.nymtech.net/api/v1/circulating-supply
```
**Get the current network topology (Mix Nodes and gateways):**
```bash
curl https://validator.nymtech.net/api/v1/mixnodes/active
```
## Mainnet endpoints
- **OpenAPI spec:** [validator.nymtech.net/api-docs/openapi.json](https://validator.nymtech.net/api-docs/openapi.json)
- **Swagger UI:** [validator.nymtech.net/api/swagger/index.html](https://validator.nymtech.net/api/swagger/index.html)
<Callout type="info">
Other NymAPI instances are available. You can find their Swagger endpoints here:
<APITable endpoint="https://api.nymtech.net/cosmwasm/wasm/v1/contract/n19604yflqggs9mk2z26mqygq43q2kr3n932egxx630svywd5mpxjsztfpvx/smart/eyJnZXRfY3VycmVudF9kZWFsZXJzIjogeyJsaW1pdCI6IDMwfX0=" />
There is also an overview of endpoints at [cosmos.directory/nyx](https://cosmos.directory/nyx).
</Callout>
## Full API reference
<br />
<RedocStandalone
specUrl="https://validator.nymtech.net/api-docs/openapi.json"
options={{
nativeScrollbars: true,
theme: {
sidebar: {
backgroundColor: '#273239',
textColor: '#FCFDFE'
}
}
}}
/>
@@ -1,3 +0,0 @@
{
"mainnet":"Mainnet Endpoints"
}
@@ -1,27 +0,0 @@
import APITable from 'components/api-table.tsx';
import { RedocStandalone } from 'redoc';
import { Callout } from 'nextra/components'
You can find the OpenAPI spec found at [https://validator.nymtech.net/api-docs/openapi.json](https://validator.nymtech.net/api-docs/openapi.json) which is also used to generate the Swagger docs deployed at [https://validator.nymtech.net/api/swagger/index.html](https://validator.nymtech.net/api/swagger/index.html).
<Callout type="info" emoji="️">
You can find the Swagger endpoints of other NymAPI instances at the following endpoints:
<APITable endpoint="https://api.nymtech.net/cosmwasm/wasm/v1/contract/n19604yflqggs9mk2z26mqygq43q2kr3n932egxx630svywd5mpxjsztfpvx/smart/eyJnZXRfY3VycmVudF9kZWFsZXJzIjogeyJsaW1pdCI6IDMwfX0=" />
There is also an overview of endpoints at [https://cosmos.directory/nyx](https://cosmos.directory/nyx).
</Callout>
<br /><br />
<RedocStandalone
specUrl="https://validator.nymtech.net/api-docs/openapi.json"
options={{
nativeScrollbars: true,
theme: {
sidebar: {
backgroundColor: '#273239',
textColor: '#FCFDFE'
}
}
}}
/>
@@ -16,12 +16,12 @@
"typescript": "Typescript SDK",
"---": {
"type": "separator",
"title": "Misc"
"title": "Extras"
},
"nymvpncli": "Nym VPN CLI",
"chain": "Interacting with Nyx Blockchain",
"tools": "Tools",
"nymvpncli": "Nym VPN CLI",
"clients": "Standlone Clients",
"clients": "Standalone Clients",
"----": {
"type": "separator"
},
@@ -1,9 +1,11 @@
# Archive page: NymConnect Setup
```admonish warning
Since the beginning of 2024 NymConnect is no longer maintained. Nym is developing a new client called [NymVPN](https://nymvpn.com), an application routing all users traffic thorugh the mixnet.
If users want to route their traffic through socks5 we advice to use maintained [Nym Socks5 Client](../clients/socks5/setup).
```
import { Callout } from 'nextra/components'
<Callout type="warning">
NymConnect is no longer maintained as of early 2024. Nym is developing a new client called [NymVPN](https://nymvpn.com), an application routing all user traffic through the Mixnet.
If you want to route traffic through SOCKS5, use the maintained [Nym Socks5 Client](../clients/socks5/setup).
</Callout>
In case you want to run deprecated NymConnect, follow these steps:
@@ -1,45 +1,43 @@
---
title: "Browser-Based App Integration"
description: "Build privacy-preserving browser apps with mixFetch and the Nym WASM SDK. Route HTTP requests and messages through the mixnet from the browser."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-04-07"
---
import { Callout } from 'nextra/components';
# Browser-Based Apps
Browsers are a very restricted environment to work in, with limited options for external communications (websockets, Web Transport API, WebRTC), mixed content restrictions (HTTPS-only), and no access to the file system or any syscalls. These aside, the main issue when trying to capture traffic and send it via a different transport - such as the Mixnet - is the lack of access to browser TLS negotiation from JS or the CA certificate store.
This means that the functionality offered by our current browser-based solutions are quite restricted / specific. There are currently two options for interacting with the Mixnet from the browser: `mixFetch`, and the WASM SDK.
Browsers are a restricted environment: communication is limited to WebSockets, Web Transport, and WebRTC; mixed content policies enforce HTTPS-only; and there is no access to the filesystem or system calls. The main obstacle for routing traffic through the Mixnet is the lack of access to browser TLS negotiation or the CA certificate store from JavaScript.
Two integration options are available, both delivered as packages bundled into your web application.
![](/images/developers/nym-browser-arch.png)
Both `mixFetch` and the WASM client are delivered to the client bundled into a web application.
## mixFetch
Drop-in replacement for browser's `fetch` API that makes HTTP(S) requests via Exit Gateways using the SOCKS Network Requester.
Uses an embedded CA certificate store to establish TLS session between `mixFetch` and the remote host, creating a client-host secure channel from the browser to the host over the Mixnet.
A drop-in replacement for the browser `fetch` API that makes HTTP(S) requests via Exit Gateways using the SOCKS Network Requester. It ships with an embedded CA certificate store to establish a TLS session between `mixFetch` and the remote host, creating a secure channel from the browser to the destination over the Mixnet.
Internally it uses the WASM client.
Internally, `mixFetch` uses the WASM client.
- [docs](./typescript/start#mixfetch)
- [example](./typescript/playground/mixfetch)
- [Docs](./typescript#mixfetch)
- [Example](./typescript/playground/mixfetch)
<Callout type="info">
### Current Limitations of `mixFetch`
`mixFetch` can currently only perform 10 concurrent requests (i.e. in-flight requests where a request has been sent to a remote endpoint, but no result has been recieved).
`mixFetchv2` - which will act more like a general-purpose userspace IP stack - is currently in development.
It is shipped with a pre-bundled CA store.
`mixFetch` currently supports a maximum of 10 concurrent in-flight requests. `mixFetchv2`, which will function as a general-purpose userspace IP stack, is in development.
</Callout>
## WASM Client
Makes Sphinx packets and cover traffic using WASM and sent over a Websocket to the Entry Gateway and receive responses.
This only works in messaging mode (i.e. messages sent either as text or binary data), and currently doesnt support making IP packets that are routed to the Internet by an Exit Gateway IPR, nor does it currently expose any stream-like API. If you want to send HTTP(S) requests, use `mixFetch`.
Constructs Sphinx packets and cover traffic in WASM, sent over a WebSocket to the Entry Gateway. Responses arrive the same way.
Note that the limitations of CSPs and Mixed Content restrictions (i.e HTTPS only) apply to the Websocket connection as normal in browsers or embedded WebViews.
This operates in messaging mode only (text or binary payloads) and does not currently support IP packet routing via the Exit Gateway IPR or any stream-like API. For HTTP(S) requests, use `mixFetch`.
Runs in a web worker to leave UI thread free for the user.
Standard browser CSP and mixed content restrictions (HTTPS only) apply to the WebSocket connection, including in embedded WebViews.
- [docs](./typescript/start#mixnet-client)
- [example](./typescript/playground/traffic)
The client runs in a web worker to keep the UI thread free.
- [Docs](./typescript#mixnet-client)
- [Example](./typescript/playground/traffic)
+1 -1
View File
@@ -13,7 +13,7 @@ There are two options for interacting with the blockchain to send tokens or inte
* `nyxd` binary
## Nym-CLI tool (recommended in most cases)
The `nym-cli` tool is a binary offering a simple interface for interacting with deployed smart contract (for instance, bonding and unbonding a mix node from the CLI), as well as creating and managing accounts and keypairs, sending tokens, and querying the blockchain.
The `nym-cli` tool is a binary offering a simple interface for interacting with deployed smart contract (for instance, bonding and unbonding a Mix Node from the CLI), as well as creating and managing accounts and keypairs, sending tokens, and querying the blockchain.
Instructions on how to do so can be found on the [`nym-cli` docs page](./tools/nym-cli)
@@ -44,7 +44,7 @@ nyxd tx bank send ledger_account $DESTINATION_ACCOOUNT 1000000unym --ledger --no
> When a command is run, the transaction will appear on the Ledger device and will require physical confirmation from the device before being signed.
## Nym-specific transactions
Nym-specific commands and queries, like bonding a mix node or delegating unvested tokens, are available in the `wasm` module, and follow the following pattern:
Nym-specific commands and queries, like bonding a Mix Node or delegating unvested tokens, are available in the `wasm` module, and follow the following pattern:
```
# Executing commands
@@ -59,8 +59,8 @@ You can find the value of `$CONTRACT_ADDRESS` in the [`network defaults`](https:
The value of `$JSON_MSG` will be a blog of `json` formatted as defined for each command and query. You can find these definitions for the mixnet smart contract [here](https://github.com/nymtech/nym/blob/master/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs) and for the vesting contract [here](https://github.com/nymtech/nym/blob/master/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs) under `ExecuteMsg` and `QueryMsg`.
### Example command execution:
#### Delegate to a mix node
You can delegate to a mix node from the CLI using `nyxd` and signing the transaction with your ledger by filling in the values of this example:
#### Delegate to a Mix Node
You can delegate to a Mix Node from the CLI using `nyxd` and signing the transaction with your ledger by filling in the values of this example:
```
CONTRACT_ADDRESS=mixnet_contract_address
@@ -28,7 +28,7 @@ Before you can use the client, you need to initalise a new instance of it, which
The `--id` in the example above is a local identifier so that you can name your clients and keep track of them on your local system; it is **never** transmitted over the network.
The `--use-reply-surbs` field denotes whether you wish to send [SURBs](/network/concepts/anonymous-replies) along with your request. It defaults to `false`, we are explicitly setting it as `true`. It defaults to `false` for compatibility with versions of the pre-smoosh `nym-network-requester` binary which will soon be deprecated.
The `--use-reply-surbs` field denotes whether you wish to send [SURBs](/network/mixnet-mode/anonymous-replies) along with your request. It defaults to `false`, we are explicitly setting it as `true`. It defaults to `false` for compatibility with versions of the pre-smoosh `nym-network-requester` binary which will soon be deprecated.
The `--provider` field needs to be filled with the Nym address of an Exit Gateway that can make network requests on your behalf. You can select one from the [mixnet explorer](https://nym.com/explorer) by copying its `Client ID` and using this as the value of the `--provider` flag. Alternatively, you could use [Harbourmaster](https://harbourmaster.nymtech.net/).
@@ -46,7 +46,7 @@ Here is an example of setting the proxy connecting in Blockstream Green:
![Blockstream Green settings](/images/developers/blockstream-green.gif)
Most wallets and other applications will work basically the same way: find the network proxy settings, enter the proxy url (host: **localhost**, port: **1080**).
Most wallets and other applications work the same way: find the network proxy settings and enter the proxy url (host: **localhost**, port: **1080**).
In some other applications, this might be written as **localhost:1080** if there's only one proxy entry field.
@@ -1,13 +1,52 @@
# Websocket Client (Standalone)
# WebSocket Client (Standalone)
> This client can also be utilised via the [Rust SDK](../rust) and [Go/C++ FFI](../rust/ffi).
> This client can also be used via the [Rust SDK](../rust) and [Go/C++ FFI](../rust/ffi).
You can run this client as a standalone process and pipe traffic into it to be sent through the mixnet. This is useful if you're building an application in a language other than Typescript or Rust and cannot utilise one of the SDKs.
The standalone WebSocket client connects to the Nym Mixnet and exposes a WebSocket interface on localhost. Applications in any language can connect to this WebSocket to send and receive messages through the Mixnet.
You can find the code for this client [here](https://github.com/nymtech/nym/tree/master/clients/native).
This is useful if you're building an application in a language other than TypeScript or Rust and cannot use one of the SDKs directly. Your application connects to the local WebSocket, and the client handles Sphinx packet construction, gateway registration, and key management.
## Download or compile client
## Download or compile
If you are using OSX or a Debian-based operating system, you can download the `nym-socks5-client` binary from our [Github releases page](https://github.com/nymtech/nym/releases).
Pre-built binaries for macOS and Debian-based Linux are available on the [GitHub releases page](https://github.com/nymtech/nym/releases). Look for the `nym-client` binary.
If you are using a different operating system, or want to build from source, simply use `cargo build --release` from the root of the Nym monorepo.
To build from source:
```bash
git clone https://github.com/nymtech/nym.git
cd nym
cargo build --release -p nym-client
```
The binary will be at `target/release/nym-client`.
## Initialize and run
```bash
# Create a new client identity
./nym-client init --id my-client
# Start the client
./nym-client run --id my-client
```
The client prints its Nym address on startup and opens a WebSocket on `ws://127.0.0.1:1977`.
## Sending and receiving
Connect to `ws://127.0.0.1:1977` from your application. Messages are JSON-formatted:
**Send a message:**
```json
{
"type": "send",
"message": "hello",
"recipient": "<recipient-nym-address>"
}
```
**Receive messages:** The client pushes incoming messages to your WebSocket connection as they arrive through the Mixnet.
## Source code
The client source is in the [Nym monorepo](https://github.com/nymtech/nym/tree/master/clients/native).
@@ -13,5 +13,5 @@ All of these code examples will do the following:
By varying the message content, you can easily build sophisticated service provider apps. For example, instead of printing the response received from the mixnet, your service provider might take some action on behalf of the user - perhaps initiating a network request, a blockchain transaction, or writing to a local data store.
<!-- THIS PAGE IS NOT WORKING AT THE MOMENT:
> You can find an example of building both frontend and service provider code with the websocket client in the [Simple Service Provider Tutorial](https://nymtech.net/developers/tutorials/simple-service-provider/simple-service-provider.html) in the Developer Portal.
> You can find an example of building both frontend and service provider code with the websocket client in the [Simple Service Provider Tutorial](https://nym.com/developers/tutorials/simple-service-provider/simple-service-provider.html) in the Developer Portal.
-->
@@ -54,7 +54,7 @@ In some applications, e.g. where people are chatting with friends who they know,
**If that fits your security model, good. However, will probably be the case that you want to send anonymous replies using Single Use Reply Blocks (SURBs)**.
You can read more about SURBs [here](/network/concepts/anonymous-replies) but in short they are ways for the receiver of this message to anonymously reply to you - the sender - **without them having to know your client address**.
You can read more about SURBs [here](/network/mixnet-mode/anonymous-replies) but in short they are ways for the receiver of this message to anonymously reply to you - the sender - **without them having to know your client address**.
Your client will send along a number of `replySurbs` to the recipient of the message. These are pre-addressed Sphinx packets that the recipient can write to the payload of (i.e. write response data to), but not view the final destination of. If the recipient is unable to fit the response data into the bucket of SURBs sent to it, it will use a SURB to request more SURBs be sent to it from your client.
@@ -1,13 +1,21 @@
---
title: "Nym Client Message Queue and Cover Traffic"
description: "How the Nym client queues messages, sends cover traffic via Poisson processes, and manages Sphinx packet streams to prevent timing attacks."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-03-15"
---
import { Callout } from 'nextra/components'
# Message Queue
<Callout type="info">
Although good to understand how the Nym Client works under the hood, this information is only of practical use if you're using the `Mixnet` module of the RustSDK and interacting with the SDK Mixnet Client at a low level. This is all mostly abstracted away with the `MixSocket`, `MixStream`, and `IpMixStream` abstractions.
Although useful for understanding how the Nym Client works internally, this information is only of practical use if you are using the [`Mixnet`](../rust/mixnet) module of the Rust SDK and interacting with the client at a low level. Most of this is abstracted away by the [`Stream`](../rust/stream) module (`AsyncRead + AsyncWrite` channels) and the [`TcpProxy`](../rust/tcpproxy) module (TCP tunnelling with message ordering).
</Callout>
## Sphinx Packet Streams
Clients, once connected to the Mixnet, **are always sending traffic into the Mixnet**; as well as the packets that you as a developer are sending from your application logic, they send [cover traffic](../../network/concepts/cover-traffic) at a constant rate defined by a Poisson process. This is part of the network's mitigation of timing attacks.
Clients, once connected to the Mixnet, **are always sending traffic into the Mixnet**; as well as the packets that you as a developer are sending from your application logic, they send [cover traffic](/network/mixnet-mode/cover-traffic) at a constant rate defined by a Poisson process. This is part of the network's mitigation of timing attacks.
There are two constant streams of sphinx packets leaving the client at the rate defined by the Poisson process.
- one that is solely cover traffic
+19 -2
View File
@@ -6,5 +6,22 @@ section: "Developers"
lastUpdated: "2026-02-01"
---
# Introduction
Nym's developer documentation covering core concepts of integrating with the Mixnet, interacting with the Nyx blockchain, an overview of the avaliable tools, and our SDK docs.
# Developer Documentation
Build applications that protect user metadata using the Nym Mixnet. This section covers SDK integration, blockchain interaction, and developer tools.
## Where to start
**Choosing an integration approach:** read [Integrations](/developers/integrations) to understand the architectural trade-offs (native SDK vs proxy vs mixFetch), then pick your path:
- **[Rust SDK](/developers/rust):** full-featured SDK with message passing, `AsyncRead`/`AsyncWrite` streams, and client pooling. Start with the [Tour](/developers/rust/tour).
- **[TypeScript SDK](/developers/typescript):** browser and Node.js SDK for mixFetch, Mixnet client, and smart contract interaction.
- **[Standalone Clients](/developers/clients):** language-agnostic SOCKS5 and WebSocket proxies for piping traffic through the Mixnet without an SDK.
## Blockchain interaction
The Nym Network is coordinated by the [Nyx blockchain](/network/infrastructure/nyx). To query chain state, submit transactions, or interact with smart contracts, see [Chain Interaction](/developers/chain).
## API reference
Auto-generated API specs for Nym infrastructure endpoints are in the [APIs section](/apis/introduction).
@@ -1,16 +1,34 @@
---
title: "Integrating With Nym"
description: "Choose an integration path for sending application traffic through the Nym mixnet, depending on your runtime environment and architecture."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-04-07"
---
import { Callout } from 'nextra/components';
# Integrating With Nym
Any application that wants to integrate with Nym involves sending its application traffic through the Mixnet using one of the available Nym Clients. There is no single solution for this, as different environments offer different access and transport options (e.g. if operating in a web browser, you do not have access to syscalls or sockets, have to deal with content security policies, etc).
As such, we have several solutions available for developers to choose from depending on the **environment** their application is expected to run in: native apps which are running on a desktop, or webapps running in a browser.
Any application that integrates with Nym sends its traffic through the Mixnet via a Nym client. The right integration path depends on two factors: **environment** and **architecture**.
<Callout type="info" emoji="️">
The list of current options available to developers to do not cover all environments and setups - we are working on expanding this list and approaching more general solutions, but there is no one-size-fits-all approach when dealing with rerouting network traffic.
## Environment
Different runtimes have different transport constraints. A browser cannot open raw sockets or access the filesystem; a desktop app can.
- **Native / Desktop**: full access to system networking and persistent storage. Use the [Rust SDK](./rust).
- **Browser**: restricted to WebSockets, Web Transport, and `fetch`, with HTTPS-only mixed content rules and no filesystem access. Use the [TypeScript SDK](./typescript).
## Architecture
The second factor is whether you control both sides of the communication.
**End-to-end (E2E)**: both sides run Nym clients. All traffic stays Sphinx-encrypted the entire way. Appropriate for peer-to-peer setups or any case where you control both endpoints.
**Proxy**: only the client side runs Nym. Traffic exits the Mixnet at an Exit Gateway and continues to the destination as normal internet traffic. Appropriate when connecting to third-party services (blockchain RPCs, external APIs) that you do not control.
<Callout type="warning">
In proxy mode, the last hop from Exit Gateway to the remote host travels as standard internet traffic. This is weaker than E2E against a global passive adversary, but still provides timing obfuscation and sender-receiver unlinkability.
</Callout>
Integration options are then further subdivided by app **architecture**; whether the application interacts with remote hosts on the public internet running independently of the app (e.g. public blockchain RPC endpoints, third-party APIs) or whether app developers have some control over the versions of the software being run on both sides of an interaction (e.g. peer to peer apps running the same software version, or client-server architectures which are running software written by the same team).
<Callout type="info" emoji="️">
This is because of the different security considerations each option offers. These are detailed in the following pages.
</Callout>
See the [Native / Desktop](./native) and [Browser](./browsers) pages for the specific modules available in each environment.
@@ -1,8 +1,8 @@
# Licensing
As a general approach, licensing is as follows this pattern:
As a general approach, licensing follows this pattern:
* <p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"><a property="dct:title" rel="cc:attributionURL" href="https://nym.com/docs">Nym Documentation</a> by <a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://nym.com">Nym Technologies</a> is licensed under <a href="http://creativecommons.org/licenses/by-nc-sa/4.0/?ref=chooser-v1" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY-NC-SA 4.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg?ref=chooser-v1"></a></p>
* [Nym Documentation](https://nym.com/docs) by [Nym Technologies](https://nym.com) is licensed under [CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) ![CC](/images/cc-icons/cc.svg) ![BY](/images/cc-icons/by.svg) ![NC](/images/cc-icons/nc.svg) ![SA](/images/cc-icons/sa.svg)
* Nym applications and binaries are [GPL-3.0-only](https://www.gnu.org/licenses/)
+31 -54
View File
@@ -1,86 +1,63 @@
---
title: "Native and Desktop App Integration"
description: "Integrate privacy into native desktop apps and CLIs using the Nym Rust SDK. Choose between end-to-end mixnet messaging or TCP proxy approaches."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-03-15"
---
import { Callout } from 'nextra/components';
# Native / Desktop Apps
Developers wanting to integrate into desktop apps & CLIs can use our Rust SDK. There are two broad approaches to using the Mixnet (E2E or as a proxy), with different modules suited for each, each with their own specific usecase and limitations.
Desktop apps and CLIs integrate via the [Rust SDK](./rust). There are two broad approaches: embedding Nym clients on both sides of the communication (E2E), or using the Mixnet as a proxy to reach external services.
## Option 1: Mixnet End-To-End
You might want to embed Nym Clients in both sides of your app, and have them send all of your app network traffic through the Mixnet: maybe two clients in a peer to peer setup, or a client and a server where it is possible for you as a developer to release both the client and server side code, and have some ability to make sure that it is being run.
Both sides of your app run Nym clients. All traffic stays Sphinx-encrypted the entire way. Works for peer-to-peer setups or any case where you control both ends.
![](/images/developers/nym-arch-client-to-client.png)
There are several options available:
### Stream Module
The [Stream module](./rust/stream) provides `AsyncRead + AsyncWrite` byte streams multiplexed over the mixnet, the closest analogue to TCP sockets.
{ /* ### Stream Wrapper Module
Exposes `MixSocket`/`MixStream` abstractions that can be split a reader/writer halves that consumes bytes, with an interface inspired by `std::net::TcpStream`. For developers who just want to read/write bytes to/from the Mixnet working with something socket-like.
- docs TODO LINK
- example TODO LINK */ }
- [docs](./rust/stream)
- [tutorial](./rust/stream/tutorial)
### Mixnet & Client Pool Modules
The Mixnet module of the SDK exposes low level connection functionality and the Mixnet Client. The Client Pool is one answer to concurrency, and allows developers to run several Nym Clients at once which can be quickly used.
{ /* This approach might be useful if you want to build custom connection logic, but **`MixSocket`/`MixStream` will probably be sufficient for the majority of usecases** where developers just want to send and receive traffic as streams. */ }
This approach might be useful if you want to build custom connection logic, but the TcpProxy Module will probably be sufficient for the majority of usecases where developers just want to send and receive traffic as streams.
The [Mixnet module](./rust/mixnet) exposes the raw message API and `MixnetClient`. The [Client Pool](./rust/client-pool) maintains pre-connected clients for bursty workloads. These are appropriate when you need full control over the communication model.
- [docs](./rust/mixnet)
- [examples](./rust/mixnet/examples)
- [tutorial](./rust/mixnet/tutorial)
### TcpProxy Module
{ /* <Callout type="warning" emoji="⚠️">
This module has been superseded by the Stream Wrapper module, and will soon be deprecated. **New features will not be added to it.** The main drawback of this (which is fixed with `MixSocket`/`MixStream`) is that TLS is impossible, as it exposes a localhost port for the consuming process to communicate with.
</Callout> */ }
A pair of abstractions built for use in a client-server setup, which both expose a `localhost` TCP Socket which apps can read/write bytes to/from.
### TcpProxy Module (Unmaintained)
- [docs](./rust/tcpproxy)
- [examples](./rust/tcpproxy/examples/singleconn)
<Callout type="info" emoji="️">
There is a new abstraction coming soon mirroring the interface and use of a TCP Socket, making it easier for developers to use the Mixnet, and also perform TLS through a Mixnet connection. Stay tuned.
<Callout type="error">
**This module is unmaintained.** Use the [Stream module](./rust/stream) for new projects. Existing users should plan to migrate when possible.
</Callout>
Exposes localhost TCP sockets that proxy traffic through the mixnet.
- [docs](./rust/tcpproxy)
## Option 2: Mixnet-As-Proxy
For developers who are only able to control the client-side code, and/or need to communicate with a 3rd party service, such as a public blockchain RPC or a remote host they do not control.
For cases where you only control the client side and need to reach a third-party service such as a blockchain RPC or remote API.
![](/images/developers/nym-arch-ip-routing.png)
<Callout type="warning">
### Security Considerations
### Security Considerations
Since traffic is only packaged as Sphinx until it gets to the Exit Gateway, where it is unwrapped into either HTTPS packets (by a Network Requester) or IP packets (by an IP Packet Router), the last hop between the Gateway and the remote host **travels as normal internet traffic**.
Traffic is Sphinx-encrypted until the Exit Gateway, where it's unwrapped into HTTPS (Network Requester) or raw IP (IP Packet Router). The last hop to the remote host **travels as normal internet traffic**.
As such, this option has fewer protections than the E2E option against a global passive adversary, but still grants you timing obfuscation and sender-receiver unlinkability between your client software and whatever service it is interacting with.
Weaker than E2E against a global passive adversary, but you still get timing obfuscation and sender-receiver unlinkability between your client and the remote service.
</Callout>
### SOCKS Client
Developers with apps that support SOCKS4,4a, or 5 can use the Socks Client exposed by the Mixnet module. This uses the Network Requester service of the chosen Exit Gateway to interact with the remote host via the chosen SOCKS proxy protocol. The Network Requester uses SURBs to anonymously reply to the original sender with whatever response it gets from the remote host.
Applications that support SOCKS4, 4a, or 5 can use the Socks Client exposed by the Mixnet module. Traffic is routed through the Exit Gateway's Network Requester, which uses SURBs to reply to the sender anonymously.
- [docs](./rust/mixnet/examples/socks)
- [example](./rust/mixnet/examples/socks)
- [docs](./rust/mixnet)
<Callout type="info" emoji="️">
There is a new abstraction coming soon that will allow the SDK to send IP packets, the beginning of a longer project to make a native Rust version of [`mixFetch`](./typescript/start#mixfetch). Stay tuned.
<Callout type="info">
Development is in progress to allow for this proxy method from native Rust, C, and Go without requiring a separate SOCKS client. Stay tuned.
</Callout>
{ /* ### `IPMixStream`
This is a version of the `MixSocket` that consumes IP packets before wrapping them in Sphinx and forwarding through the Mixnet to the IP Packet Router of the chosen Exit Gateway, where they are unwrapped and treated as normal IP packets. The IPR uses SURBs to anonymously reply to the original sender with whatever response it gets from the remote host.
<Callout type="info" emoji="️">
Currently only consumes IP packets - those who do not want to work with IP packets directly should check `mixtcp` below for doing something with HTTP(S).
</Callout>
- docs TODO LINK
- examples TODO LINK
### `MixTCP`
A proof of concept TCP/IP crate containing a [`smoltcp`](https://docs.rs/smoltcp/latest/smoltcp/index.html) `device` that uses the Mixnet for transport. Examples of using this to make a TLS handshake and perform an HTTPS request can be found linked below.
<Callout type="info" emoji="️">
This crate is currently a proof of concept which is in active development. This crate will become the basis of a general-purpose HTTP-through-Mixnet crate in the near future.
</Callout>
- docs TODO LINK
- example TODO LINK */ }
@@ -173,13 +173,13 @@ Print current tunnel configuration:
nym-vpnc tunnel get
```
Enable two-hop mode (WireGuard) traffic jumps directly from entry gateway to exit gateway:
Enable two-hop mode (WireGuard): traffic jumps directly from entry gateway to exit gateway:
```sh
nym-vpnc tunnel set --two-hop on
```
Enable Mixnet (5-hop) disable two-hop to route traffic through the full mixnet for maximum privacy:
Enable Mixnet (5-hop): disable two-hop to route traffic through the full mixnet for maximum privacy:
```sh
nym-vpnc tunnel set --two-hop off
@@ -293,7 +293,7 @@ nym-vpnc ad-block set disabled
```
<Callout type="info">
You can test ad-blocking with [adblock.turtlecute.org](https://adblock.turtlecute.org/). Some browsers cache DNS internally, so toggling ad-block on/off at runtime may not have an immediate effect a browser restart may be needed. Use `nslookup` or `dig` to verify that domains are being blocked.
You can test ad-blocking with [adblock.turtlecute.org](https://adblock.turtlecute.org/). Some browsers cache DNS internally, so toggling ad-block on/off at runtime may not have an immediate effect; a browser restart may be needed. Use `nslookup` or `dig` to verify that domains are being blocked.
</Callout>
## DNS
+27 -4
View File
@@ -1,11 +1,34 @@
---
title: "Nym Rust SDK: Privacy Apps for the Mixnet"
description: "Rust SDK reference for building privacy applications on the Nym mixnet. Covers TcpProxy, Mixnet module, Client Pool, FFI bindings, and code examples."
description: "Rust SDK reference for building privacy applications on the Nym mixnet. Covers the Mixnet client, Stream multiplexing, Client Pool, and code examples."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-02-01"
lastUpdated: "2026-03-13"
---
# Introduction
# Rust SDK
The Rust SDK allows exposes a few different modules, some more plug and play than others. Each of which handles exposes a Nym Client, which handles finding and using a route for packets through the Mixnet, encryption, and cover traffic, all under the hood.
import { Callout } from 'nextra/components'
import { CratesPaused } from '../../components/crates-paused'
All modules share a common `MixnetClient` that manages gateway connections, Sphinx packet encryption, routing, and cover traffic.
<Callout type="info">
Full API reference: [**docs.rs/nym-sdk**](https://docs.rs/nym-sdk/latest/nym_sdk/)
</Callout>
<CratesPaused />
For an overview of what the SDK can do, see the **[Tour](./rust/tour)**. For setup instructions, see [Installation](./rust/importing).
## Modules
- **[Stream](./rust/stream)**: multiplexed `AsyncRead + AsyncWrite` byte streams over the Mixnet. **If you're used to TCP sockets, start here.**
- **[Mixnet](./rust/mixnet)**: raw message payloads, independently routed, no connections or ordering. Use this when you want full control over the communication model.
- **[Client Pool](./rust/client-pool)**: keeps ready-to-use `MixnetClient` instances warm for bursty workloads.
- **[TcpProxy](./rust/tcpproxy)** *(deprecated)*: TCP socket proxying with session management and message ordering. Use Stream for new projects.
- **[FFI](./rust/ffi)**: Go and C/C++ bindings.
@@ -1,7 +1,9 @@
{
"importing": "Importing",
"tcpproxy": "TcpProxy Module",
"tour": "Tour",
"importing": "Installation",
"mixnet": "Mixnet Module",
"stream": "Stream Module",
"tcpproxy": "TcpProxy Module (Deprecated)",
"client-pool": "Client Pool Module",
"ffi": "FFI"
}
@@ -1,9 +1,73 @@
---
title: "Client Pool: Pre-Connected Mixnet Clients"
description: "The Nym ClientPool maintains ready-to-use MixnetClient instances, eliminating connection latency for bursty traffic patterns."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-03-15"
---
# Client Pool
import { Callout } from 'nextra/components';
import { Callout } from 'nextra/components'
We have a configurable-size Client Pool for processes that require multiple clients in quick succession (this is used by default by the [`TcpProxyClient`](./tcpproxy) for instance)
The `ClientPool` maintains a configurable number of connected ephemeral `MixnetClient` instances, ready for immediate use. This eliminates the connection latency that comes with creating a new client on each request: the gateway handshake, key generation, and topology fetch all happen ahead of time.
This will be useful for developers looking to build connection logic, or just are using raw SDK clients in a sitatuation where there are multiple connections with a lot of churn.
## How it works
> You can find this code [here](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/client_pool.rs)
```mermaid
---
config:
theme: neo-dark
---
flowchart LR
BG["Background loop"] -->|creates clients| P["Pool (Vec)"]
P -->|"get_mixnet_client()"| APP["Your application"]
APP -->|uses and disconnects| D["Done"]
BG -->|"pool < reserve? create another"| P
```
1. **Create** the pool with a target reserve size: `ClientPool::new(5)`
2. **Start** the background loop: `pool.start()`. It immediately begins connecting clients
3. **Pop** a client when needed: `pool.get_mixnet_client()` returns `Some(client)` or `None` if the pool is empty
4. **Use** the client normally: send messages, open streams, etc.
5. **Disconnect** the client when done. The background loop notices the pool is below reserve and creates a replacement
Clients are **consumed, not returned**. The pool creates new ones to maintain the reserve. If the pool is empty, you can fall back to `MixnetClient::connect_new()` (slower, but keeps things working).
<Callout type="info">
The `NymProxyClient` (TcpProxy) uses a `ClientPool` internally: one client per incoming TCP connection.
</Callout>
## Quick example
```rust
use nym_sdk::client_pool::ClientPool;
use nym_network_defaults::setup_env;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
nym_bin_common::logging::setup_tracing_logger();
// Load mainnet network defaults into env vars (required by ClientPool)
setup_env(None::<String>);
let pool = ClientPool::new(5); // maintain 5 clients in reserve
let pool_clone = pool.clone();
tokio::spawn(async move { pool_clone.start().await });
// Get a client when needed
if let Some(client) = pool.get_mixnet_client().await {
println!("Got client: {}", client.nym_address());
client.disconnect().await;
}
pool.disconnect_pool().await;
Ok(())
}
```
## Further reading
- [Tutorial: Handle bursty traffic](./client-pool/tutorial): step-by-step guide covering pool creation, burst handling, and fallback logic
- [API reference on docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/client_pool/): type details, method signatures, and architecture docs
- [Example source on GitHub](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/client_pool.rs): complete working example
@@ -1,4 +1,4 @@
{
"architecture": "Architecture",
"example": "Example"
"tutorial": "Tutorial",
"examples": "Examples"
}
@@ -1,31 +0,0 @@
# Client Pool Architecture
import { Callout } from 'nextra/components';
<Callout type="warning">
There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with.
This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet.
It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/).
</Callout>
## Motivations
In situations where multiple connections are expected, and the number of connections can vary greatly, the Client Pool reduces time spent waiting for the creation of a Mixnet Client blocking your code sending traffic through the Mixnet. Instead, a configurable number of Clients can be generated and run in the background which can be very quickly grabbed, used, and disconnected.
The Pool can be simply run as a background process for the runtime of your program.
## Clients & Lifetimes
The Client Pool creates **ephemeral Mixnet Clients** which are used and then disconnected. Using the [`TcpProxy`](../tcpproxy) as an example, Clients are used for the lifetime of a single incoming TCP connection; after the TCP connection is closed, the Mixnet client is disconnected.
Clients are popped from the pool when in use, and another Client is created to take its place. If connections are coming in faster than Clients are replenished, you can instead generate an ephemeral Client on the fly, or wait; this is up to the developer to decide. You can see an example of this logic in the example on the next page.
## Runtime Loop
Aside from a few helper / getter functions and a graceful `disconnect_pool()`, the Client Pool is mostly made up of a very simple loop around some conditional logic making up `start()`:
- if the number of Clients in the pool is `< client_pool_reserve_number` (set on `new()`) then create more,
- if the number of Clients in the pool `== client_pool_reserve_number` (set on `new()`) then `sleep`,
- if `client_pool_reserve_number == 0` just `sleep`.
`disconnect_pool()` will cause this loop to `break` via cancellation token.
@@ -1,110 +0,0 @@
# Client Pool Example
import { Callout } from 'nextra/components';
<Callout type="warning">
There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with.
This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet.
It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/).
</Callout>
> You can find this code [here](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/client_pool.rs)
```rust
use anyhow::Result;
use nym_network_defaults::setup_env;
use nym_sdk::client_pool::ClientPool;
use nym_sdk::mixnet::{MixnetClientBuilder, NymNetworkDetails};
use tokio::signal::ctrl_c;
// This client pool is used internally by the TcpProxyClient but can also be used by the Mixnet module, in case you're quickly swapping clients in and out but won't want to use the TcpProxy module.
//
// Run with: cargo run --example client_pool -- ../../../envs/<NETWORK>.env
#[tokio::main]
async fn main() -> Result<()> {
nym_bin_common::logging::setup_logging();
setup_env(std::env::args().nth(1));
let conn_pool = ClientPool::new(2); // Start the Client Pool with 2 Clients always being kept in reserve
let client_maker = conn_pool.clone();
tokio::spawn(async move {
client_maker.start().await?;
Ok::<(), anyhow::Error>(())
});
println!("\n\nWaiting a few seconds to fill pool\n\n");
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
let pool_clone_one = conn_pool.clone();
let pool_clone_two = conn_pool.clone();
tokio::spawn(async move {
let client_one = match pool_clone_one.get_mixnet_client().await {
Some(client) => {
println!("Grabbed client {} from pool", client.nym_address());
client
}
None => {
println!("Not enough clients in pool, creating ephemeral client");
let net = NymNetworkDetails::new_from_env();
let client = MixnetClientBuilder::new_ephemeral()
.network_details(net)
.build()?
.connect_to_mixnet()
.await?;
println!(
"Using {} for the moment, created outside of the connection pool",
client.nym_address()
);
client
}
};
let our_address = client_one.nym_address();
println!("\n\nClient 1: {our_address}\n\n");
client_one.disconnect().await;
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; // Emulate doing something
return Ok::<(), anyhow::Error>(());
});
tokio::spawn(async move {
let client_two = match pool_clone_two.get_mixnet_client().await {
Some(client) => {
println!("Grabbed client {} from pool", client.nym_address());
client
}
None => {
println!("Not enough clients in pool, creating ephemeral client");
let net = NymNetworkDetails::new_from_env();
let client = MixnetClientBuilder::new_ephemeral()
.network_details(net)
.build()?
.connect_to_mixnet()
.await?;
println!(
"Using {} for the moment, created outside of the connection pool",
client.nym_address()
);
client
}
};
let our_address = *client_two.nym_address();
println!("\n\nClient 2: {our_address}\n\n");
client_two.disconnect().await;
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; // Emulate doing something
return Ok::<(), anyhow::Error>(());
});
wait_for_ctrl_c(conn_pool).await?;
Ok(())
}
async fn wait_for_ctrl_c(pool: ClientPool) -> Result<()> {
println!("\n\nPress CTRL_C to disconnect pool\n\n");
ctrl_c().await?;
println!("CTRL_C received. Killing client pool");
pool.disconnect_pool().await;
Ok(())
}
```
@@ -0,0 +1,19 @@
---
title: "Client Pool Examples"
description: "Runnable Rust example for the Nym Client Pool: managing multiple MixnetClients with ephemeral fallback."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-03-26"
---
# Examples
Runnable examples in [`sdk/rust/nym-sdk/examples/`](https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/examples). Each file is self-contained with step-by-step comments.
```bash
cargo run --example <name>
```
| Example | Source | What it demonstrates |
|---|---|---|
| Client Pool | [`client_pool.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/client_pool.rs) | Creating a pool of `MixnetClient`s, retrieving clients from the pool, and falling back to ephemeral clients when the pool is empty |
@@ -0,0 +1,277 @@
---
title: "Client Pool Tutorial: Handle Bursty Traffic"
description: "Step-by-step Rust tutorial to use Nym ClientPool for handling bursts of concurrent mixnet operations without blocking on client creation."
schemaType: "HowTo"
section: "Developers"
lastUpdated: "2026-03-26"
---
# Tutorial: Handle Bursty Traffic with Client Pool
import { Callout } from 'nextra/components'
import { CodeVerified } from '../../../../components/code-verified'
In this tutorial you'll build a program that uses `ClientPool` to handle bursts of concurrent Mixnet operations without blocking on client creation. You'll see how the pool pre-creates clients in the background, how to pop them under load, and what happens when demand exceeds supply.
## What you'll learn
- Creating and starting a `ClientPool`
- Popping clients from the pool for concurrent operations
- Falling back to on-demand client creation when the pool is empty
- Observing pool replenishment
- Graceful shutdown
<CodeVerified />
## Prerequisites
- Rust toolchain (1.70+)
- A working internet connection
## Step 1: Set up the project
```sh
cargo init nym-pool-demo
cd nym-pool-demo
```
Add dependencies to `Cargo.toml`:
```toml
[dependencies]
nym-sdk = { git = "https://github.com/nymtech/nym", rev = "4077717" }
nym-network-defaults = { git = "https://github.com/nymtech/nym", rev = "4077717" }
nym-bin-common = { git = "https://github.com/nymtech/nym", rev = "4077717", features = ["basic_tracing"] }
tokio = { version = "1", features = ["full"] }
blake3 = "=1.7.0" # required pin — see https://nymtech.net/developers/rust/importing
```
## Step 2: Create and start the pool
The pool is created with a **reserve size**: the number of connected clients it tries to maintain at all times. The `start()` method runs a background loop that creates clients whenever the pool drops below the reserve.
Create `src/main.rs`:
```rust
use nym_sdk::client_pool::ClientPool;
use nym_sdk::mixnet::MixnetMessageSender;
use nym_network_defaults::setup_env;
use std::time::Duration;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_tracing_logger();
// Load mainnet network defaults into env vars (required by ClientPool)
setup_env(None::<String>);
// Create a pool that maintains 3 clients in reserve
let pool = ClientPool::new(3);
// Start the pool in a background task.
// It immediately begins connecting clients.
let pool_bg = pool.clone();
tokio::spawn(async move {
pool_bg.start().await.unwrap();
});
println!("Pool started — waiting for clients to connect...");
tokio::time::sleep(Duration::from_secs(15)).await;
// Check how many are ready
let count = pool.get_client_count().await;
println!("Pool has {count} clients ready");
```
<Callout type="info">
Creating a `MixnetClient` takes several seconds (gateway handshake, key generation, topology fetch). The pool does this work ahead of time so your application doesn't block when it needs a client.
</Callout>
## Step 3: Pop clients and use them
When you call `get_mixnet_client()`, the pool removes a client and returns it. The background loop notices the shortfall and starts creating a replacement.
```rust
// Simulate a burst of 3 concurrent tasks, each needing a client
let mut handles = vec![];
for i in 1..=3 {
let pool = pool.clone();
let handle = tokio::spawn(async move {
// Pop a client from the pool
let mut client = match pool.get_mixnet_client().await {
Some(c) => {
println!("Task {i}: got client {} from pool", c.nym_address());
c
}
None => {
// Pool is empty — fall back to creating one on the fly.
// This is slower but keeps things working.
println!("Task {i}: pool empty, creating client on the fly...");
nym_sdk::mixnet::MixnetClient::connect_new().await.unwrap()
}
};
// Do something with the client — here, send a message to ourselves
let addr = *client.nym_address();
client
.send_plain_message(addr, format!("hello from task {i}"))
.await
.unwrap();
// Wait for the message to arrive
if let Some(msgs) = client.wait_for_messages().await {
for msg in msgs {
if !msg.message.is_empty() {
println!(
"Task {i}: received {:?}",
String::from_utf8_lossy(&msg.message)
);
}
}
}
// Disconnect when done — the pool will create a replacement
client.disconnect().await;
println!("Task {i}: done");
});
handles.push(handle);
}
// Wait for all tasks to finish
for h in handles {
h.await.unwrap();
}
```
## Step 4: Observe replenishment
After popping all 3 clients, the pool background loop starts creating replacements. Give it time and check:
```rust
// Pool should be replenishing
println!("\nWaiting for pool to replenish...");
tokio::time::sleep(Duration::from_secs(15)).await;
let count = pool.get_client_count().await;
println!("Pool has {count} clients ready again");
```
## Step 5: Shut down gracefully
```rust
// Disconnect all remaining clients and stop the background loop
pool.disconnect_pool().await;
println!("Pool shut down");
}
```
## Step 6: Run it
```sh
RUST_LOG=info cargo run
```
You'll see output like:
```
Pool started — waiting for clients to connect...
Pool has 3 clients ready
Task 1: got client 8gk4Y...@2xU4d... from pool
Task 2: got client F3qR7...@9nK2m... from pool
Task 3: got client A7bN2...@4pL8w... from pool
Task 1: received "hello from task 1"
Task 2: received "hello from task 2"
Task 3: received "hello from task 3"
Task 1: done
Task 2: done
Task 3: done
Waiting for pool to replenish...
Pool has 3 clients ready again
Pool shut down
```
## When to use the pool
The pool is most useful when:
- **You have bursty traffic:** many concurrent operations that each need their own client
- **Latency matters:** you can't afford the several-second delay of creating a client on each request
- **You're building a service:** an API endpoint that creates a client per request would benefit from pre-warmed clients
If your application only ever needs one client at a time, just use `MixnetClient::connect_new()` directly.
<Callout type="info">
The `NymProxyClient` (TcpProxy module) uses a `ClientPool` internally: one client per incoming TCP connection.
</Callout>
## What you've learned
- **`ClientPool::new(n)`** creates a pool targeting `n` reserve clients
- **`pool.start()`** runs a background loop that creates clients whenever the pool is below reserve
- **`pool.get_mixnet_client()`** pops a client; returns `None` if the pool is empty
- **Clients are consumed, not returned.** The pool automatically creates replacements
- **`pool.disconnect_pool()`** shuts down all remaining clients and stops the background loop
- **Fall back to on-demand creation** when the pool is empty for resilience
## Complete code
```rust
use nym_sdk::client_pool::ClientPool;
use nym_sdk::mixnet::MixnetMessageSender;
use nym_network_defaults::setup_env;
use std::time::Duration;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_tracing_logger();
setup_env(None::<String>);
let pool = ClientPool::new(3);
let pool_bg = pool.clone();
tokio::spawn(async move { pool_bg.start().await.unwrap() });
println!("Waiting for pool to fill...");
tokio::time::sleep(Duration::from_secs(15)).await;
println!("Pool has {} clients", pool.get_client_count().await);
let mut handles = vec![];
for i in 1..=3 {
let pool = pool.clone();
handles.push(tokio::spawn(async move {
let mut client = match pool.get_mixnet_client().await {
Some(c) => c,
None => nym_sdk::mixnet::MixnetClient::connect_new().await.unwrap(),
};
let addr = *client.nym_address();
client
.send_plain_message(addr, format!("hello from task {i}"))
.await
.unwrap();
if let Some(msgs) = client.wait_for_messages().await {
for msg in msgs.iter().filter(|m| !m.message.is_empty()) {
println!("Task {i}: {}", String::from_utf8_lossy(&msg.message));
}
}
client.disconnect().await;
}));
}
for h in handles {
h.await.unwrap();
}
println!("Waiting for replenishment...");
tokio::time::sleep(Duration::from_secs(15)).await;
println!("Pool has {} clients", pool.get_client_count().await);
pool.disconnect_pool().await;
println!("Done");
}
```
@@ -1,59 +1,105 @@
---
title: "FFI Bindings: Go and C/C++"
description: "Use the Nym SDK from Go and C/C++ via FFI bindings. Covers mixnet messaging, anonymous replies, and TcpProxy lifecycle from non-Rust languages."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-03-15"
---
# FFI Bindings
import { Callout } from 'nextra/components';
We currently have FFI bindings for Go and C/C++. See the table below to check the coverage of functionality we expect devs would like to see.
import { Callout } from 'nextra/components'
The [`nym/sdk/ffi`](https://github.com/nymtech/nym/tree/master/sdk/ffi) directory has the following structure:
The SDK exposes FFI bindings for Go and C/C++. The source lives in [`sdk/ffi`](https://github.com/nymtech/nym/tree/develop/sdk/ffi):
```
ffi
├── cpp
├── go
── README.md
└── shared
├── cpp # C/C++ bindings (manual C FFI)
├── go # Go bindings (via uniffi-bindgen-go)
── shared # Shared Rust implementation
```
The main functionality of exposed functions will be imported from `sdk/ffi/shared` into `sdk/ffi/<LANGUAGE>` in order to cut down on code duplication, and so that the imported bindings can be language-specific with regards to types and any `unsafe` code that is required, as well as allowing for the use of language-specific FFI libraries in the future (e.g. we are using `uniffi-bindgen-go` for Go, and at the moment have custom C/C++ bindings, which we might in the future replace with `cxx`).
Core logic lives in `shared/` and is imported into language-specific wrappers. The shared layer handles thread safety and ensures client operations run on blocking threads on the Rust side of the FFI boundary.
Furthermore, the `shared/` code makes sure that client access is thread-safe, and that client actions happen in blocking threads on the Rust side of the FFI boundary.
## What's exposed
## Mixnet Module
This is the basic mixnet component of the SDK, exposing client functionality with which people can build custom interfaces with the Mixnet. These functions are exposed to both Go and C/C++ via the `sdk/ffi/shared/` crate.
**Mixnet** (Go and C/C++): ephemeral and persistent client creation, sending messages, anonymous replies via SURBs, listening for incoming messages.
| `shared/lib.rs` function | Rust Function |
| ------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `init_ephemeral_internal()` | `MixnetClient::connect_new()` |
| `init_default_storage_internal(config_dir: PathBuf)` | `MixnetClientBuilder::new_with_default_storage(config_dir)` |
| `get_self_address_internal()` | `MixnetClient.nym_address()` |
| `send_message_internal(recipient: Recipient, message: &str)` | `MixnetClient.send_plain_message(recipient, message)` |
| `reply_internal(recipient: AnonymousSenderTag, message: &str)`| `MixnetClient.send_reply(recipient, message)` |
**TcpProxy** (Go only): client and server creation and lifecycle.
> We have also implemented `listen_for_incoming_internal()` which is a wrapper around the Mixnet client's `wait_for_messages()`. This is a helper method for listening out for and handling incoming messages.
### Currently Unsupported Functionality
At the time of writing the following functionality is not exposed to the shared FFI library:
- `split_sender()`: the ability to [split a client into sender and receiver](./mixnet/examples/split-send) for concurrent send/receive.
- The use of [custom network topologies](./mixnet/examples/custom-topology).
- `Socks5::new()`: creation and use of the [socks5/4a/4 proxy client](./mixnet/examples/socks).
## TcpProxy Module
A connection abstraction which exposes a local TCP socket which developers are able to interact with basically as expected, being able to read/write to/from a bytestream, without really having to take into account the workings of the Mixnet/Sphinx/the message-based format of the underlying client.
<Callout type="info" emoji="️">
At the time of writing this functionality is **only** exposed to Go. C/C++ bindings will follow in the future in a larger update to the C FFI.
<Callout type="warning">
The TcpProxy module is deprecated. For new projects, use the [Stream module](./stream) instead.
</Callout>
**Client Pool and Stream** have no standalone FFI bindings yet. The TcpProxy bindings use the Client Pool internally.
| `shared/lib.rs` function | Rust Function |
| --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `proxy_client_new_internal(server_address: Recipient, listen_address: &str, listen_port: &str, close_timeout: u64, env: Option<String>)`| `NymProxyClient::new(server_address, listen_address, listen_port, close_timeout, env)`|
| `proxy_client_new_defaults_internal(server_address, env)` | `NymProxyClient::new_with_defaults(server_address, env)` |
| `proxy_client_run_internal()` | `NymProxyClient.run()` |
| `proxy_server_new_internal(upstream_address: &str, config_dir: &str, env: Option<String>)` | `NymProxyServer::new(upstream_address, config_dir, env)` |
| `proxy_server_run_internal()` | `NymProxyServer.run_with_shutdown()` |
| `proxy_server_address_internal()` | `NymProxyServer.nym_address()` |
## Quick example (Go)
## Client Pool
There are currently no FFI bindings for the Client Pool. This will be coming in the future. The bindings for the TcpProxy have been updated to be able to use the Client Pool under the hood, but the standalone Pool is not yet exposed to FFI.
```go
// Initialize an ephemeral client
bindings.InitEphemeral()
// Get our Nym address
addr, _ := bindings.GetSelfAddress()
// Send a message through the Mixnet
bindings.SendMessage(addr, "hello from Go")
// Listen for incoming messages
msg, _ := bindings.ListenForIncoming()
fmt.Println("Received:", msg.Message)
// Reply anonymously via SURBs
bindings.Reply(msg.Sender, "reply from Go")
```
## Quick example (C++)
The C++ bindings use callbacks for return values and a `ReceivedMessage` struct for incoming data:
```cpp
extern "C" {
struct ReceivedMessage {
const uint8_t* message;
size_t size;
const char* sender_tag;
};
void init_logging();
char init_ephemeral();
char get_self_address(void (*callback)(const char*));
char send_message(const char*, const char*);
char listen_for_incoming(void (*callback)(ReceivedMessage));
char reply(const char*, const char*);
}
// Get address via callback
char addr[134];
void on_address(const char* s) { strcpy(addr, s); }
// Receive message via callback
char sender_tag[22];
void on_message(ReceivedMessage msg) {
std::cout << "Received: " << msg.message << std::endl;
strcpy(sender_tag, msg.sender_tag);
}
int main() {
init_ephemeral();
get_self_address(on_address);
send_message(addr, "hello from C++");
listen_for_incoming(on_message);
reply(sender_tag, "reply from C++");
}
```
## Building
Each language has a `build.sh` script that compiles the Rust shared library and generates bindings. See the README in each directory for prerequisites.
## Examples and source
- [Go mixnet example](https://github.com/nymtech/nym/blob/develop/sdk/ffi/go/example.go): init, send, receive, SURB reply
- [Go TcpProxy example](https://github.com/nymtech/nym/blob/develop/sdk/ffi/go/proxy_example.go): proxy client and server with TCP echo
- [C++ example](https://github.com/nymtech/nym/blob/develop/sdk/ffi/cpp/src/main.cpp): same flow using Boost threads
- [`sdk/ffi` source](https://github.com/nymtech/nym/tree/develop/sdk/ffi): full source and build scripts
@@ -1,13 +1,51 @@
# Installation
import { Callout } from 'nextra/components';
---
title: "Install the Nym Rust SDK"
description: "Add nym-sdk to your Rust project from Git or crates.io. Covers version requirements, minimum Rust version, and current feature gate status."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-03-27"
---
The `nym-sdk` crate is **not yet available via [crates.io](https://crates.io)**. As such, in order to import the crate you must specify the Nym monorepo in your `Cargo.toml` file. Since the `HEAD` of `master` is always the most recent release, we recommend developers use that for their imports, unless they have a reason to pull in a specific historic version of the code.
# Installation
import { Callout } from 'nextra/components';
import { CratesPaused } from '../../../components/crates-paused'
<CratesPaused />
```toml
# importing HEAD of master branch
nym-sdk = { git = "https://github.com/nymtech/nym", branch = "master" }
# importing HEAD of the third release of 2023, codename 'kinder'
nym-sdk = { git = "https://github.com/nymtech/nym", branch = "release/2023.3-kinder" }
[dependencies]
nym-sdk = { git = "https://github.com/nymtech/nym", rev = "4077717" }
blake3 = "=1.7.0" # pin to avoid a transitive dependency conflict — see note below
```
Work will occur in the future to break the monorepo down into importable features, in order to reduce the number of dependencies imported by developers.
<Callout type="warning">
**Temporary pin required.** You must pin `blake3 = "=1.7.0"` in your `Cargo.toml` to avoid a build failure caused by a transitive `digest` version conflict. This will be resolved in a future SDK release.
</Callout>
You can also track a branch if you want the latest changes:
```toml
# development branch (latest changes, may be unstable)
nym-sdk = { git = "https://github.com/nymtech/nym", branch = "develop" }
# latest stable release
nym-sdk = { git = "https://github.com/nymtech/nym", branch = "master" }
```
**Minimum Rust version:** 1.70+
### crates.io (older API only)
If you don't need the Stream module or other recent additions, you can still use the published crate:
```toml
[dependencies]
nym-sdk = "1.20.4"
```
This version includes the Mixnet message API, Client Pool, and TcpProxy modules.
<Callout type="warning">
**No feature gates yet.** Importing `nym-sdk` pulls in everything (mixnet, tcp_proxy, client_pool, etc.) and their full dependency trees. Cargo feature flags are planned.
</Callout>
@@ -3,12 +3,60 @@ title: "Nym Rust SDK: Mixnet Messaging Module"
description: "Use the Nym Rust SDK Mixnet module to send messages through the mixnet. Covers builder patterns, custom topologies, SOCKS proxy, and anonymous replies."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-02-01"
lastUpdated: "2026-03-13"
---
# Mixnet Module
import { Callout } from 'nextra/components';
This module exposes the logic of creating and interacting with clients and Mixnet messages. This is recommended for those wanting to either start playing around with the Mixnet and how it works, or build connection logic.
The `mixnet` module is the core of the Nym SDK. It provides [`MixnetClient`](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.MixnetClient.html) for connecting to the Nym Mixnet, sending messages through Sphinx packet encryption and 5-hop routing, and receiving reconstructed messages on the other side.
> For developers wanting something more 'plug and play' we recommend the [`TcpProxy` module](./tcpproxy).
<Callout type="warning">
Messages are individually routed through the Mixnet with no guaranteed ordering or persistent connections. If you want familiar socket-like I/O (`read`/`write`), use the [Stream module](./stream) instead. See the [Tour](./tour) for how the two approaches compare.
</Callout>
## Two operating modes
The client operates in one of two mutually exclusive modes:
**Message mode** (default): send and receive raw message payloads:
```rust
use nym_sdk::mixnet::{self, MixnetMessageSender};
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
// Send a message
client.send_plain_message(*client.nym_address(), "hello").await.unwrap();
// Receive messages
if let Some(msgs) = client.wait_for_messages().await {
for msg in msgs {
println!("Got: {}", String::from_utf8_lossy(&msg.message));
}
}
client.disconnect().await;
```
**Stream mode:** persistent `AsyncRead + AsyncWrite` channels. See the [Stream module](./stream) for details.
<Callout type="info">
Stream mode is activated by calling `open_stream()` or `listener()`. Once active, message-mode methods return `Error::StreamModeActive`. This is a one-way transition.
</Callout>
## API reference
- [API reference on docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/): full architecture documentation, all types, builder methods, traits, and configuration options
- [Examples on GitHub](https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/examples): runnable examples covering simple send/receive, builder patterns, custom topologies, SOCKS proxy, anonymous replies, and more
Run any example with:
```sh
cargo run --example <example_name>
```
## Next steps
- [Tutorial: Send your first private message](./mixnet/tutorial): step-by-step guide covering sending, receiving, SURBs, and persistent identity
- [Troubleshooting](./mixnet/troubleshooting): common issues with logging, empty messages, and client lifecycle
- [Stream module](./stream): if you need persistent bidirectional byte channels
@@ -1,6 +1,5 @@
{
"examples": "Basic Examples",
"message-helpers": "Message Helpers",
"message-types": "Message Types",
"tutorial": "Tutorial",
"examples": "Examples",
"troubleshooting": "Troubleshooting"
}
@@ -1,19 +1,29 @@
---
title: "Mixnet Module Examples"
description: "Runnable Rust examples for the Nym mixnet module: sending messages, SURB anonymous replies, MixnetClientBuilder, persistent storage, and parallel send/receive."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-03-15"
---
# Examples
import { Callout } from 'nextra/components';
Runnable examples in [`sdk/rust/nym-sdk/examples/`](https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/examples). Each file is self-contained with step-by-step comments.
<Callout type="warning">
There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with.
This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet.
It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/).
</Callout>
All the following examples can be found in the `nym-sdk` [examples directory](https://github.com/nymtech/nym/tree/master/sdk/rust/nym-sdk/examples) in the monorepo. Just navigate to `nym/sdk/rust/nym-sdk/examples/` and run the files from there with:
```sh
cargo run --example <NAME_OF_FILE>
```bash
cargo run --example <NAME>
```
If you wish to run these outside of the workspace - such as if you want to use one as the basis for your own project - then make sure to import the `sdk`, `tokio`, and `nym_bin_common` crates.
| Example | Source | What it demonstrates |
|---|---|---|
| Simple | [`simple.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/simple.rs) | Send a message to yourself and print it |
| SURB Reply | [`surb_reply.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/surb_reply.rs) | Anonymous replies using `AnonymousSenderTag` and `send_reply()` |
| Builder | [`builder.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/builder.rs) | Using `MixnetClientBuilder` with ephemeral keys |
| Builder with Storage | [`builder_with_storage.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/builder_with_storage.rs) | Persisting keys to disk with `StoragePaths` |
| Parallel Send/Receive | [`parallel_sending_and_receiving.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/parallel_sending_and_receiving.rs) | Using `split_sender()` for concurrent tasks |
| Sandbox Testnet | [`sandbox.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/sandbox.rs) | Connecting to the Sandbox testnet instead of mainnet |
| Bandwidth Credential | [`bandwidth.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/bandwidth.rs) | Acquiring a bandwidth credential for paid mixnet access |
| Custom Topology | [`custom_topology_provider.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/custom_topology_provider.rs) | Implementing the `TopologyProvider` trait to filter or customize node selection |
| Overwrite Topology | [`manually_overwrite_topology.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs) | Manually constructing a topology with hardcoded nodes |
| Control Requests | [`control_requests.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/control_requests.rs) | Sending service provider control requests (health, version, binary info) |
| Custom Storage | [`manually_handle_storage.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/manually_handle_storage.rs) | Implementing custom storage backends for keys, gateways, and credentials |
@@ -1,10 +0,0 @@
{
"simple": "Simple Send",
"builders": "Builder Patterns",
"testnet": "Configurable Network",
"custom-topology": "Custom Network Topologies",
"split-send": "Concurrent Send & Receive",
"socks": "Socks Proxy",
"storage": "Manually Handle Storage",
"surbs": "Anonymous Replies"
}
@@ -1,4 +0,0 @@
# Builder Patterns
import { Callout } from 'nextra/components';
Since there are two ways of creating an SDK client - ephemeral and with-storage - then there are two ways of applying the Builder Pattern to client creation.
@@ -1,4 +0,0 @@
{
"builder": "Ephemeral",
"builder-with-storage": "With Storage"
}
@@ -1,73 +0,0 @@
# Mixnet Client Builder with Storage
import { Callout } from 'nextra/components';
The previous example involves ephemeral keys - if we want to create and then maintain a client identity over time, our code becomes a little more complex as we need to create, store, and conditionally load these keys.
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/builder_with_storage.rs).
```rust
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
use std::path::PathBuf;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Specify some config options
let config_dir = PathBuf::from("/tmp/mixnet-client");
let storage_paths = mixnet::StoragePaths::new_from_dir(&config_dir).unwrap();
// Create the client with a storage backend, and enable it by giving it some paths. If keys
// exists at these paths, they will be loaded, otherwise they will be generated.
let client = mixnet::MixnetClientBuilder::new_with_default_storage(storage_paths)
.await
.unwrap()
.build()
.unwrap();
// Now we connect to the mixnet, using keys now stored in the paths provided.
let mut client = client.connect_to_mixnet().await.unwrap();
// Be able to get our client address
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send a message throught the mixnet to ourselves
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message");
if let Some(received) = client.wait_for_messages().await {
for r in received {
println!("Received: {}", String::from_utf8_lossy(&r.message));
}
}
client.disconnect().await;
}
```
As seen in the example above, the `mixnet::MixnetClientBuilder::new()` function handles checking for keys in a storage location, loading them if present, or creating them and storing them if not, making client key management very simple.
Assuming our client config is stored in `/tmp/mixnet-client`, the following files are generated:
```
$ tree /tmp/mixnet-client
mixnet-client
├── ack_key.pem
├── db.sqlite
├── db.sqlite-shm
├── db.sqlite-wal
├── gateway_details.json
├── gateway_shared.pem
├── persistent_reply_store.sqlite
├── private_encryption.pem
├── private_identity.pem
├── public_encryption.pem
└── public_identity.pem
1 directory, 11 files
```
@@ -1,44 +0,0 @@
# Mixnet Client Builder
import { Callout } from 'nextra/components';
You can spin up an ephemeral client like so. This client will not have a persistent identity and its keys will be dropped on restart. Since there is currently no way of reconnecting a client that has been disconnected after use, then treat disconnecting a client the same as dropping its keys entirely.
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/builder.rs).
```rust
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Create client builder, including ephemeral keys. The builder can be usable in the context
// where you don't want to connect just yet.
let client = mixnet::MixnetClientBuilder::new_ephemeral()
.build()
.unwrap();
// Now we connect to the mixnet, using ephemeral keys already created
let mut client = client.connect_to_mixnet().await.unwrap();
// Be able to get our client address
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send a message through the mixnet to ourselves
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message");
if let Some(received) = client.wait_for_messages().await {
for r in received {
println!("Received: {}", String::from_utf8_lossy(&r.message));
}
}
client.disconnect().await;
}
```
@@ -1,17 +0,0 @@
# Importing and using a custom network topology
import { Callout } from 'nextra/components'
<Callout type="warning" emoji="⚠️">
These examples are **not** the same as using a configurable network: these functions define a subset of nodes to use on a given network, whereas the [testnet](./testnet) example is an example of switching to use a different network entirely. The two can be combined, but if you are looking for how to connect your client to a testnet, see the `testnet` file.
</Callout>
If you want to send traffic through a sub-set of nodes (for instance, ones you control, or a small test setup) when developing, debugging, or performing research, you will need to import these nodes as a custom network topology, instead of grabbing it from the [`Mainnet Nym-API`](https://validator.nymtech.net/api/swagger/index.html).
There are two ways to do this:
## Custom Topology Provider
If you are also running a Validator and Nym API for your network, you can specify that endpoint. Clients will then use this endpoint to grab a network topology on startup. You can also use this to specify using a testnet.
## Import a specific topology manually
If you aren't running a Validator and Nym API, and just want to import a specific sub-set of mix nodes, you can also overwrite the grabbed topology manually.
@@ -1,4 +0,0 @@
{
"custom-provider": "Custom Topology Provider",
"manual-topology": "Manually Overwrite Topology"
}
@@ -1,96 +0,0 @@
# Custom Topology Provider
import { Callout } from 'nextra/components';
If you are also running a Validator and Nym API for your network, you can specify that endpoint as such and interact with it as clients usually do (under the hood).
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/custom_topology_provider.rs)
```rust
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
use nym_topology::provider_trait::{async_trait, TopologyProvider};
use nym_topology::{nym_topology_from_detailed, NymTopology};
use nym_validator_client::nym_api::NymApiClientExt;
use url::Url;
struct MyTopologyProvider {
validator_client: nym_http_api_client::Client,
}
impl MyTopologyProvider {
fn new(nym_api_url: Url) -> MyTopologyProvider {
let validator_client = nym_http_api_client::Client::builder::<_, nym_validator_client::models::RequestError>(nym_api_url)
.expect("Failed to create API client builder")
.build::<nym_validator_client::models::RequestError>()
.expect("Failed to build API client");
MyTopologyProvider {
validator_client,
}
}
async fn get_topology(&self) -> NymTopology {
let mixnodes = self
.validator_client
.get_cached_active_mixnodes()
.await
.unwrap();
// in our topology provider only use mixnodes that have mix_id divisible by 3
// and have more than 100k nym (i.e. 100'000'000'000 unym) in stake
// why? because this is just an example to showcase arbitrary uses and capabilities of this trait
let filtered_mixnodes = mixnodes
.into_iter()
.filter(|mix| {
mix.mix_id() % 3 == 0 && mix.total_stake() > "100000000000".parse().unwrap()
})
.collect::<Vec<_>>();
let gateways = self.validator_client.get_cached_gateways().await.unwrap();
nym_topology_from_detailed(filtered_mixnodes, gateways)
}
}
#[async_trait]
impl TopologyProvider for MyTopologyProvider {
// this will be manually refreshed on a timer specified inside mixnet client config
async fn get_new_topology(&mut self) -> Option<NymTopology> {
Some(self.get_topology().await)
}
}
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
let nym_api = "https://validator.nymtech.net/api/".parse().unwrap();
let my_topology_provider = MyTopologyProvider::new(nym_api);
// Passing no config makes the client fire up an ephemeral session and figure things out on its own
let mut client = mixnet::MixnetClientBuilder::new_ephemeral()
.custom_topology_provider(Box::new(my_topology_provider))
.build()
.unwrap()
.connect_to_mixnet()
.await
.unwrap();
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send a message through the mixnet to ourselves
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message (ctrl-c to exit)");
client
.on_messages(|msg| println!("Received: {}", String::from_utf8_lossy(&msg.message)))
.await;
}
```
@@ -1,103 +0,0 @@
# Manually Overwrite Topology
import { Callout } from 'nextra/components';
If you aren't running a Validator and Nym API, and just want to import a specific sub-set of mix nodes, you can simply overwrite the grabbed topology manually.
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs)
```rust
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
use nym_topology::mix::Layer;
use nym_topology::{mix, NymTopology};
use std::collections::BTreeMap;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Passing no config makes the client fire up an ephemeral session and figure shit out on its own
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
let starting_topology = client.read_current_topology().await.unwrap();
// but we don't like our default topology, we want to use only those very specific, hardcoded, nodes:
let mut mixnodes = BTreeMap::new();
mixnodes.insert(
1,
vec![mix::Node {
mix_id: 63,
owner: None,
host: "172.105.92.48".parse().unwrap(),
mix_host: "172.105.92.48:1789".parse().unwrap(),
identity_key: "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK"
.parse()
.unwrap(),
sphinx_key: "CBmYewWf43iarBq349KhbfYMc9ys2ebXWd4Vp4CLQ5Rq"
.parse()
.unwrap(),
layer: Layer::One,
version: "1.1.0".into(),
}],
);
mixnodes.insert(
2,
vec![mix::Node {
mix_id: 23,
owner: None,
host: "178.79.143.65".parse().unwrap(),
mix_host: "178.79.143.65:1789".parse().unwrap(),
identity_key: "4Yr4qmEHd9sgsuQ83191FR2hD88RfsbMmB4tzhhZWriz"
.parse()
.unwrap(),
sphinx_key: "8ndjk5oZ6HxUZNScLJJ7hk39XtUqGexdKgW7hSX6kpWG"
.parse()
.unwrap(),
layer: Layer::Two,
version: "1.1.0".into(),
}],
);
mixnodes.insert(
3,
vec![mix::Node {
mix_id: 66,
owner: None,
host: "139.162.247.97".parse().unwrap(),
mix_host: "139.162.247.97:1789".parse().unwrap(),
identity_key: "66UngapebhJRni3Nj52EW1qcNsWYiuonjkWJzHFsmyYY"
.parse()
.unwrap(),
sphinx_key: "7KyZh8Z8KxuVunqytAJ2eXFuZkCS7BLTZSzujHJZsGa2"
.parse()
.unwrap(),
layer: Layer::Three,
version: "1.1.0".into(),
}],
);
// but we like the available gateways, so keep using them!
// (we like them because the author of this example is too lazy to use the same hardcoded gateway
// during client initialisation to make sure we are able to send to ourselves : ) )
let custom_topology = NymTopology::new(mixnodes, starting_topology.gateways().to_vec());
client.manually_overwrite_topology(custom_topology).await;
// and everything we send now should only ever go via those nodes
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send a message through the mixnet to ourselves
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message (ctrl-c to exit)");
client
.on_messages(|msg| println!("Received: {}", String::from_utf8_lossy(&msg.message)))
.await;
}
```
@@ -1,37 +0,0 @@
# Simple Send
import { Callout } from 'nextra/components'
Lets look at a very simple example of how you can import and use the websocket client in a piece of Rust code.
Simply importing the `nym_sdk` crate into your project allows you to create a client and send traffic through the mixnet.
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/simple.rs)
```rust
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Passing no config makes the client fire up an ephemeral session and figure shit out on its own
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
// Be able to get our client address
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send a message through the mixnet to ourselves
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message (ctrl-c to exit)");
client
.on_messages(|msg| println!("Received: {}", String::from_utf8_lossy(&msg.message)))
.await;
}
```
@@ -1,50 +0,0 @@
# Socks Proxy
import { Callout } from 'nextra/components'
If you are looking at implementing Nym as a transport layer for a crypto wallet or desktop app, this is probably the best place to start if they can speak SOCKS5, 4a, or 4.
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/socks5.rs)
```rust
use nym_sdk::mixnet;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
println!("Connecting receiver");
let mut receiving_client = mixnet::MixnetClient::connect_new().await.unwrap();
let socks5_config = mixnet::Socks5::new(receiving_client.nym_address().to_string());
let sending_client = mixnet::MixnetClientBuilder::new_ephemeral()
.socks5_config(socks5_config)
.build()
.unwrap();
println!("Connecting sender");
let sending_client = sending_client.connect_to_mixnet_via_socks5().await.unwrap();
let proxy = reqwest::Proxy::all(sending_client.socks5_url()).unwrap();
let reqwest_client = reqwest::Client::builder().proxy(proxy).build().unwrap();
tokio::spawn(async move {
println!("Sending socks5-wrapped http request");
// Message should be sent through the mixnet, via socks5
// We don't expect to get anything, as there is no network requester on the other end
reqwest_client.get("https://nym.com").send().await.ok()
});
println!("Waiting for message");
if let Some(received) = receiving_client.wait_for_messages().await {
for r in received {
println!(
"Received socks5 message requesting for endpoint: {}",
String::from_utf8_lossy(&r.message[10..27])
);
}
}
receiving_client.disconnect().await;
sending_client.disconnect().await;
}
```
@@ -1,52 +0,0 @@
# Send and Receive in Different Tasks
import { Callout } from 'nextra/components'
If you need to split the different actions of your client across different tasks, you can do so like this. You can think of this analogously to spliting a Tcp Stream into read/write. This functionality is also useful for embedding a sending and receiving client into different tasks.
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/parallel_sending_and_receiving.rs)
```rust
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::StreamExt;
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Passing no config makes the client fire up an ephemeral session and figure stuff out on its own
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
// Be able to get our client address
let our_address = *client.nym_address();
println!("Our client nym address is: {our_address}");
let sender = client.split_sender();
// receiving task
let receiving_task_handle = tokio::spawn(async move {
if let Some(received) = client.next().await {
println!("Received: {}", String::from_utf8_lossy(&received.message));
}
client.disconnect().await;
});
// sending task
let sending_task_handle = tokio::spawn(async move {
sender
.send_plain_message(our_address, "hello from a different task!")
.await
.unwrap();
});
// wait for both tasks to be done
println!("waiting for shutdown");
sending_task_handle.await.unwrap();
receiving_task_handle.await.unwrap();
}
```
@@ -1,229 +0,0 @@
# Manually Handle Storage
import { Callout } from 'nextra/components'
If you're integrating mixnet functionality into an existing app and want to integrate saving client configs and keys into your existing storage logic, you can manually perform these actions.
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/manually_handle_storage.rs)
```rust
use nym_sdk::mixnet::{
self, ActiveGateway, BadGateway, ClientKeys, EmptyReplyStorage, EphemeralCredentialStorage,
GatewayRegistration, GatewaysDetailsStore, KeyStore, MixnetClientStorage, MixnetMessageSender,
};
use nym_topology::provider_trait::async_trait;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Just some plain data to pretend we have some external storage that the application
// implementer is using.
let mock_storage = MockClientStorage::empty();
let mut client = mixnet::MixnetClientBuilder::new_with_storage(mock_storage)
.build()
.unwrap()
.connect_to_mixnet()
.await
.unwrap();
// Be able to get our client address
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// Send important info up the pipe to a buddy
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
println!("Waiting for message");
if let Some(received) = client.wait_for_messages().await {
for r in received {
println!("Received: {}", String::from_utf8_lossy(&r.message));
}
}
client.disconnect().await;
}
#[allow(unused)]
struct MockClientStorage {
pub key_store: MockKeyStore,
pub gateway_details_store: MockGatewayDetailsStore,
pub reply_store: EmptyReplyStorage,
pub credential_store: EphemeralCredentialStorage,
}
impl MockClientStorage {
fn empty() -> Self {
Self {
key_store: MockKeyStore,
gateway_details_store: MockGatewayDetailsStore,
reply_store: EmptyReplyStorage::default(),
credential_store: EphemeralCredentialStorage::default(),
}
}
}
impl MixnetClientStorage for MockClientStorage {
type KeyStore = MockKeyStore;
type ReplyStore = EmptyReplyStorage;
type CredentialStore = EphemeralCredentialStorage;
type GatewaysDetailsStore = MockGatewayDetailsStore;
fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) {
(self.reply_store, self.credential_store)
}
fn key_store(&self) -> &Self::KeyStore {
&self.key_store
}
fn reply_store(&self) -> &Self::ReplyStore {
&self.reply_store
}
fn credential_store(&self) -> &Self::CredentialStore {
&self.credential_store
}
fn gateway_details_store(&self) -> &Self::GatewaysDetailsStore {
&self.gateway_details_store
}
}
struct MockKeyStore;
#[async_trait]
impl KeyStore for MockKeyStore {
type StorageError = MyError;
async fn load_keys(&self) -> Result<ClientKeys, Self::StorageError> {
println!("loading stored keys");
Err(MyError)
}
async fn store_keys(&self, _keys: &ClientKeys) -> Result<(), Self::StorageError> {
println!("storing keys");
Ok(())
}
}
struct MockGatewayDetailsStore;
#[async_trait]
impl GatewaysDetailsStore for MockGatewayDetailsStore {
type StorageError = MyError;
async fn active_gateway(&self) -> Result<ActiveGateway, Self::StorageError> {
println!("getting active gateway");
Err(MyError)
}
async fn set_active_gateway(&self, _gateway_id: &str) -> Result<(), Self::StorageError> {
println!("setting active gateway");
Ok(())
}
async fn all_gateways(&self) -> Result<Vec<GatewayRegistration>, Self::StorageError> {
println!("getting all registered gateways");
Err(MyError)
}
async fn has_gateway_details(&self, _gateway_id: &str) -> Result<bool, Self::StorageError> {
println!("checking for gateway details");
Err(MyError)
}
async fn load_gateway_details(
&self,
_gateway_id: &str,
) -> Result<GatewayRegistration, Self::StorageError> {
println!("loading gateway details");
Err(MyError)
}
async fn store_gateway_details(
&self,
_details: &GatewayRegistration,
) -> Result<(), Self::StorageError> {
println!("storing gateway details");
Ok(())
}
async fn remove_gateway_details(&self, _gateway_id: &str) -> Result<(), Self::StorageError> {
println!("removing gateway details");
Ok(())
}
}
//
// struct MockReplyStore;
//
// #[async_trait]
// impl ReplyStorageBackend for MockReplyStore {
// type StorageError = MyError;
//
// async fn flush_surb_storage(
// &mut self,
// _storage: &CombinedReplyStorage,
// ) -> Result<(), Self::StorageError> {
// todo!()
// }
//
// async fn init_fresh(&mut self, _fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError> {
// todo!()
// }
//
// async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError> {
// todo!()
// }
// }
//
// struct MockCredentialStore;
//
// #[async_trait]
// impl CredentialStorage for MockCredentialStore {
// type StorageError = MyError;
//
// async fn insert_coconut_credential(
// &self,
// _voucher_value: String,
// _voucher_info: String,
// _serial_number: String,
// _binding_number: String,
// _signature: String,
// _epoch_id: String,
// ) -> Result<(), Self::StorageError> {
// todo!()
// }
//
// async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, Self::StorageError> {
// todo!()
// }
//
// async fn consume_coconut_credential(&self, id: i64) -> Result<(), Self::StorageError> {
// todo!()
// }
// }
#[derive(thiserror::Error, Debug)]
#[error("foobar")]
struct MyError;
impl From<BadGateway> for MyError {
fn from(_: BadGateway) -> Self {
MyError
}
}
```
@@ -1,86 +0,0 @@
# Anonymous Replies with SURBs (Single Use Reply Blocks)
import { Callout } from 'nextra/components'
Both functions used to send messages through the mixnet (`send_message` and `send_plain_message`) send a pre-determined number of SURBs along with their messages by default.
You can read more about how SURBs function under the hood [here](../../../../network/traffic/anonymous-replies).
In order to reply to an incoming message using SURBs, you can construct a `recipient` from the `sender_tag` sent along with the message you wish to reply to.
> You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/surb_reply.rs)
```rust
use nym_sdk::mixnet::{
AnonymousSenderTag, MixnetClientBuilder, MixnetMessageSender, ReconstructedMessage,
StoragePaths,
};
use std::path::PathBuf;
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_logging();
// Specify some config options
let config_dir = PathBuf::from("/tmp/surb-example");
let storage_paths = StoragePaths::new_from_dir(&config_dir).unwrap();
// Create the client with a storage backend, and enable it by giving it some paths. If keys
// exists at these paths, they will be loaded, otherwise they will be generated.
let client = MixnetClientBuilder::new_with_default_storage(storage_paths)
.await
.unwrap()
.build()
.unwrap();
// Now we connect to the mixnet, using keys now stored in the paths provided.
let mut client = client.connect_to_mixnet().await.unwrap();
// Be able to get our client address
let our_address = client.nym_address();
println!("\nOur client nym address is: {our_address}");
// Send a message through the mixnet to ourselves using our nym address
client
.send_plain_message(*our_address, "hello there")
.await
.unwrap();
// we're going to parse the sender_tag (AnonymousSenderTag) from the incoming message and use it to 'reply' to ourselves instead of our Nym address.
// we know there will be a sender_tag since the sdk sends SURBs along with messages by default.
println!("Waiting for message\n");
// get the actual message - discard the empty vec sent along with a potential SURB topup request
let mut message: Vec<ReconstructedMessage> = Vec::new();
while let Some(new_message) = client.wait_for_messages().await {
if new_message.is_empty() {
continue;
}
message = new_message;
break;
}
let mut parsed = String::new();
if let Some(r) = message.first() {
parsed = String::from_utf8(r.message.clone()).unwrap();
}
// parse sender_tag: we will use this to reply to sender without needing their Nym address
let return_recipient: AnonymousSenderTag = message[0].sender_tag.unwrap();
println!(
"\nReceived the following message: {} \nfrom sender with surb bucket {}",
parsed, return_recipient
);
// reply to self with it: note we use `send_str_reply` instead of `send_str`
println!("Replying with using SURBs");
client
.send_reply(return_recipient, "hi an0n!")
.await
.unwrap();
println!("Waiting for message (once you see it, ctrl-c to exit)\n");
client
.on_messages(|msg| println!("\nReceived: {}", String::from_utf8_lossy(&msg.message)))
.await;
}
```
@@ -1,46 +0,0 @@
# Configurable Network
import { Callout } from 'nextra/components'
If you want to connect your Mixnet client to a different network than Mainnet, simply pull in a file from [`nym/envs`](https://github.com/nymtech/nym/tree/master/envs) as such:
```rust
use futures::StreamExt;
use nym_network_defaults::setup_env;
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
// An example of creating a client relying on a testnet, in this case Sandbox.
#[tokio::main]
async fn main() -> anyhow::Result<()> {
nym_bin_common::logging::setup_logging();
// relative root is `sdk/rust/nym-sdk/` for fallback file path
let env_path =
std::env::var("NYM_ENV_PATH").unwrap_or_else(|_| "../../../envs/sandbox.env".to_string());
setup_env(Some(&env_path));
let sandbox_network = mixnet::NymNetworkDetails::new_from_env();
let mixnet_client = mixnet::MixnetClientBuilder::new_ephemeral()
.network_details(sandbox_network)
.build()?;
let mut client = mixnet_client.connect_to_mixnet().await?;
let our_address = client.nym_address();
// Send a message throughout the mixnet to ourselves
client
.send_plain_message(*our_address, "hello there")
.await?;
println!("Waiting for message");
if let Some(received) = client.next().await {
println!("Received: {}", String::from_utf8_lossy(&received.message));
} else {
eprintln!("Failed to receive message.");
}
client.disconnect().await;
Ok(())
}
```
@@ -1,75 +0,0 @@
# Message Helpers
import { Callout } from 'nextra/components';
<Callout type="warning">
There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with.
This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet.
It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/).
</Callout>
## Handling incoming messages
When listening out for a response to a sent message (e.g. if you have sent a request to a service, and are awaiting the response) you will want to await [non-empty messages (if you don't know why, read the info on this here)](./troubleshooting#client-receives-empty-messages-when-listening-for-response). This can be done with something like the helper functions here:
```rust
use nym_sdk::mixnet::ReconstructedMessage;
pub async fn wait_for_non_empty_message(
client: &mut MixnetClient,
) -> anyhow::Result<ReconstructedMessage> {
while let Some(mut new_message) = client.wait_for_messages().await {
if !new_message.is_empty() {
return Ok(new_message.pop().unwrap());
}
}
bail!("did not receive any non-empty message")
}
pub fn handle_response(message: ReconstructedMessage) -> anyhow::Result<ResponseTypes> {
ResponseTypes::try_deserialize(message.message)
}
// Note here that the only difference between handling a request and a response
// is that a request will have a sender_tag to parse.
//
// This is used for anonymous replies with SURBs.
pub fn handle_request(
message: ReconstructedMessage,
) -> anyhow::Result<(RequestTypes, Option<AnonymousSenderTag>)> {
let request = RequestTypes::try_deserialize(message.message)?;
Ok((request, message.sender_tag))
}
```
The above helper functions are used as such by the client in tutorial example: it sends a message to the service (what the message is isn't important - just that your client has sent a message _somewhere_ and you are awaiting a response), waits for a _non_empty_ message, then handles it (then logs it - but you can do whatever you want, parse it, etc):
```rust
// Send serialised request to service via mixnet what is await-ed here is
// placing the message in the client's message queue, NOT the sending itself.
let _ = client
.send_message(sp_address, message.serialize(), Default::default())
.await;
// Await a non-empty message
let received = wait_for_non_empty_message(client).await?;
// Handle the response received (the non-empty message awaited above)
let sp_response = handle_response(received)?;
// Match JSON -> ResponseType
let res = match sp_response {
crate::ResponseTypes::Balance(response) => {
println!("{:#?}", response);
response.balance
}
};
```
## Iterating over incoming messages
It is recommended to use `nym_client.next().await` over `nym_client.wait_for_messages().await` as the latter will return one message at a time which will probably be easier to deal with. See the [parallel send and receive example](./examples/split-send) for an example.
## Remember to disconnect your client
You should always **manually disconnect your client** with `client.disconnect().await` as seen in the code examples. This is important as your client is writing to a local DB and dealing with SURB storage, so needs to gracefully shutdown.
@@ -1,30 +0,0 @@
# Message Types
import { Callout } from 'nextra/components';
<Callout type="warning">
There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with.
This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet.
It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/).
</Callout>
There are several functions used to send outgoing messages through the Mixnet, each with a different level of customisation:
- `send(&self, message: InputMessage) -> Result<()>`
Sends a `InputMessage` to the mixnet. This is the most low-level sending function, for full customization. Called by `send_message()`.
- `send_message<M>(&self, address: Recipient, message: M, surbs: IncludedSurbs) -> Result<()>`
Sends bytes to the supplied Nym address. There is the option to specify the number of reply-SURBs to include. Called by `send_plain_message()`.
- `send_plain_message<M>(&self, address: Recipient, message: M) -> Result<()>`
Sends data to the supplied Nym address with the default surb behaviour.
> Note we specify *outgoing* messages above: this is because the SDK assumes that replies will be anonymous via [SURBs](../../../network/traffic/anonymous-replies).
Replies rely on the creation of an `AnonymousSenderTag` by parsing and storing the `sender_tag` from incoming messages, and using this to reply, instead of the `Receipient` type used by the functions outlined above:
`send_reply<M>(&self, recipient_tag: AnonymousSenderTag, message: M) -> Result<()>` will send the reply message to the supplied anonymous recipient.
> You can find all of the function definitions [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/src/mixnet/traits.rs).
@@ -1,130 +1,78 @@
---
title: "Mixnet Module Troubleshooting"
description: "Solutions for common Nym Rust SDK issues: client disconnect errors, empty SURB messages, verbose logging, and database lock problems."
schemaType: "FAQPage"
section: "Developers"
lastUpdated: "2026-03-15"
---
# Troubleshooting
import { Callout } from 'nextra/components';
<Callout type="warning">
There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with.
Common issues and how to resolve them.
This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet.
## Always disconnect your client
You should always **manually disconnect your client** with `client.disconnect().await`. The client writes to a local DB and manages SURB storage, so it needs to shut down gracefully. Failing to do this can lead to the errors described below.
It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/).
## Waiting for non-empty messages
When listening for a response, you may receive empty messages. These are SURB replenishment requests: the remote side asking for more reply SURBs. Filter them out:
```rust
let mut message = None;
while let Some(new_message) = client.wait_for_messages().await {
if !new_message.is_empty() {
message = new_message.into_iter().next();
break;
}
}
```
<Callout type="info">
Prefer `client.next().await` (from the `futures::StreamExt` trait, not the Nym Stream module) over `client.wait_for_messages().await`; it returns one message at a time which is easier to work with. You'll need `use futures::StreamExt;` in scope.
</Callout>
Below are several common issues or questions you may have.
If you come across something that isn't explained here, [PRs are welcome](https://github.com/nymtech/nym/issues/new/choose).
## Verbose `task client is being dropped` logging
### On client shutdown (expected)
If this is happening at the end of your code when disconnecting your client, this is fine; we just have a verbose client! When calling `client.disconnect().await` this is simply informing you that the client is shutting down.
On client shutdown / disconnect this is to be expected - this can be seen in many of the code examples as well. We use the [`nym_bin_common::logging`](https://github.com/nymtech/nym/blob/master/common/bin-common/src/logging/mod.rs) import to set logging in our example code. This defaults to `INFO` level.
When calling `client.disconnect().await`, the client logs that its background tasks are shutting down. This is normal and expected.
If you wish to quickly lower the verbosity of your client process logs when developing you can prepend your command with `RUST_LOG=<LOGGING_LEVEL>`.
If you want to run the `builder.rs` example with only `WARN` level logging and below:
Control log verbosity with `RUST_LOG`:
```sh
cargo run --example builder
```
Becomes:
```sh
RUST_LOG=warn cargo run --example builder
```
You can also make the logging _more_ verbose with:
```sh
RUST_LOG=debug cargo run --example builder
RUST_LOG=warn cargo run --example simple
```
### Not on client shutdown (unexpected)
If this is happening unexpectedly then you might be shutting your client process down too early. See the [accidentally killing your client process](#accidentally-killing-your-client-process-too-early) below for possible explanations and how to fix this issue.
If you see these messages unexpectedly, you may be killing the client process too early. See the next section.
## Accidentally killing your client process too early
If you are seeing either of the following errors when trying to run a client, specifically sending a message, then you may be accidentally killing your client process.
```sh
2023-11-02T10:31:03.930Z INFO TaskClient-BaseNymClient-real_traffic_controller-ack_control-action_controller > the task client is getting dropped
2023-11-02T10:31:04.625Z INFO TaskClient-BaseNymClient-received_messages_buffer-request_receiver > the task client is getting dropped
2023-11-02T10:31:04.626Z DEBUG nym_client_core::client::real_messages_control::acknowledgement_control::input_message_listener > InputMessageListener: Exiting
2023-11-02T10:31:04.626Z INFO TaskClient-BaseNymClient-real_traffic_controller-ack_control-input_message_listener > the task client is getting dropped
2023-11-02T10:31:04.626Z INFO TaskClient-BaseNymClient-real_traffic_controller-reply_control > the task client is getting dropped
2023-11-02T10:31:04.626Z DEBUG nym_client_core::client::real_messages_control > The reply controller has finished execution!
2023-11-02T10:31:04.626Z DEBUG nym_client_core::client::real_messages_control::acknowledgement_control > The input listener has finished execution!
2023-11-02T10:31:04.626Z INFO nym_task::manager > All registered tasks succesfully shutdown
```
If you see errors like `Polling shutdown failed: channel closed` or panics about `action control task has died`, your client is being dropped before it finishes sending.
```sh
2023-11-02T11:22:08.408Z ERROR TaskClient-BaseNymClient-topology_refresher > Assuming this means we should shutdown...
2023-11-02T11:22:08.408Z ERROR TaskClient-BaseNymClient-mix_traffic_controller > Polling shutdown failed: channel closed
2023-11-02T11:22:08.408Z INFO TaskClient-BaseNymClient-gateway_transceiver-child > the task client is getting dropped
2023-11-02T11:22:08.408Z ERROR TaskClient-BaseNymClient-mix_traffic_controller > Assuming this means we should shutdown...
thread 'tokio-runtime-worker' panicked at 'action control task has died: TrySendError { kind: Disconnected }', /home/.local/share/cargo/git/checkouts/nym-fbd2f6ea2e760da9/a800cba/common/client-core/src/client/real_messages_control/message_handler.rs:634:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
2023-11-02T11:22:08.477Z INFO TaskClient-BaseNymClient-real_traffic_controller-ack_control-input_message_listener > the task client is getting dropped
2023-11-02T11:22:08.477Z ERROR TaskClient-BaseNymClient-real_traffic_controller-ack_control-input_message_listener > Polling shutdown failed: channel closed
2023-11-02T11:22:08.477Z ERROR TaskClient-BaseNymClient-real_traffic_controller-ack_control-input_message_listener > Assuming this means we should shutdown...
```
`send_plain_message()` is async, but **it only blocks until the message is placed in the client's internal queue**, not until it's actually sent into the Mixnet. After queuing, the client still needs to route-encrypt the message and interleave it with cover traffic.
Using the following piece of code as an example:
Make sure the program stays alive long enough. In practice this means awaiting a response or calling `sleep` before disconnecting:
```rust
use nym_sdk::mixnet::{MixnetClient, MixnetMessageSender, Recipient};
use clap::Parser;
// Send a message
client.send_plain_message(recipient, "hello").await.unwrap();
#[derive(Debug, Clone, Parser)]
enum Opts {
Client {
recipient: Recipient
// Wait for the reply (keeps the client alive)
if let Some(received) = client.wait_for_messages().await {
for r in received {
println!("Received: {}", String::from_utf8_lossy(&r.message));
}
}
#[tokio::main]
async fn main() {
let opts: Opts = Parser::parse();
nym_bin_common::logging::setup_logging();
let mut nym_client = MixnetClient::connect_new().await.expect("Could not build Nym client");
match opts {
Opts::Client { recipient } => {
nym_client.send_plain_message(recipient, "some message string").await.expect("send failed");
}
}
}
// Always disconnect gracefully
client.disconnect().await;
```
This is a simplified snippet of code for sending a simple hardcoded message with the following command:
```sh
cargo run client <RECIPIENT_NYM_ADDRESS>
```
You might assume that `send`-ing your message would _just work_ as `nym_client.send_plain_message()` is an async function; you might expect that the client will block until the message is actually sent into the mixnet, then shutdown.
However, this is not true.
**This will only block until the message is put into client's internal queue**. Therefore in the above example, the client is being shut down before the message is _actually sent to the mixnet_; after being placed in the client's internal queue, there is still work to be done under the hood, such as route encrypting the message and placing it amongst the stream of cover traffic.
The simple solution? Make sure the program/client stays active, either by calling `sleep`, or listening out for new messages. As sending a one-shot message without listening out for a response is likely not what you'll be doing, then you will be then awaiting a response (see the [message helpers page](./message-helpers) for an example of this).
Furthermore, you should always **manually disconnect your client** with `client.disconnect().await` as seen in the code examples. This is important as your client is writing to a local DB and dealing with SURB storage.
## Client receives empty messages when listening for response
If you are sending out a message, it makes sense for your client to then listen out for incoming messages; this would probably be the reply you get from the service you've sent a message to.
You might however be receiving messages without data attached to them / empty payloads. This is most likely because your client is receiving a message containing a [SURB request](../../../network/traffic/anonymous-replies) - a SURB requesting more SURB packets to be sent to the service, in order for them to have enough packets (with a big enough overall payload) to split the entire response to your initial request across.
Whether the `data` of a SURB request being empty is a feature or a bug is to be decided - there is some discussion surrounding whether we can use SURB requests to send additional data to streamline the process of sending large replies across the mixnet.
You can find a few helper functions [here](./message-helpers) to help deal with this issue in the meantime.
> If you can think of a more succinct or different way of handling this do reach out - we're happy to hear other opinions
## Lots of `duplicate fragment received` messages
You might see a lot of `WARN` level logs about duplicate fragments in your logs, depending on the log level you're using. This occurs when a packet is retransmitted somewhere in the Mixnet, but then the original makes it to the destination client as well. This is not something to do with your client logic, but instead the state of the Mixnet.
`WARN` level logs about duplicate fragments are caused by Mixnet-level packet retransmission: the original and the retransmitted copy both arrive. This is not a bug in your client logic.
@@ -0,0 +1,314 @@
---
title: "Mixnet Tutorial: Send Your First Private Message"
description: "Step-by-step Rust tutorial to connect to the Nym mixnet, send and receive messages, reply anonymously with SURBs, and persist client identity."
schemaType: "HowTo"
section: "Developers"
lastUpdated: "2026-03-26"
---
# Tutorial: Send Your First Private Message
import { Callout } from 'nextra/components'
import { CodeVerified } from '../../../../components/code-verified'
By the end of this tutorial you'll have a working program that sends a Sphinx-encrypted message to itself through the Nym Mixnet, receives it, and replies anonymously using SURBs. The later sections cover persistent identity and concurrent send/receive.
**You'll need:** Rust 1.70+ and an internet connection (clients connect to the live Mixnet).
<CodeVerified />
## Step 1: Set up the project
```sh
cargo init nym-mixnet-demo
cd nym-mixnet-demo
```
Add dependencies to `Cargo.toml`:
```toml
[dependencies]
nym-sdk = { git = "https://github.com/nymtech/nym", rev = "4077717" }
nym-bin-common = { git = "https://github.com/nymtech/nym", rev = "4077717", features = ["basic_tracing"] }
tokio = { version = "1", features = ["full"] }
blake3 = "=1.7.0" # required pin — see https://nymtech.net/developers/rust/importing
```
## Step 2: Connect and send
Replace the contents of `src/main.rs`:
```rust
use nym_sdk::mixnet::{self, MixnetMessageSender};
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_tracing_logger();
// connect_new() creates an ephemeral client — keys are generated in
// memory and discarded on disconnect.
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
let our_address = client.nym_address();
println!("Connected: {our_address}");
// The message is Sphinx-encrypted and mixed across 5 nodes.
// send_plain_message only blocks until the message is queued —
// encryption and mixing happen in background tasks.
client
.send_plain_message(*our_address, "hello from the mixnet!")
.await
.unwrap();
println!("Sent — waiting for arrival...");
```
<Callout type="info">
`setup_tracing_logger()` shows what the SDK is doing under the hood: gateway connections, topology fetches, Sphinx packet encryption. If the output is too verbose, comment out the line or filter with `RUST_LOG=warn cargo run`.
</Callout>
## Step 3: Receive
```rust
// wait_for_messages() returns the next batch of incoming messages.
// Filter empty messages — these are SURB replenishment requests.
let message = loop {
if let Some(msgs) = client.wait_for_messages().await {
if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) {
break msg;
}
}
};
println!("Received: {}", String::from_utf8_lossy(&message.message));
```
## Step 4: Reply anonymously
Every message includes a `sender_tag`, an opaque `AnonymousSenderTag` that lets you reply **without knowing the sender's address**. The SDK bundles SURBs (Single Use Reply Blocks) with every outgoing message by default:
```rust
let sender_tag = message.sender_tag.expect("should have sender tag");
// send_reply uses the SURB — the sender's address is never revealed.
client.send_reply(sender_tag, "hello back, anonymously!").await.unwrap();
let reply = loop {
if let Some(msgs) = client.wait_for_messages().await {
if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) {
break msg;
}
}
};
println!("Reply: {}", String::from_utf8_lossy(&reply.message));
client.disconnect().await;
}
```
## Step 5: Run it
```sh
RUST_LOG=info cargo run
```
```
Connected: 8gk4Y...@2xU4d...
Sent — waiting for arrival...
Received: hello from the mixnet!
Reply: hello back, anonymously!
```
## Going further: persist your identity
The ephemeral client above generates a new address on every run. To keep the same address across restarts, use `MixnetClientBuilder` with on-disk storage:
```rust
use nym_sdk::mixnet::{self, MixnetMessageSender, StoragePaths};
#[tokio::main]
async fn main() {
// Keys are generated on first run, then loaded from disk on subsequent runs.
let paths = StoragePaths::new_from_dir("./my-client-data").unwrap();
let mut client = mixnet::MixnetClientBuilder::new_with_default_storage(paths)
.await
.unwrap()
.build()
.unwrap()
.connect_to_mixnet()
.await
.unwrap();
println!("Address: {}", client.nym_address());
// Same API as before — send, receive, reply.
client
.send_plain_message(*client.nym_address(), "persistent identity!")
.await
.unwrap();
if let Some(msgs) = client.wait_for_messages().await {
for m in msgs.into_iter().filter(|m| !m.message.is_empty()) {
println!("Received: {}", String::from_utf8_lossy(&m.message));
}
}
// Always disconnect for clean shutdown — background tasks need to be
// stopped and state files flushed.
client.disconnect().await;
}
```
Run it twice; the address stays the same.
## Going further: send and receive from different tasks
Add `futures` to your `Cargo.toml`:
```toml
futures = "0.3"
```
Use `split_sender()` to get a clone-able send handle for use in separate tasks:
```rust
use futures::StreamExt;
use nym_sdk::mixnet::{self, MixnetMessageSender};
#[tokio::main]
async fn main() {
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
let addr = *client.nym_address();
// split_sender() returns a clone-able MixnetClientSender.
let sender = client.split_sender();
// Spawn a receiver — the original client implements futures::Stream.
let rx = tokio::spawn(async move {
if let Some(msg) = client.next().await {
println!("Received: {}", String::from_utf8_lossy(&msg.message));
}
client.disconnect().await;
});
// Spawn a sender on a different task.
let tx = tokio::spawn(async move {
sender.send_plain_message(addr, "hello from another task!").await.unwrap();
});
tx.await.unwrap();
rx.await.unwrap();
}
```
## What's happening underneath
`connect_new()` generates an ephemeral identity (ed25519 + x25519 keypair), fetches the current network topology, selects a gateway, and opens a persistent WebSocket connection. `send_plain_message()` wraps the payload in Sphinx packets, layered encryption where each of the 5 Mix Nodes can only decrypt one layer and learn the next hop, never the full route. `wait_for_messages()` drains a local queue fed by the gateway; messages arrive out of order by design, to defeat timing analysis.
SURBs (Single Use Reply Blocks) are pre-computed return routes bundled with each outgoing message. The recipient uses them to reply without learning the sender's address. Each is single-use; the SDK replenishes them automatically.
`split_sender()` clones the send channel while the original client retains the receive side. Both halves can run on separate tokio tasks without synchronization.
## Complete code
### Ephemeral client
New address on every run, good for quick experiments:
```rust
use nym_sdk::mixnet::{self, MixnetMessageSender};
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_tracing_logger();
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
let our_address = client.nym_address();
println!("Connected: {our_address}");
client
.send_plain_message(*our_address, "hello from the mixnet!")
.await
.unwrap();
println!("Sent — waiting for arrival...");
let message = loop {
if let Some(msgs) = client.wait_for_messages().await {
if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) {
break msg;
}
}
};
println!("Received: {}", String::from_utf8_lossy(&message.message));
let sender_tag = message.sender_tag.expect("should have sender tag");
client.send_reply(sender_tag, "hello back, anonymously!").await.unwrap();
let reply = loop {
if let Some(msgs) = client.wait_for_messages().await {
if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) {
break msg;
}
}
};
println!("Reply: {}", String::from_utf8_lossy(&reply.message));
client.disconnect().await;
}
```
### Persistent identity
Same address across restarts. Use this for real applications:
```rust
use nym_sdk::mixnet::{self, MixnetMessageSender, StoragePaths};
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_tracing_logger();
let paths = StoragePaths::new_from_dir("./my-client-data").unwrap();
let mut client = mixnet::MixnetClientBuilder::new_with_default_storage(paths)
.await
.unwrap()
.build()
.unwrap()
.connect_to_mixnet()
.await
.unwrap();
let our_address = client.nym_address();
println!("Connected: {our_address}");
client
.send_plain_message(*our_address, "hello from the mixnet!")
.await
.unwrap();
println!("Sent — waiting for arrival...");
let message = loop {
if let Some(msgs) = client.wait_for_messages().await {
if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) {
break msg;
}
}
};
println!("Received: {}", String::from_utf8_lossy(&message.message));
let sender_tag = message.sender_tag.expect("should have sender tag");
client.send_reply(sender_tag, "hello back, anonymously!").await.unwrap();
let reply = loop {
if let Some(msgs) = client.wait_for_messages().await {
if let Some(msg) = msgs.into_iter().find(|m| !m.message.is_empty()) {
break msg;
}
}
};
println!("Reply: {}", String::from_utf8_lossy(&reply.message));
client.disconnect().await;
}
```
@@ -0,0 +1,149 @@
---
title: "Stream Module: AsyncRead/AsyncWrite Over the Mixnet"
description: "The Nym Stream module provides persistent, bidirectional byte channels over the mixnet with standard Rust AsyncRead and AsyncWrite traits."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-03-15"
---
# Stream Module
import { Callout } from 'nextra/components'
import { CratesPaused } from '../../../components/crates-paused'
<CratesPaused />
The Mixnet is fundamentally message-based: no persistent connections, no guaranteed ordering, no TCP. The default [message API](./mixnet) works at this level, sending individual payloads independently through Mix Nodes. This is effective for privacy but unlike how most networking code is structured.
The **Stream module** bridges the gap by providing persistent, bidirectional byte channels that behave like TCP sockets. Each `MixnetStream` implements [`AsyncRead`](https://docs.rs/tokio/latest/tokio/io/trait.AsyncRead.html) and [`AsyncWrite`](https://docs.rs/tokio/latest/tokio/io/trait.AsyncWrite.html), so `tokio::io::copy`, codecs, `BufReader`/`BufWriter`, and any other async I/O consumer work without modification. **If you're coming from socket-based networking, start here.**
All streams are multiplexed over a single `MixnetClient`. A background router task reads a small header on each incoming message and dispatches the payload to the correct stream by ID, so multiple concurrent streams require no additional connections or gateways.
## How it works
The two sides of a stream connection follow a client/server pattern:
1. **Opener** calls `client.open_stream(recipient, surbs)`. This generates a random `StreamId`, registers the stream locally, and sends an `Open` message through the Mixnet.
2. **Listener** calls `listener.accept()`, which blocks until an `Open` arrives, registers the new stream, and returns a `MixnetStream` ready for reading and writing.
3. Both sides read and write using standard `AsyncRead`/`AsyncWrite`. Bytes are wrapped in a 16-byte LP frame header (stream ID, message type, sequence number), routed through the Mixnet, and demultiplexed on arrival.
4. **Cleanup** happens on `drop`. The stream deregisters from the local router. No close message is sent over the wire, since a close could race ahead of in-flight data.
```text
┌─────────────────────────────────────────────────────────┐
│ MixnetClient │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ MixnetStream │ │ MixnetStream │ ... │
│ │ (peer A) │ │ (peer B) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │writes │writes │
│ ▼ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ ClientInput.input_sender │ │
│ └──────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ── mixnet ── │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ reconstructed_receiver │ │
│ └──────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Router task │ │
│ │ decode header → dispatch by ID │ │
│ └──┬──────────────────────────┬───┘ │
│ │ Open messages │ Data messages │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │MixnetListener│ │ StreamMap lookup │ │
│ │ .accept() │ │ → per-stream tx │ │
│ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Complete example
A minimal example with two clients on the same machine: one opens a stream to the other, sends a message, and reads a reply.
```rust
use nym_sdk::mixnet;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use std::time::Duration;
const TIMEOUT: Duration = Duration::from_secs(60);
#[tokio::main]
async fn main() {
// Connect two ephemeral clients
let mut sender = mixnet::MixnetClient::connect_new().await.unwrap();
let mut receiver = mixnet::MixnetClient::connect_new().await.unwrap();
let receiver_addr = *receiver.nym_address();
// The receiver creates a listener (activates stream mode)
let mut listener = receiver.listener().unwrap();
// The sender opens a stream to the receiver's Nym address
let mut outbound = sender.open_stream(receiver_addr, None).await.unwrap();
// The receiver accepts the incoming stream
let mut inbound = tokio::time::timeout(TIMEOUT, listener.accept())
.await
.expect("timed out")
.expect("listener closed");
// Send data and read it back — just like a TCP socket
outbound.write_all(b"hello from sender").await.unwrap();
outbound.flush().await.unwrap();
let mut buf = vec![0u8; 1024];
let n = tokio::time::timeout(TIMEOUT, inbound.read(&mut buf))
.await
.expect("timed out")
.expect("read failed");
println!("Receiver got: {}", String::from_utf8_lossy(&buf[..n]));
// Reply back through the same stream
inbound.write_all(b"hello from receiver").await.unwrap();
inbound.flush().await.unwrap();
let n = tokio::time::timeout(TIMEOUT, outbound.read(&mut buf))
.await
.expect("timed out")
.expect("read failed");
println!("Sender got: {}", String::from_utf8_lossy(&buf[..n]));
// Streams deregister on drop, then disconnect clients
drop(outbound);
drop(inbound);
sender.disconnect().await;
receiver.disconnect().await;
}
```
<Callout type="info">
The receiver replies via **reply SURBs** (Single Use Reply Blocks) and never learns the sender's Nym address.
</Callout>
## When to use streams vs messages
| | Messages | Streams | TcpProxy |
|---|---|---|---|
| **Pattern** | Raw message payloads | Persistent bidirectional channels | TCP socket proxying |
| **API** | `send_plain_message()` / `wait_for_messages()` | `AsyncRead` + `AsyncWrite` | Localhost TCP socket |
| **Multiplexing** | N/A | Multiple streams per client | One client per TCP connection |
| **Ordering** | No guarantees | Sequence-based reordering | Session-based ordering |
| **Best for** | Simple notifications, one-shot requests | Interactive protocols, streaming data, any code expecting async I/O | Wrapping existing TCP applications |
| **Status** | Stable | New | Deprecated |
<Callout type="warning">
**Streams and messages are mutually exclusive.** Once you call `open_stream()` or `listener()`, the message-based API (`send_plain_message`, `wait_for_messages`) is permanently disabled on that client. This is a one-way transition: there is no switching back without disconnecting and reconnecting. See the [`stream_mode_guard.rs` example](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/stream_mode_guard.rs) for details.
</Callout>
## Next steps
- [Tutorial: Build a private echo server](./stream/tutorial): server and client communicating over streams
- [Architecture](./stream/architecture): wire protocol, router task, data flow, stream cleanup, and known limitations
- [Examples](./stream/examples): annotated walkthroughs of the SDK examples (multi-stream, idle timeout, throughput testing)
@@ -0,0 +1,5 @@
{
"tutorial": "Tutorial",
"architecture": "Architecture",
"examples": "Examples"
}
@@ -0,0 +1,92 @@
---
title: "Stream Module Architecture"
description: "Internal architecture of the Nym Stream subsystem: wire protocol, multiplexing, router task, and how concurrent byte channels share a single MixnetClient."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-03-15"
---
# Stream Architecture
import { Callout } from 'nextra/components'
{/* Canonical source: sdk/rust/nym-sdk/src/mixnet/stream/ARCHITECTURE.md */}
## Overview
The stream subsystem gives each `MixnetClient` the ability to hold many concurrent byte channels (`AsyncRead + AsyncWrite`) to different remote peers, multiplexed over a single client connection.
```mermaid
---
config:
theme: neo-dark
---
flowchart TD
subgraph MixnetClient
SA["MixnetStream A"] -->|writes| CI["Client input channel"]
SB["MixnetStream B"] -->|writes| CI
CI --> MX["── Mixnet ──"]
MX --> RT["Router task"]
RT -->|Open messages| ML["MixnetListener.accept()"]
RT -->|Data messages| SM["Stream routing table"]
SM --> SA
SM --> SB
end
```
## Wire protocol
Every stream message has a fixed 16-byte LP frame header prepended to the payload:
```
[LpFrameKind: 2 bytes LE][StreamId: 8 bytes BE][MsgType: 1 byte][SequenceNum: 4 bytes BE][Reserved: 1 byte][payload ...]
```
- **LpFrameKind:** `3` (SphinxStream). Distinguishes stream traffic from other LP frame types (Opaque, Registration, Forward).
- **StreamId:** random `u64` generated by the opener, used to multiplex streams.
- **MsgType:** `Open` (0) or `Data` (1).
- **SequenceNum:** `u32` counter, incremented per write. Used by the receiver's per-stream reorder buffer to deliver data in the correct order.
- **Reserved:** must be `0x00`.
There is no `Close` message type; see [Known Limitations](#known-limitations) for why.
## Stream mode
Stream mode is activated lazily on the first call to `open_stream()` or `listener()`. This is a **one-way transition**:
1. The client's message receiver is handed off to a background router task
2. `stream_mode` flag is set to `true`
3. Message-based methods (`send_plain_message`, `wait_for_messages`) are disabled and return errors
There is no switching back without disconnecting and creating a new client.
## Opening and accepting streams
**Opening (outbound):**
1. `open_stream(recipient, surbs)` generates a random `StreamId`
2. An `Open` message is sent through the Mixnet to the recipient
3. A `MixnetStream` is returned, ready for writing and reading
**Accepting (inbound):**
1. `listener.accept()` waits for an `Open` message from a remote peer
2. A `MixnetStream` is created with the opener's `sender_tag` for anonymous replies
3. The stream is ready for bidirectional I/O
## Cleanup
- **On `drop`:** the stream deregisters from the routing table. No close message is sent over the wire.
- **Idle timeout:** streams idle for longer than the configured timeout (default: 30 minutes) are automatically cleaned up. Configure with [`MixnetClientBuilder::with_stream_idle_timeout()`](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.MixnetClientBuilder.html).
## Known limitations
<Callout type="info">
**Sequence-based reordering.** The Mixnet does not guarantee message ordering at the transport level, but each stream write includes a `sequence_num` in the LP frame header. The receiver maintains a per-stream reorder buffer (BTreeMap keyed by sequence number) that buffers out-of-order messages and drains them in sequence. This means protocols that depend on byte ordering (HTTP, TLS, protobuf) work correctly over streams.
- **Buffer cap:** 256 messages per stream. If the buffer fills (e.g. a large gap in sequence numbers), the receiver skips ahead to the lowest buffered sequence.
- **Duplicates:** messages with a sequence number below the next expected are dropped.
- There is no `Close` message type, since a close could race ahead of in-flight data.
</Callout>
## Internal details
For the full implementation details (router task, `StreamMap`, `PollSender` usage, base-client type rationale), see the `ARCHITECTURE.md` file next to the module source code. This will also be available on [docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/) once crate publication resumes with the Lewes Protocol.
@@ -0,0 +1,22 @@
---
title: "Stream Module Examples"
description: "Runnable Rust examples for the Nym Stream module: bidirectional read/write, idle timeouts, mode guards, and throughput benchmarks."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-03-15"
---
# Examples
Runnable examples in [`sdk/rust/nym-sdk/examples/`](https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/examples). Each file is self-contained with step-by-step comments.
```bash
cargo run --example <name>
```
| Example | Source | What it demonstrates |
|---|---|---|
| Simple Read/Write | [`stream_simple_read_write.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/stream_simple_read_write.rs) | Multiple concurrent streams, bidirectional communication |
| Idle Timeout | [`stream_idle_timeout.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/stream_idle_timeout.rs) | Configuring `with_stream_idle_timeout`, observing EOF after cleanup |
| Mode Guard | [`stream_mode_guard.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/stream_mode_guard.rs) | Mutual exclusion between stream and message modes |
| Throughput | [`stream_throughput.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/stream_throughput.rs) | Sending 1 MB over a single stream, verifying data integrity |
@@ -0,0 +1,353 @@
---
title: "Stream Tutorial: Build a Private Echo Server"
description: "Step-by-step Rust tutorial to build an echo server and client communicating through the Nym mixnet using AsyncRead and AsyncWrite streams."
schemaType: "HowTo"
section: "Developers"
lastUpdated: "2026-03-26"
---
# Tutorial: Build a Private Echo Server
import { Callout } from 'nextra/components'
import { CodeVerified } from '../../../../components/code-verified'
In this tutorial you'll build two programs: a server that listens for incoming streams and echoes back whatever it receives, and a client that opens a stream, sends data, and reads the echo. Both communicate through the Nym Mixnet using `AsyncRead` and `AsyncWrite`, just like TCP sockets.
## What you'll learn
- Setting up a `MixnetListener` to accept incoming streams
- Opening an outbound stream with `open_stream()`
- Reading and writing with standard tokio I/O traits
- How streams are multiplexed over a single `MixnetClient`
- Clean shutdown and stream lifecycle
<CodeVerified />
## Prerequisites
- Rust toolchain (1.70+)
- A working internet connection (clients connect to the live Nym Mixnet)
## Step 1: Set up the project
```sh
cargo init nym-echo
cd nym-echo
rm src/main.rs
```
Add dependencies to `Cargo.toml`:
```toml
[dependencies]
nym-sdk = { git = "https://github.com/nymtech/nym", rev = "4077717" }
nym-bin-common = { git = "https://github.com/nymtech/nym", rev = "4077717", features = ["basic_tracing"] }
tokio = { version = "1", features = ["full"] }
blake3 = "=1.7.0" # required pin — see https://nymtech.net/developers/rust/importing
```
## Step 2: Build the echo server
The server connects a `MixnetClient`, creates a listener, and accepts streams in a loop. Each stream gets its own task that reads data and writes it back.
Create `src/bin/server.rs`:
```rust
use nym_sdk::mixnet;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_tracing_logger();
// Connect to the Mixnet
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
println!("Echo server listening at: {}", client.nym_address());
// Create a listener — this activates stream mode.
// From this point, message-based methods are disabled.
let mut listener = client.listener().unwrap();
// Accept streams in a loop
loop {
let mut stream = match listener.accept().await {
Some(s) => s,
None => {
println!("Listener closed");
break;
}
};
let stream_id = stream.id();
println!("Accepted stream {stream_id}");
// Spawn a task to handle each stream concurrently
tokio::spawn(async move {
let mut buf = vec![0u8; 4096];
loop {
let n = match stream.read(&mut buf).await {
Ok(0) => break, // EOF — stream closed
Ok(n) => n,
Err(e) => {
eprintln!("Stream {stream_id} read error: {e}");
break;
}
};
let data = &buf[..n];
println!(
"Stream {stream_id} received {} bytes: {:?}",
n,
String::from_utf8_lossy(data)
);
// Echo it back
if let Err(e) = stream.write_all(data).await {
eprintln!("Stream {stream_id} write error: {e}");
break;
}
stream.flush().await.unwrap();
}
println!("Stream {stream_id} closed");
});
}
}
```
<Callout type="info">
**`listener()` can only be called once per client.** It takes exclusive ownership of the inbound message channel. A second call returns `Error::ListenerAlreadyTaken`.
</Callout>
## Step 3: Build the client
The client connects, opens a stream to the server, sends a few messages, reads back the echoes, and disconnects.
Create `src/bin/client.rs`:
```rust
use nym_sdk::mixnet::{self, Recipient};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
const TIMEOUT: Duration = Duration::from_secs(60);
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_tracing_logger();
// Read the server's Nym address from the command line
let server_addr: Recipient = std::env::args()
.nth(1)
.expect("Usage: client <SERVER_NYM_ADDRESS>")
.parse()
.expect("Invalid Nym address");
// Connect to the Mixnet
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
println!("Client address: {}", client.nym_address());
// Open a stream to the server.
// The second argument (None) uses the default number of reply SURBs.
let mut stream = client.open_stream(server_addr, None).await.unwrap();
println!("Stream opened: {}", stream.id());
// Give the Open message time to traverse the mixnet and reach the server.
// open_stream() returns immediately after sending — it doesn't wait for
// the server to accept. Writing too soon risks the data arriving before
// the Open, which the server would drop.
tokio::time::sleep(Duration::from_secs(5)).await;
// Send three messages and read back the echo for each
for i in 1..=3 {
let msg = format!("message {i}");
println!("Sending: {msg}");
stream.write_all(msg.as_bytes()).await.unwrap();
stream.flush().await.unwrap();
// Read the echo
let mut buf = vec![0u8; 1024];
let n = tokio::time::timeout(TIMEOUT, stream.read(&mut buf))
.await
.expect("timed out waiting for echo")
.expect("read failed");
println!("Echo: {}", String::from_utf8_lossy(&buf[..n]));
}
// Drop the stream to deregister it from the router
drop(stream);
// Disconnect the client
client.disconnect().await;
println!("Done!");
}
```
## Step 4: Run it
In one terminal, start the server:
```sh
RUST_LOG=info cargo run --bin server
```
It prints its Nym address:
```
Echo server listening at: 8gk4Y...@2xU4d...
```
In a second terminal, start the client with the server's address:
```sh
RUST_LOG=info cargo run --bin client -- 8gk4Y...@2xU4d...
```
You'll see the messages traverse the Mixnet and echo back:
```
Client address: F3qR7...@9nK2m...
Stream opened: 12345678
Sending: message 1
Echo: message 1
Sending: message 2
Echo: message 2
Sending: message 3
Echo: message 3
Done!
```
On the server side:
```
Accepted stream 12345678
Stream 12345678 received 9 bytes: "message 1"
Stream 12345678 received 9 bytes: "message 2"
Stream 12345678 received 9 bytes: "message 3"
Stream 12345678 closed
```
## How it works internally
1. The server's `listener()` activates **stream mode**, which spawns a **router task** that decodes incoming Mixnet messages and dispatches them by stream ID.
2. The client's `open_stream()` generates a random 8-byte `StreamId`, sends an `Open` message through the Mixnet, and registers the stream in a local routing table.
3. When the server's router receives the `Open` message, it delivers it to `listener.accept()`, which creates the inbound `MixnetStream`.
4. Each `write_all()` prepends a 16-byte LP frame header (`[LpFrameKind: 2B][StreamId: 8B][MsgType: 1B][SequenceNum: 4B][Reserved: 1B]`) and sends the data through the Mixnet as a Sphinx packet.
5. On arrival, the router reads the `LpFrameKind` to identify it as stream traffic, decodes the header, finds the matching stream by ID, and delivers the raw payload to `read()`.
6. The inbound stream replies via **reply SURBs**, the same anonymous reply mechanism as the message API, applied transparently. The server never learns the client's Nym address.
7. When a stream is dropped, it deregisters from the local router. No close message is sent over the wire, since a close could race ahead of in-flight data.
See the [Architecture](./architecture) page for the full technical details.
## What you've learned
- `client.listener()` activates stream mode and returns a `MixnetListener`
- `listener.accept()` blocks until a remote peer opens a stream
- `client.open_stream(recipient, surbs)` opens an outbound stream to a Nym address
- `MixnetStream` implements `AsyncRead + AsyncWrite`, so standard tokio I/O works unchanged
- Multiple streams are multiplexed over a single client
- Streams deregister on `drop`; no close handshake is needed
- The server replies via SURBs and never learns the client's address
## Complete code
### Server (`src/bin/server.rs`)
```rust
use nym_sdk::mixnet;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_tracing_logger();
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
println!("Echo server listening at: {}", client.nym_address());
let mut listener = client.listener().unwrap();
loop {
let mut stream = match listener.accept().await {
Some(s) => s,
None => break,
};
let stream_id = stream.id();
println!("Accepted stream {stream_id}");
tokio::spawn(async move {
let mut buf = vec![0u8; 4096];
loop {
let n = match stream.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(n) => n,
};
if let Err(_) = stream.write_all(&buf[..n]).await {
break;
}
stream.flush().await.unwrap();
}
println!("Stream {stream_id} closed");
});
}
}
```
### Client (`src/bin/client.rs`)
```rust
use nym_sdk::mixnet::{self, Recipient};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
const TIMEOUT: Duration = Duration::from_secs(60);
#[tokio::main]
async fn main() {
nym_bin_common::logging::setup_tracing_logger();
let server_addr: Recipient = std::env::args()
.nth(1)
.expect("Usage: client <SERVER_NYM_ADDRESS>")
.parse()
.expect("Invalid Nym address");
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
println!("Client address: {}", client.nym_address());
let mut stream = client.open_stream(server_addr, None).await.unwrap();
println!("Stream opened: {}", stream.id());
// Wait for the Open message to reach the server through the mixnet
tokio::time::sleep(Duration::from_secs(5)).await;
for i in 1..=3 {
let msg = format!("message {i}");
println!("Sending: {msg}");
stream.write_all(msg.as_bytes()).await.unwrap();
stream.flush().await.unwrap();
let mut buf = vec![0u8; 1024];
let n = tokio::time::timeout(TIMEOUT, stream.read(&mut buf))
.await
.expect("timed out waiting for echo")
.expect("read failed");
println!("Echo: {}", String::from_utf8_lossy(&buf[..n]));
}
drop(stream);
client.disconnect().await;
println!("Done!");
}
```
@@ -3,14 +3,253 @@ title: "Nym TcpProxy: Route TCP via the Mixnet"
description: "Route TCP traffic through the Nym mixnet using the TcpProxy Rust module. Covers architecture, single and multi-connection patterns, and troubleshooting."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-02-11"
lastUpdated: "2026-03-27"
---
# TcpProxy Module
import { Callout } from 'nextra/components';
import { CodeVerified } from '../../../components/code-verified'
This module exposes the `TcpProxyClient` and the `TcpProxyServer` which can be used to proxy traffic through the Mixnet in a way that is more familiar to developers than the methods exposed by the [`Mixnet` module](./mixnet).
<Callout type="error">
**This module is unmaintained.** The TcpProxy is no longer actively developed in favour of the [Stream module](./stream), which provides `AsyncRead + AsyncWrite` streams directly over the Mixnet without the TCP socket overhead. Existing users should plan to migrate to streams when possible. The TcpProxy will continue to work but will not receive new features or bug fixes.
</Callout>
Both `Client` and `Server` are intended to be initialised and then run in a background thread, exposing a configurable `localhost` socket which developers can read/write/stream to without having to worry about the message-based nature of sending and receiving traffic to/from the Mixnet.
The Stream module offers the same key benefit (familiar I/O patterns on top of the Mixnet) with a simpler API. Streams multiplex connections on a single client, eliminate the localhost socket overhead, and now include sequence-based message reordering. There is no remaining reason to choose TcpProxy over Streams for new projects.
---
`NymProxyClient` and `NymProxyServer` proxy TCP traffic through the Mixnet. Both run in a background thread and expose a configurable `localhost` socket that you read and write to like any other TCP connection.
> Non-Rust/Go developers who want to experiment with this module can start with the [standalone binaries](../tools/standalone-tcpproxy).
## Examples
| Example | Source |
|---|---|
| Single connection | [`tcp_proxy_single_connection.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/tcp_proxy_single_connection.rs) |
| Multiple connections | [`tcp_proxy_multistream.rs`](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/tcp_proxy_multistream.rs) |
```bash
cargo run --example tcp_proxy_single_connection
cargo run --example tcp_proxy_multistream
```
## API reference
- [API reference on docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/tcp_proxy/): architecture overview, client/server examples, and type documentation
## Tutorial
<CodeVerified />
Set up the project:
```sh
cargo init nym-tcp-proxy
cd nym-tcp-proxy
rm src/main.rs
```
Add dependencies to `Cargo.toml`:
```toml
[dependencies]
nym-sdk = { git = "https://github.com/nymtech/nym", rev = "4077717" }
nym-network-defaults = { git = "https://github.com/nymtech/nym", rev = "4077717" }
nym-bin-common = { git = "https://github.com/nymtech/nym", rev = "4077717", features = ["basic_tracing"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
blake3 = "=1.7.0" # required pin — see https://nymtech.net/developers/rust/importing
[[bin]]
name = "proxy_server"
path = "src/bin/proxy_server.rs"
[[bin]]
name = "proxy_client"
path = "src/bin/proxy_client.rs"
```
### Server
The server connects to the Mixnet and forwards incoming traffic to a local TCP service (e.g. a web server on port 8000).
```rust
use nym_sdk::tcp_proxy::NymProxyServer;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
nym_bin_common::logging::setup_tracing_logger();
let mut server = NymProxyServer::new(
"127.0.0.1:8000", // upstream address (host:port)
"./proxy-server-config", // config directory for persistent keys
None, // env file (None = mainnet)
None, // gateway (None = auto-select)
).await?;
println!("Proxy server address: {}", server.nym_address());
server.run_with_shutdown().await?;
Ok(())
}
```
### Client
The client opens a localhost TCP socket and tunnels all traffic through the Mixnet to the server.
```rust
use nym_sdk::tcp_proxy::NymProxyClient;
use nym_sdk::mixnet::Recipient;
use nym_network_defaults::setup_env;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
nym_bin_common::logging::setup_tracing_logger();
// Load mainnet network defaults into env vars (required by NymProxyClient's internal ClientPool)
setup_env(None::<String>);
let server_addr: Recipient = std::env::args()
.nth(1).expect("Usage: proxy_client <SERVER_NYM_ADDRESS>")
.parse()?;
let client = NymProxyClient::new(
server_addr,
"127.0.0.1", // listen host
"8070", // listen port
60, // close timeout (seconds)
None, // env file (None = mainnet)
1, // client pool size
).await?;
let proxy = tokio::spawn(async move { client.run().await });
// Wait for the pool to create a client and the proxy to be ready.
// The first startup takes ~10-15s while the client connects to the Mixnet.
println!("Waiting for proxy to be ready...");
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
let mut stream = TcpStream::connect("127.0.0.1:8070").await?;
stream.write_all(b"GET / HTTP/1.0\r\nHost: localhost\r\n\r\n").await?;
let mut response = Vec::new();
stream.read_to_end(&mut response).await?;
println!("Response:\n{}", String::from_utf8_lossy(&response));
drop(stream);
proxy.abort();
Ok(())
}
```
### Run it
Start an upstream TCP service (e.g. a simple HTTP server):
```sh
python3 -m http.server 8000
```
In a second terminal, start the proxy server:
```sh
RUST_LOG=info cargo run --bin proxy_server
```
Copy the Nym address it prints, then in a third terminal:
```sh
RUST_LOG=info cargo run --bin proxy_client -- <SERVER_NYM_ADDRESS>
```
The response will take 3060 seconds to arrive as it traverses the Mixnet in both directions.
## Architecture
Each sub-module handles Nym clients differently:
- **`NymProxyClient`** relies on the [Client Pool](./client-pool) to create clients and keep a reserve. If incoming TCP connections outpace the pool, it creates an ephemeral client per connection. One client maps to one TCP connection.
- **`NymProxyServer`** has a single Nym client with a persistent identity.
### Sessions & message ordering
Messages are wrapped in a session ID per connection, with individual messages given an incrementing message ID. Once all messages are sent, the client sends a `Close` message to notify the server that there are no more outbound messages for this session.
> Session management and message IDs are necessary since *the Mixnet guarantees message delivery but not message ordering*: in the case of trying to e.g. send gRPC protobuf through the Mixnet, ordering is required so that a buffer is not split across Sphinx packet payloads, and that the 2nd half of the frame is not passed upstream to the parser before the 1st half.
The key data structure:
```rust
pub struct ProxiedMessage {
message: Payload,
session_id: Uuid,
message_id: u16,
}
```
### Full request/response flow
```mermaid
---
config:
theme: neo-dark
layout: elk
---
sequenceDiagram
box Local Machine
participant Client Process
participant NymProxyClient
end
Client Process->>NymProxyClient: Request bytes
NymProxyClient->>NymProxyClient: New session
NymProxyClient->>Entry Gateway: Sphinx Packets: Message 1
Entry Gateway-->>NymProxyClient: Acks
NymProxyClient->>Entry Gateway: Sphinx Packets: Message 2
Entry Gateway-->>NymProxyClient: Acks
NymProxyClient->>Entry Gateway: Sphinx Packets: Close Message
Entry Gateway-->>NymProxyClient: Acks
Entry Gateway-->>Mix Nodes: All Packets, Acks, etc
Note right of Mix Nodes: We are omitting the 3 hops etc for brevity here
Mix Nodes-->> Exit Gateway: All Packets, Acks, etc
Exit Gateway->>NymProxyServer: Sphinx Packets: Message 2
NymProxyServer-->>Exit Gateway: Acks
loop Message Buffer
NymProxyServer->>NymProxyServer: Wait for Message 1
Exit Gateway->>NymProxyServer: Sphinx Packets: Message 1
NymProxyServer-->>Exit Gateway: Acks
NymProxyServer->>NymProxyServer: Message Received: trigger upstream send
end
Note right of NymProxyServer: Note this happens **per session**
NymProxyServer->>Upstream Process: Reconstructed request bytes
Upstream Process->>Upstream Process: Do something with request
Exit Gateway->>NymProxyServer: Sphinx Packets: Close Message
NymProxyServer-->>Exit Gateway: Acks
NymProxyServer->>NymProxyServer: Trigger Client timeout start for session
Upstream Process->>NymProxyServer: Response bytes
NymProxyServer->>NymProxyServer: Write to provided SURB payloads
NymProxyServer->>Exit Gateway: Anonymous replies
box Remote Host
participant NymProxyServer
participant Upstream Process
end
Entry Gateway->>NymProxyClient: Sphinx Packets: Reply Message 2
NymProxyClient-->Entry Gateway: Ack
Loop Message Buffer:
NymProxyClient->>NymProxyClient: Wait for Message 1
Entry Gateway->>NymProxyClient: Sphinx Packets: Message 1
NymProxyClient-->>Entry Gateway: Acks
NymProxyClient->>NymProxyClient: Message Received: trigger send
NymProxyClient->>Client Process: Response bytes
end
Note right of NymProxyClient: Note this happens **per session**
```
## Troubleshooting
### Lots of `duplicate fragment received` messages
`WARN` level logs about duplicate fragments are caused by Mixnet-level packet retransmission, where both the original and the retransmitted copy arrive at the destination. This is expected behaviour, not a bug in the client or TcpProxy module.
@@ -1,5 +0,0 @@
{
"architecture": "Architecture",
"examples": "Examples",
"troubleshooting": "Troubleshooting"
}
@@ -1,209 +0,0 @@
# Architecture
import { Callout } from 'nextra/components'
## Motivations
The motivation behind the creation of the `TcpProxy` module is to allow developers to interact with the Mixnet in a way that is far more familiar to them: simply setting up a connection with a transport, being returned a socket, and then being able to stream data to/from it, similar to something like the Tor [`arti`](https://gitlab.torproject.org/tpo/core/arti/-/tree/main/crates/arti-client) client.
<Callout type="info" emoji="️">
This is an initial version of the module which we are releasing to developers to experiment with. If you run into problems or any functionality that is missing, do reach out on Matrix and let us know.
Furthermore we will be working on optimisations to the module over time - most of this will occur under the hood (e.g. implementing a configurable connection pool for the `ProxyClient`), but all updates will occur according to SemVer, so don't worry about breaking changes!
</Callout>
## Clients
Each of the sub-modules exposed by the `TcpProxy` deal with Nym clients in a different way.
- the `NymProxyClient` relies on the [`Client Pool`](../client-pool) to create clients and keep a certain number of them in reserve. If the amount of incoming TCP connections rises quicker than the Client Pool can create clients, or you have the pool size set to `0`, the `TcpProxyClient` creates an ephemeral client per new TCP connection, which is closed according to the configurable timeout: we map one ephemeral client per TCP connection. This is to deal with multiple simultaneous streams.
- the `NymProxyServer` has a single Nym client with a persistent identity.
## Framing
We are currently relying on the [`tokio::Bytecodec`](https://docs.rs/tokio-util/latest/tokio_util/codec/struct.BytesCodec.html) and [`framedRead`](https://docs.rs/tokio-util/latest/tokio_util/codec/struct.Framed.html) to frame bytes moving through the `NymProxyClient` and `NymProxyServer`.
> For those interested, under the hood the client uses our own [`NymCodec`](https://github.com/nymtech/nym/blob/27ac34522cf0f8bfe1ca265e0b57ee52f2ded0d2/common/nymsphinx/framing/src/codec.rs) to frame message bytes as Sphinx packet payloads.
## Sessions & Message Ordering
We have implemented session management and message ordering, where messages are wrapped in a session ID per connection, with individual messages being given an incrememting message ID. Once all the messages have been sent, the `NymProxyClient` then sends a `Close` message as the last outgoing message. This is to notify the `NymProxyServer` that there are no more outbound messages for this session, and that it can trigger the session timeout.
> Session management and message IDs are necessary since *the Mixnet guarantees message delivery but not message ordering*: in the case of trying to e.g. send gRPC protobuf through the Mixnet, ordering is required so that a buffer is not split across Sphinx packet payloads, and that the 2nd half of the frame is not passed upstream to the gRPC parser before the 1st half, even if it is received first.
Lets step through a full request/response path between a client process communicating with a remote host via the proxies:
### Outgoing Client Request
The `NymProxyClient` instance, once initialised and running, listens out for incoming TCP connections on its localhost port.
On receiving one, it will create a new session ID and packetise the incoming bytes into messages of the following structure:
```rust
pub struct ProxiedMessage {
message: Payload,
session_id: Uuid,
message_id: u16,
}
```
> This code can be found [here](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/src/tcp_proxy/utils.rs#L147C1-L152C2)
And then send these to the Nym address of the `NymProxyServer` instance. Not much to see here regarding message ordering, as the potential for reordering only starts once packets are travelling through the Mixnet.
```mermaid
---
config:
theme: neo-dark
layout: elk
---
sequenceDiagram
box Local Machine
participant Client Process
participant NymProxyClient
end
Client Process->>NymProxyClient: Request bytes
NymProxyClient->>NymProxyClient: New session
NymProxyClient->>EntryGateway: Sphinx Packets: Message 1
EntryGateway-->>NymProxyClient: Acks
NymProxyClient->>EntryGateway: Sphinx Packets: Message 2
EntryGateway-->>NymProxyClient: Acks
NymProxyClient->>EntryGateway: Sphinx Packets: Message 3
EntryGateway-->>NymProxyClient: Acks
NymProxyClient->>EntryGateway: Sphinx Packets: Close Message
NymProxyClient->>NymProxyClient: Start Client Close timeout
EntryGateway-->>NymProxyClient: Acks
```
### Server Receives Request & Responds
Here is a diagrammatic representation of a situation in which the request arrives out of order, and how the message buffer deals with this so as not to pass a malformed request upstream to the process running on the same remote host:
```mermaid
---
config:
theme: neo-dark
layout: elk
---
sequenceDiagram
Exit Gateway->>NymProxyServer: Sphinx Packets: Message 2
NymProxyServer-->>Exit Gateway: Acks
Exit Gateway->>NymProxyServer: Sphinx Packets: Message 3
NymProxyServer-->>Exit Gateway: Acks
loop Message Buffer
NymProxyServer->>NymProxyServer: Wait for Message 1
Exit Gateway->>NymProxyServer: Sphinx Packets: Message 1
NymProxyServer-->>Exit Gateway: Acks
NymProxyServer->>NymProxyServer: Message Received: trigger upstream send
end
Note right of NymProxyServer: Note this happens **per session**
NymProxyServer->>Upstream Process: Reconstructed request bytes
Upstream Process->>Upstream Process: Do something with request
Exit Gateway->>NymProxyServer: Sphinx Packets: Message Close
NymProxyServer-->>Exit Gateway: Acks
NymProxyServer->>NymProxyServer: Trigger Client timeout start for session
Upstream Process->>NymProxyServer: Response bytes
NymProxyServer->>NymProxyServer: Write to provided SURB payloads
NymProxyServer->>Exit Gateway: Anonymous replies
box Remote Host
participant NymProxyServer
participant Upstream Process
end
```
> Note that this is per-session, with a session mapped to a single TCP connection. Both the `NymProxyClient` and `Server` are able to handle multiple concurrent connections.
### Client Receives Response
The `ProxyClient` deals with incoming traffic in the same way as the `ProxyServer`, with a per-session message queue:
```mermaid
---
config:
theme: neo-dark
layout: elk
---
sequenceDiagram
box Local Machine
participant Client Process
participant NymProxyClient
end
Entry Gateway--xNymProxyClient: Sphinx Packets: Reply Message 1 dropped: No Ack!
Entry Gateway->>NymProxyClient: Sphinx Packets: Reply Message 2
NymProxyClient-->Entry Gateway: Ack
Entry Gateway->>NymProxyClient: Sphinx Packets: Reply Message 3
NymProxyClient-->Entry Gateway: Ack
Loop Message Buffer:
NymProxyClient->>NymProxyClient: Wait for Message 1
Entry Gateway->>NymProxyClient: Sphinx Packets: Message 1
NymProxyClient-->>Entry Gateway: Acks
NymProxyClient->>NymProxyClient: Message Received: trigger send
NymProxyClient->>Client Process: Response bytes
end
Note right of NymProxyClient: Note this happens **per session**
```
After receiving the packets, it can then forward the recoded bytes to the requesting process.
### Full Flow Diagram
```mermaid
---
config:
theme: neo-dark
layout: elk
---
sequenceDiagram
box Local Machine
participant Client Process
participant NymProxyClient
end
Client Process->>NymProxyClient: Request bytes
NymProxyClient->>NymProxyClient: New session
NymProxyClient->>Entry Gateway: Sphinx Packets: Message 1
Entry Gateway-->>NymProxyClient: Acks
NymProxyClient->>Entry Gateway: Sphinx Packets: Message 2
Entry Gateway-->>NymProxyClient: Acks
NymProxyClient->>Entry Gateway: Sphinx Packets: Message 3
Entry Gateway-->>NymProxyClient: Acks
NymProxyClient->>Entry Gateway: Sphinx Packets: Close Message
Entry Gateway-->>NymProxyClient: Acks
Entry Gateway-->>Mix Nodes: All Packets, Acks, etc
Note right of Mix Nodes: We are omitting the 3 hops etc for brevity here
Mix Nodes-->> Exit Gateway: All Packets, Acks, etc
Exit Gateway->>NymProxyServer: Sphinx Packets: Message 2
NymProxyServer-->>Exit Gateway: Acks
Exit Gateway->>NymProxyServer: Sphinx Packets: Message 3
NymProxyServer-->>Exit Gateway: Acks
loop Message Buffer
NymProxyServer->>NymProxyServer: Wait for Message 1
Exit Gateway->>NymProxyServer: Sphinx Packets: Message 1
NymProxyServer-->>Exit Gateway: Acks
NymProxyServer->>NymProxyServer: Message Received: trigger upstream send
end
Note right of NymProxyServer: Note this happens **per session**
NymProxyServer->>Upstream Process: Reconstructed request bytes
Upstream Process->>Upstream Process: Do something with request
Exit Gateway->>NymProxyServer: Sphinx Packets: Close Message
NymProxyServer-->>Exit Gateway: Acks
NymProxyServer->>NymProxyServer: Trigger Client timeout start for session
Upstream Process->>NymProxyServer: Response bytes
NymProxyServer->>NymProxyServer: Write to provided SURB payloads
NymProxyServer->>Exit Gateway: Anonymous replies
box Remote Host
participant NymProxyServer
participant Upstream Process
end
Entry Gateway--xNymProxyClient: Sphinx Packets: Reply Message 1 dropped: No Ack!
Entry Gateway->>NymProxyClient: Sphinx Packets: Reply Message 2
NymProxyClient-->Entry Gateway: Ack
Entry Gateway->>NymProxyClient: Sphinx Packets: Reply Message 3
NymProxyClient-->Entry Gateway: Ack
Loop Message Buffer:
NymProxyClient->>NymProxyClient: Wait for Message 1
Entry Gateway->>NymProxyClient: Sphinx Packets: Message 1
NymProxyClient-->>Entry Gateway: Acks
NymProxyClient->>NymProxyClient: Message Received: trigger send
NymProxyClient->>Client Process: Response bytes
end
Note right of NymProxyClient: Note this happens **per session**
```
@@ -1,4 +0,0 @@
{
"singleconn": "Single Connection",
"multiconn": "Multi Connection"
}
@@ -1,170 +0,0 @@
# Multi Connection Example
import { Callout } from 'nextra/components'
This example starts off several Tcp connections on a loop to a remote endpoint: in this case the `TcpListener` behind the `NymProxyServer` instance on the echo server found in
[`nym/tools/echo-server/`](https://github.com/nymtech/nym/tree/develop/tools/echo-server). It pipes a few messages to it, logs the replies, and keeps track of the number of replies received per connection.
> You can find this code [here](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/tcp_proxy_multistream.rs)
```rust
use nym_sdk::mixnet::Recipient;
use nym_sdk::tcp_proxy;
use rand::rngs::SmallRng;
use rand::Rng;
use rand::SeedableRng;
use serde::{Deserialize, Serialize};
use std::env;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use tokio::signal;
use tokio_stream::StreamExt;
use tokio_util::codec;
use tokio_util::sync::CancellationToken;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[derive(Serialize, Deserialize, Debug)]
struct ExampleMessage {
message_id: i8,
message_bytes: Vec<u8>,
tcp_conn: i8,
}
// This example just starts off a bunch of Tcp connections on a loop to a remote endpoint: in this case the TcpListener behind the NymProxyServer instance on the echo server found in `nym/tools/echo-server/`. It pipes a few messages to it, logs the replies, and keeps track of the number of replies received per connection.
//
// To run:
// - run the echo server with `cargo run`
// - run this example with `cargo run --example tcp_proxy_multistream -- <ECHO_SERVER_NYM_ADDRESS> <ENV_FILE_PATH> <CLIENT_PORT>` e.g.
// cargo run --example tcp_proxy_multistream -- DMHyxo8n6sKWHHTVvjRVDxDSMX8gYXRU1AQ6UpwsrWiB.6STYCWGWyRxqn2juWdgjMkAMsT9EaAzPpLWq5zkS68MB@CJG5zTcmoLijmDrtAiLV9PZHxNz8LQu6hmgA89V2RxxL ../../../envs/canary.env 8080
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let server_address = env::args().nth(1).expect("Server address not provided");
let server: Recipient =
Recipient::try_from_base58_string(&server_address).expect("Invalid server address");
// Comment this out to just see println! statements from this example.
// Nym client logging is very informative but quite verbose.
// The Message Decay related logging gives you an ideas of the internals of the proxy message ordering: you need to switch
// to DEBUG to see the contents of the msg buffer, sphinx packet chunking, etc.
tracing_subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::new("info")
.add_directive("nym_sdk::client_pool=info".parse().unwrap())
.add_directive("nym_sdk::tcp_proxy_client=debug".parse().unwrap()),
)
.init();
let env_path = env::args().nth(2).expect("Env file not specified");
let env = env_path.to_string();
let listen_port = env::args().nth(3).expect("Port not specified");
// Within the TcpProxyClient, individual client shutdown is triggered by the timeout. The final argument is how many clients to keep in reserve in the client pool when running the TcpProxy.
let proxy_client =
tcp_proxy::NymProxyClient::new(server, "127.0.0.1", &listen_port, 45, Some(env), 2).await?;
// For our disconnect() logic below
let proxy_clone = proxy_client.clone();
tokio::spawn(async move {
proxy_client.run().await?;
Ok::<(), anyhow::Error>(())
});
let example_cancel_token = CancellationToken::new();
let client_cancel_token = example_cancel_token.clone();
let watcher_cancel_token = example_cancel_token.clone();
// Cancel listener thread
tokio::spawn(async move {
signal::ctrl_c().await?;
println!(":: CTRL_C received, shutting down + cleanup up proxy server config files");
watcher_cancel_token.cancel();
proxy_clone.disconnect().await;
Ok::<(), anyhow::Error>(())
});
println!("waiting for everything to be set up..");
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
println!("done. sending bytes");
// In the info traces you will see the different session IDs being set up, one for each TcpStream.
for i in 0..8 {
let client_cancel_inner_token = client_cancel_token.clone();
if client_cancel_token.is_cancelled() {
break;
}
let conn_id = i;
let local_tcp_addr = format!("127.0.0.1:{}", listen_port.clone());
tokio::spawn(async move {
// Now the client and server proxies are running we can create and pipe traffic to/from
// a socket on the same port as our ProxyClient instance as if we were just communicating
// between a client and host via a normal TcpStream - albeit with a decent amount of additional latency.
//
// The assumption regarding integration is that you know what you're sending, and will do proper
// framing before and after, know what data types you're expecting; the proxies are just piping bytes
// back and forth using tokio's `Bytecodec` under the hood.
let stream = TcpStream::connect(local_tcp_addr).await?;
let (read, mut write) = stream.into_split();
// Lets just send a bunch of messages to the server with variable delays between them, with a message and tcp connection ids to keep track of ordering on the server side (for illustrative purposes **only**; keeping track of anonymous replies is handled by the proxy under the hood with Single Use Reply Blocks (SURBs); for this illustration we want some kind of app-level message id, but irl most of the time you'll probably be parsing on e.g. the incoming response type instead)
tokio::spawn(async move {
for i in 0..8 {
if client_cancel_inner_token.is_cancelled() {
break;
}
let mut rng = SmallRng::from_entropy();
let delay: f64 = rng.gen_range(2.5..5.0);
tokio::time::sleep(tokio::time::Duration::from_secs_f64(delay)).await;
let random_bytes = gen_bytes_fixed(i as usize);
let msg = ExampleMessage {
message_id: i,
message_bytes: random_bytes,
tcp_conn: conn_id,
};
let serialised = bincode::serialize(&msg)?;
write
.write_all(&serialised)
.await
.expect("couldn't write to stream");
println!(">> client sent msg {} on conn {}", &i, &conn_id);
}
Ok::<(), anyhow::Error>(())
});
tokio::spawn(async move {
let mut reply_counter = 0;
let codec = codec::BytesCodec::new();
let mut framed_read = codec::FramedRead::new(read, codec);
while let Some(Ok(bytes)) = framed_read.next().await {
match bincode::deserialize::<ExampleMessage>(&bytes) {
Ok(msg) => {
reply_counter += 1;
println!("<< conn {} received {}/8", msg.tcp_conn, reply_counter);
}
Err(e) => {
println!("<< client received something that wasn't an example message of {} bytes. error: {}", bytes.len(), e);
}
}
}
});
Ok::<(), anyhow::Error>(())
});
let mut rng = SmallRng::from_entropy();
let delay: f64 = rng.gen_range(4.5..7.0);
tokio::time::sleep(tokio::time::Duration::from_secs_f64(delay)).await;
}
Ok(())
}
// emulate a series of small messages followed by a closing larger one
fn gen_bytes_fixed(i: usize) -> Vec<u8> {
let amounts = [10, 15, 50, 1000, 10, 15, 500, 2000];
let len = amounts[i];
let mut rng = rand::thread_rng();
(0..len).map(|_| rng.gen::<u8>()).collect()
}
```
@@ -1,229 +0,0 @@
# Single Connection Example
import { Callout } from 'nextra/components'
This is a basic example which opens a single TCP connection and writes a bunch of messages between a client and some 'echo server' logic, so only uses a single session under the hood and doesn't really show off the message ordering capabilities; this is mainly just a quick introductory illustration on how:
- the mixnet does message ordering
- the NymProxyClient and NymProxyServer can be hooked into and used to communicate between two otherwise pretty vanilla TcpStreams
For a more irl example check the [multi connection example](./multiconn).
> You can find this code [here](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/tcp_proxy_single_connection.rs)
```rust
use nym_sdk::tcp_proxy;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::sync::atomic::{AtomicU8, Ordering};
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
use tokio::signal;
use tokio_stream::StreamExt;
use tokio_util::codec;
use tokio_util::sync::CancellationToken;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[derive(Serialize, Deserialize, Debug)]
struct ExampleMessage {
message_id: i8,
message_bytes: Vec<u8>,
}
// This is a basic example which opens a single TCP connection and writes a bunch of messages between a client and an echo
// server, so only uses a single session under the hood and doesn't really show off the message ordering capabilities; this is mainly
// just a quick introductory illustration on how:
// - the mixnet does message ordering
// - the NymProxyClient and NymProxyServer can be hooked into and used to communicate between two otherwise pretty vanilla TcpStreams
//
// For a more irl example checkout tcp_proxy_multistream.rs
//
// Run this with:
// `cargo run --example tcp_proxy_single_connection <SERVER_LISTEN_PORT> <ENV_FILE_PATH> <CLIENT_LISTEN_PATH>` e.g.
// `cargo run --example tcp_proxy_single_connection 8081 ../../../envs/canary.env 8080 `
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Keep track of sent/received messages
let counter = AtomicU8::new(0);
// Comment this out to just see println! statements from this example, as Nym client logging is very informative but quite verbose.
// The Message Decay related logging gives you an ideas of the internals of the proxy message ordering. To see the contents of the msg buffer, sphinx packet chunking, etc change the tracing::Level to DEBUG.
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::new("nym_sdk::tcp_proxy=info"))
.init();
let server_port = env::args()
.nth(1)
.expect("Server listen port not specified");
let upstream_tcp_addr = format!("127.0.0.1:{}", server_port);
// This dir gets cleaned up at the end: NOTE if you switch env between tests without letting the file do the automatic cleanup, make sure to manually remove this directory up before running again, otherwise your client will attempt to use these keys for the new env
let home_dir = dirs::home_dir().expect("Unable to get home directory");
let conf_path = format!("{}/tmp/nym-proxy-server-config", home_dir.display());
let env_path = env::args().nth(2).expect("Env file not specified");
let env = env_path.to_string();
let client_port = env::args().nth(3).expect("Port not specified");
let mut proxy_server =
tcp_proxy::NymProxyServer::new(&upstream_tcp_addr, &conf_path, Some(env_path.clone()))
.await?;
let proxy_nym_addr = proxy_server.nym_address();
// We'll run the instance with a long timeout since we're sending everything down the same Tcp connection, so should be using a single session.
// Within the TcpProxyClient, individual client shutdown is triggered by the timeout.
// The final argument is how many clients to keep in reserve in the client pool when running the TcpProxy.
let proxy_client =
tcp_proxy::NymProxyClient::new(*proxy_nym_addr, "127.0.0.1", &client_port, 5, Some(env), 1)
.await?;
// For our disconnect() logic below
let proxy_clone = proxy_client.clone();
tokio::spawn(async move {
proxy_server.run_with_shutdown().await?;
Ok::<(), anyhow::Error>(())
});
tokio::spawn(async move {
proxy_client.run().await?;
Ok::<(), anyhow::Error>(())
});
let example_cancel_token = CancellationToken::new();
let server_cancel_token = example_cancel_token.clone();
let client_cancel_token = example_cancel_token.clone();
let watcher_cancel_token = example_cancel_token.clone();
// Cancel listener thread
tokio::spawn(async move {
signal::ctrl_c().await?;
println!(":: CTRL_C received, shutting down + cleanup up proxy server config files");
fs::remove_dir_all(conf_path)?;
watcher_cancel_token.cancel();
proxy_clone.disconnect().await;
Ok::<(), anyhow::Error>(())
});
// 'Server side' thread: echo back incoming as response to the messages sent in the 'client side' thread below
tokio::spawn(async move {
let listener = TcpListener::bind(upstream_tcp_addr).await?;
loop {
if server_cancel_token.is_cancelled() {
break;
}
let (socket, _) = listener.accept().await.unwrap();
let (read, mut write) = socket.into_split();
let codec = codec::BytesCodec::new();
let mut framed_read = codec::FramedRead::new(read, codec);
while let Some(Ok(bytes)) = framed_read.next().await {
match bincode::deserialize::<ExampleMessage>(&bytes) {
Ok(msg) => {
println!(
"<< server received {}: {} bytes",
msg.message_id,
msg.message_bytes.len()
);
let msg = ExampleMessage {
message_id: msg.message_id,
message_bytes: msg.message_bytes,
};
let serialised = bincode::serialize(&msg)?;
write
.write_all(&serialised)
.await
.expect("couldnt send reply");
println!(
">> server sent {}: {} bytes",
msg.message_id,
msg.message_bytes.len()
);
}
Err(e) => {
println!("<< server received something that wasn't an example message of {} bytes. error: {}", bytes.len(), e);
}
}
}
}
#[allow(unreachable_code)]
Ok::<(), anyhow::Error>(())
});
// Just wait for Nym clients to connect, TCP clients to bind, etc. If there isn't a client in the pool (or you started it with 0) already then the TcpProxyClient just spins up an ephemeral client itself.
println!("waiting for everything to be set up..");
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
println!("done. sending bytes");
// Now the client and server proxies are running we can create and pipe traffic to/from
// a socket on the same port as our ProxyClient instance as if we were just communicating
// between a client and host via a normal TcpStream - albeit with a decent amount of additional latency.
//
// The assumption regarding integration is that you know what you're sending, and will do proper
// framing before and after, know what data types you're expecting, etc; the proxies are just piping bytes
// back and forth using tokio's `Bytecodec` under the hood.
let local_tcp_addr = format!("127.0.0.1:{}", client_port);
let stream = TcpStream::connect(local_tcp_addr).await?;
let (read, mut write) = stream.into_split();
// 'Client side' thread; lets just send a bunch of messages to the server with variable delays between them, with an id to keep track of ordering in the printlns; the mixnet only guarantees message delivery, not ordering. You might not be necessarily streaming traffic in this manner IRL, but this example is a good illustration of how messages travel through the mixnet.
// - On the level of individual messages broken into multiple packets, the Proxy abstraction deals with making sure that everything is sent between the sockets in the corrent order.
// - On the level of different messages, this is not enforced: you might see in the logs that message 1 arrives at the server and is reconstructed after message 2.
tokio::spawn(async move {
let mut rng = SmallRng::from_entropy();
for i in 0..10 {
if client_cancel_token.is_cancelled() {
break;
}
let random_bytes = gen_bytes_fixed(i as usize);
let msg = ExampleMessage {
message_id: i,
message_bytes: random_bytes,
};
let serialised = bincode::serialize(&msg)?;
write
.write_all(&serialised)
.await
.expect("couldn't write to stream");
println!(">> client sent {}: {} bytes", &i, msg.message_bytes.len());
let delay = rng.gen_range(3.0..7.0);
tokio::time::sleep(tokio::time::Duration::from_secs_f64(delay)).await;
}
Ok::<(), anyhow::Error>(())
});
let codec = codec::BytesCodec::new();
let mut framed_read = codec::FramedRead::new(read, codec);
while let Some(Ok(bytes)) = framed_read.next().await {
match bincode::deserialize::<ExampleMessage>(&bytes) {
Ok(msg) => {
println!(
"<< client received {}: {} bytes",
msg.message_id,
msg.message_bytes.len()
);
counter.fetch_add(1, Ordering::SeqCst);
println!(
":: messages received back: {:?}/10",
counter.load(Ordering::SeqCst)
);
}
Err(e) => {
println!("<< client received something that wasn't an example message of {} bytes. error: {}", bytes.len(), e);
}
}
}
Ok(())
}
fn gen_bytes_fixed(i: usize) -> Vec<u8> {
// let amounts = vec![1, 10, 50, 100, 150, 200, 350, 500, 750, 1000];
let amounts = [158, 1088, 505, 1001, 150, 200, 3500, 500, 750, 100];
let len = amounts[i];
let mut rng = rand::thread_rng();
(0..len).map(|_| rng.gen::<u8>()).collect()
}
```
@@ -1,5 +0,0 @@
# Troubleshooting
import { Callout } from 'nextra/components'
## Lots of `duplicate fragment received` messages
You might see a lot of `WARN` level logs about duplicate fragments in your logs, depending on the log level you're using. This occurs when a packet is retransmitted somewhere in the Mixnet, but then the original makes it to the destination client as well. This is not something to do with your client logic, but instead the state of the Mixnet.
@@ -0,0 +1,164 @@
# Tour of the Rust SDK
import { Callout } from 'nextra/components'
A quick walkthrough of the most important things you can do with `nym-sdk`. Each section shows working code and links to the module that covers it in depth.
<Callout type="warning">
**The Mixnet is not like regular internet networking.** There are no persistent connections, no guaranteed message ordering, and no TCP underneath. At its core, the Mixnet is a message-based anonymity network: you send individual payloads that are Sphinx-encrypted, mixed through multiple nodes, and independently reconstructed at the destination.
The raw [message API](./mixnet) therefore works differently from what most developers expect. The [Stream module](./stream) bridges this gap by providing `AsyncRead + AsyncWrite` byte streams on top of the Mixnet. If you are coming from socket-based networking, start with streams.
</Callout>
## Send a raw message payload
The message API gives you direct access to the Mixnet's native communication model: individually addressed payloads with no connections and no ordering guarantees. This is useful when you want full control, but it's not how most networking code works:
```rust
use nym_sdk::mixnet::{self, MixnetMessageSender};
#[tokio::main]
async fn main() {
let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
let addr = *client.nym_address();
println!("Connected: {addr}");
// Send a message to ourselves
client
.send_plain_message(addr, "hello mixnet!")
.await
.unwrap();
// Receive it (filter empty SURB management messages)
if let Some(msgs) = client.wait_for_messages().await {
for msg in msgs.iter().filter(|m| !m.message.is_empty()) {
println!("Got: {}", String::from_utf8_lossy(&msg.message));
}
}
// Always disconnect for clean shutdown
client.disconnect().await;
}
```
The message is Sphinx-encrypted, mixed across 5 nodes, and reconstructed on arrival. The whole round trip takes a few seconds.
Next: [Mixnet module](./mixnet) | [Tutorial: Send Your First Private Message](./mixnet/tutorial)
## Reply anonymously with SURBs
Every received message carries a `sender_tag`, an opaque token that lets you reply **without knowing the sender's Nym address**. Replies travel back through pre-built Single Use Reply Blocks (SURBs):
```rust
// After receiving a message...
let tag = received_msg.sender_tag.expect("message includes sender tag");
client.send_reply(tag, "anonymous reply!").await.unwrap();
```
The replying side never learns where the reply is going, enabling anonymous communication without mutual identity disclosure.
## Open a bidirectional stream
If you're used to working with TCP sockets, this is where you'll feel at home. The [Stream module](./stream) provides persistent, bidirectional byte channels that implement tokio's `AsyncRead + AsyncWrite`, so any code that works with sockets works with `MixnetStream`:
```rust
use nym_sdk::mixnet;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() {
let mut sender = mixnet::MixnetClient::connect_new().await.unwrap();
let mut receiver = mixnet::MixnetClient::connect_new().await.unwrap();
let recv_addr = *receiver.nym_address();
// Receiver creates a listener (activates stream mode)
let mut listener = receiver.listener().unwrap();
// Sender opens a stream to the receiver
let mut out = sender.open_stream(recv_addr, None).await.unwrap();
// Receiver accepts it
let mut inc = listener.accept().await.unwrap();
// Standard tokio I/O — write, flush, read
out.write_all(b"hello stream").await.unwrap();
out.flush().await.unwrap();
let mut buf = vec![0u8; 1024];
let n = inc.read(&mut buf).await.unwrap();
println!("{}", String::from_utf8_lossy(&buf[..n]));
drop(out);
drop(inc);
sender.disconnect().await;
receiver.disconnect().await;
}
```
<Callout type="info">
Activating stream mode (by calling `listener()` or `open_stream()`) disables message-based methods like `send_plain_message()` and `wait_for_messages()`. A single client operates in one mode at a time.
</Callout>
Next: [Stream module](./stream) | [Tutorial: Build a Private Echo Server](./stream/tutorial)
## Use a client pool for bursty traffic
Creating a `MixnetClient` takes several seconds (gateway handshake, key generation, topology fetch). The [Client Pool](./client-pool) pre-creates clients in the background so they're ready when you need them:
```rust
use nym_sdk::client_pool::ClientPool;
#[tokio::main]
async fn main() {
let pool = ClientPool::new(3); // maintain 3 clients in reserve
let bg = pool.clone();
tokio::spawn(async move { bg.start().await });
// Wait for pool to fill, then grab a ready client
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
if let Some(client) = pool.get_mixnet_client().await {
println!("Got client: {}", client.nym_address());
client.disconnect().await;
}
pool.disconnect_pool().await;
}
```
Clients are consumed, not returned; the pool creates replacements automatically.
Next: [Client Pool module](./client-pool) | [Tutorial: Handle Bursty Traffic](./client-pool/tutorial)
## Persist your identity
By default, `connect_new()` creates ephemeral keys that are discarded on disconnect. To keep the same Nym address across restarts, use the builder with on-disk storage:
```rust
use nym_sdk::mixnet::{MixnetClientBuilder, StoragePaths};
use std::path::PathBuf;
let storage = StoragePaths::new_from_dir(
&PathBuf::from("/tmp/my-nym-client")
).unwrap();
let client = MixnetClientBuilder::new_with_default_storage(storage)
.await
.unwrap()
.build()
.unwrap()
.connect_to_mixnet()
.await
.unwrap();
// This address is the same every time you run with the same path
println!("Persistent address: {}", client.nym_address());
```
## Where to go next
- [Installation](./importing): add `nym-sdk` to your project
- [Mixnet Tutorial](./mixnet/tutorial): send, receive, and reply with SURBs
- [Stream Tutorial](./stream/tutorial): build a private echo server
- [Client Pool Tutorial](./client-pool/tutorial): handle bursty traffic
- [API Reference on docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/): type details, method signatures, architecture docs
@@ -1,6 +1,6 @@
---
title: "Nym Developer Tools: CLI, Echo & TcpProxy"
description: "Overview of Nym developer tools including nym-cli for blockchain interaction, echo server for traffic testing, and standalone TcpProxy binary downloads."
title: "Nym Developer Tools: CLI, Diagnostics & TcpProxy"
description: "Overview of Nym developer tools including nym-cli for blockchain interaction, diagnostic tool for troubleshooting, and standalone TcpProxy binary downloads."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-02-01"
@@ -8,8 +8,10 @@ lastUpdated: "2026-02-01"
# Tools
There are a few tools available to developers for chain interaction: the `nym-cli` tool, which operates as an easier-to-use wrapper around `nyxd`, to allow operators to script interactions with their infrastructure (and those who prefer CLI tools).
Standalone binaries for development and testing. These don't require an SDK; download or compile them and use them directly.
There is also a basic echo server tool which app developers can use as a quick endpoint for traffic testing. This will be deployed onto a persistent public server in the future so devs dont have to run it themselves.
Finally, there are also a pair of standalone versions of the TcpProxy Rust SDK module for developers to begin experimenting with sending app traffic through them mixnet (**this module will soon be deprecated in place of the `MixSocket`/`MixStream` abstractions**).
| Tool | Use case |
|---|---|
| [nym-cli](./tools/nym-cli) | Command-line interface for interacting with the Nyx blockchain: querying state, submitting transactions, managing keys. An easier-to-use wrapper around `nyxd`. |
| [Diagnostic Tool](./tools/diagnostic-tool) | Network diagnostic utility for troubleshooting connectivity issues. |
| [Standalone TcpProxy](./tools/standalone-tcpproxy) | Pre-built binaries of the TcpProxy client and server for proxying TCP traffic through the Mixnet. Note: the TcpProxy module is unmaintained; use the [Stream module](./rust/stream) for new projects. |
@@ -1,6 +1,5 @@
{
"nym-cli": "Nym-cli",
"diagnostic-tool": "Diagnostic Tool",
"echo-server": "Echo Server",
"standalone-tcpproxy": "TcpProxy Binaries (Standalone)"
}
@@ -1,29 +0,0 @@
# Echo Server
There is an initial version of a simple echo server located at [`nym/tools/echo-server`](https://github.com/nymtech/nym/tree/develop/tools/echo-server).
This is an initial minimal implementation of an echo server built using the [`NymProxyServer`](../rust/tcpproxy) Rust SDK abstraction that, aside from the initialisation and running of a `NymProxyServer` instance in the background, is essentially a vanilla TCP echo server written with `tokio`.
This server was initially built for the `TcpProxy` tests, but can be useful for developers to need a constant endpoint to ping when developing. In the future this will be deployed to a remote server so developers don't have to run their own.
## Build
Run `cargo build --release` from `nym/tools/echo-server`. The binary will be in the main workspace `target/release` dir.
## Run
```sh
Usage: echo-server [OPTIONS]
Options:
-g, --gateway <GATEWAY> Optional gateway to use
-c, --config-path <CONFIG_PATH> Optional config path to specify
-e, --env <ENV> Optional env file - defaults to Mainnet if None
-l, --listen-port <LISTEN_PORT> Listen port [default: 8080]
-h, --help Print help
```
## Logging
Every 10 seconds, the server logs:
- the total number of bytes received since startup
- the total number of bytes sent since startup
- the current number of concurrent connections it has
- the total number of concurrent connections it has
@@ -10,15 +10,15 @@ See the [commands](commands.mdx) page for an overview of all command options.
There is a limitation the staking address can only perform the following actions (and are visible via the Nym Wallet:
- Bond on the gateway's or mix node's behalf.
- Delegate or Un-delegate (to a mix node in order to begin receiving rewards)
- Bond on the gateway's or Mix Node's behalf.
- Delegate or Un-delegate (to a Mix Node in order to begin receiving rewards)
- Claiming the rewards on the account
```admonish note title=""
The staking address has no ability to withdraw any coins from the parent's account.
```
The staking address must maintain the same level of security as the parent mnemonic; while the parent mnemonic's delegations and bonding events will be visible to the parent owner, the staking address will be the only account capable of undoing the bonding and delegating from the mix nodes or gateway.
The staking address must maintain the same level of security as the parent mnemonic; while the parent mnemonic's delegations and bonding events will be visible to the parent owner, the staking address will be the only account capable of undoing the bonding and delegating from the Mix Nodes or gateway.
Query for staking on behalf of someone else
```
@@ -1,5 +1,11 @@
# Standalone TcpProxy Binaries
import { Callout } from 'nextra/components'
<Callout type="error">
**Deprecated.** The TcpProxy module is no longer actively developed. The [Stream module](/developers/rust/stream) provides the same functionality (familiar `AsyncRead`/`AsyncWrite` I/O over the Mixnet) with a simpler API, multiplexed connections, and sequence-based message reordering. Use Streams for new projects.
</Callout>
Standalone versions of the `TcpProxyClient` and `TcpProxyServer` [sdk module](../rust/tcpproxy) can be found [here](https://github.com/nymtech/standalone-tcp-proxies/tree/main).
These might be an easy way for developers to start proxying their traffic throught the mixnet and understanding the sort of latency they should expect, and whether their application can currently tolerate it. They might also prove useful for server setups where several components are being run via init scripts, and the addition of a separate process is acceptable.
@@ -3,9 +3,185 @@ title: "Nym TypeScript SDK: Privacy for Web Apps"
description: "TypeScript SDK for integrating web apps with the Nym mixnet. Covers mixFetch, Mixnet Client, Smart Contracts, and Cosmos Kit with live playground examples."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-02-11"
lastUpdated: "2026-03-13"
---
# Introduction
import { Callout } from 'nextra/components'
import { TableContainer, Table, TableBody, TableCell, TableRow, Paper } from '@mui/material'
import { NPMLink } from '../../components/npm';
This guide contains information about the various TypeScript SDK modules that facilitate interaction with different components of the Nym stack: the Nym mixnet & the Nyx blockchain.
# TypeScript SDK
The TypeScript SDK lets you build browser-based applications that communicate through the Nym mixnet. Import SDK packages via NPM as you would any other TypeScript library.
<Callout type="warning">
The Nym Mixnet routes traffic through multiple nodes with no persistent connections or guaranteed ordering. The SDK abstracts the complexity, but understanding the [underlying model](/developers/rust/tour) helps when debugging.
</Callout>
## Packages
<TableContainer component={Paper}>
<Table>
<TableBody>
<TableRow>
<TableCell>
**mixFetch**
</TableCell>
<TableCell>
A drop-in replacement for [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)
that sends HTTP requests over the Nym mixnet
</TableCell>
<TableCell>
<NPMLink packageName={'@nymproject/mix-fetch'} kind={'esm'}/><br/>
<NPMLink packageName={'@nymproject/mix-fetch-full-fat'} kind={'esm'} preBundled/><br/>
<NPMLink packageName={'@nymproject/mix-fetch-commonjs'} kind={'cjs'}/><br/>
<NPMLink packageName={'@nymproject/mix-fetch-full-fat-commonjs'} kind={'cjs'} preBundled/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
**Mixnet Client**
</TableCell>
<TableCell>
Send and receive text and binary messages over the Nym mixnet
</TableCell>
<TableCell>
<NPMLink packageName={'@nymproject/sdk'} kind={'esm'}/><br/>
<NPMLink packageName={'@nymproject/sdk-full-fat'} kind={'esm'} preBundled/><br/>
<NPMLink packageName={'@nymproject/sdk-commonjs'} kind={'cjs'}/><br/>
<NPMLink packageName={'@nymproject/sdk-full-fat-commonjs'} kind={'cjs'} preBundled/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
**Nym Smart Contracts**
</TableCell>
<TableCell>
Query and execute methods on the smart contracts that run the Nym mixnet
</TableCell>
<TableCell>
<NPMLink packageName={'@nymproject/contract-clients'} kind={'esm'}/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
### Which variant should I use?
All packages (except Contract Clients) come in four variants:
- **ESM:** For new projects with current tooling. You may need to [configure your bundler](./typescript/bundling) to handle WASM and web worker components.
- **ESM full-fat:** Pre-bundled with inline WASM and web workers. No bundler config needed.
- **CommonJS:** For older projects using CommonJS. WASM and web workers need to be [bundled](./typescript/bundling/webpack).
- **CommonJS full-fat:** Pre-bundled, works without additional configuration.
<Callout type="warning">
All `*-full-fat` variants have large bundle sizes because they include WASM and web workers as inline Base64 strings. Use the standard ESM variant if bundle size matters.
</Callout>
## Installation
### mixFetch
```bash
npm install @nymproject/mix-fetch-full-fat
```
### Mixnet Client
```bash
npm install @nymproject/sdk-full-fat
```
### Nym Smart Contracts
```bash
npm install @nymproject/contract-clients @cosmjs/cosmwasm-stargate @cosmjs/proto-signing
```
### Install everything
```bash
npm install @nymproject/contract-clients @cosmjs/cosmwasm-stargate @cosmjs/proto-signing @nymproject/sdk-full-fat @nymproject/mix-fetch-full-fat
```
## Quick start
### mixFetch
Use [`mixFetch`](https://www.npmjs.com/package/@nymproject/mix-fetch) as a drop-in replacement for `fetch` to send HTTP requests over the mixnet:
```ts
import { mixFetch } from '@nymproject/mix-fetch';
// HTTP GET
const response = await mixFetch('https://nym.com');
const html = await response.text();
// HTTP POST
const apiResponse = await mixFetch('https://api.example.com', {
method: 'POST',
body: JSON.stringify({ foo: 'bar' }),
headers: { 'Content-Type': 'application/json' }
});
```
### Mixnet Client
Create a [`Mixnet Client`](https://www.npmjs.com/package/@nymproject/sdk) to send and receive messages through the mixnet:
```js
import { createNymMixnetClient } from '@nymproject/sdk';
const nym = await createNymMixnetClient();
const nymApiUrl = 'https://validator.nymtech.net/api';
// Subscribe to incoming messages
nym.events.subscribeToTextMessageReceivedEvent((e) => {
console.log('Got a message: ', e.args.payload);
});
// Connect to the mixnet
await nym.client.start({ clientId: 'my-app', nymApiUrl });
// Send a message to yourself
const recipient = nym.client.selfAddress();
nym.client.send({ payload: 'Hello mixnet', recipient });
```
### Nym Smart Contracts
Use the [Contract Clients](https://www.npmjs.com/package/@nymproject/contract-clients) to query or execute on Nym smart contracts:
```js
import { contracts } from '@nymproject/contract-clients';
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
const signer = await DirectSecp256k1HdWallet.fromMnemonic("...");
const accounts = await signer.getAccounts();
const cosmWasmSigningClient = await SigningCosmWasmClient.connectWithSigner(
"https://rpc.nymtech.net:443", signer
);
const client = new contracts.Mixnet.MixnetClient(
cosmWasmSigningClient,
accounts[0].address,
'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr'
);
// Delegate 1 NYM to mixnode with id 100
const result = await client.delegateToMixnode(
{ mixId: 100 }, 'auto', undefined,
[{ amount: `${1_000_000}`, denom: 'unym' }]
);
console.log(`Tx Hash = ${result.transactionHash}`);
```
## Next steps
- **[Step-by-step examples](./typescript/examples):** Full working projects for each package
- **[Live playground](./typescript/playground):** Try the SDK in your browser
- **[Bundling](./typescript/bundling):** Configure Webpack or ESBuild for WASM and web workers
- **[TypeDoc reference](./typescript/api):** generated reference for all packages
@@ -1,23 +0,0 @@
# TS SDK FAQ
import { Callout } from 'nextra/components'
## Why and when does the mixnet client complain about insufficient topology?
It will in one of the following cases:
- There are empty mix layers - although this is rare;
- The gateway you've registered with does not appear in the network topology -> it is either unbonded or was blacklisted;
- The gateway you want to send packets to does not appear in the network topology -> it is either unbonded or was blacklisted;
To avoid the last two, you need to make sure the gateway you are calling is bonded and whitelisted.
## How can I check whether the gateway I am connecting to is bonded and not blacklisted?
The easiest way of checking what gateway you're registered with is to look at your client address.
Client addresses are in the format of:
`client-id . client-dh @ gateway-id. `
To illustrate this: `DpB3cHAchJiNBQi5FrZx2csXb1mrHkpYh9Wzf8Rjsuko.ANNWrvHqMYuertHGHUrZdBntQhpzfbWekB39qez9U2Vx@2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh `
- `DpB3cHAchJiNBQi5FrZx2csXb1mrHkpYh9Wzf8Rjsuko`: is the client's identity key;
- `ANNWrvHqMYuertHGHUrZdBntQhpzfbWekB39qez9U2Vx`: is the client's Diffie Hellman key;
- `2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh`: is the gateway's identity, which is what you'll need to check the state of the gateway in the [Nym Explorer](https://nym.com/explorer).
@@ -1,9 +1,6 @@
{
"overview": "SDK overview",
"installation": "Installation",
"start": "Getting started",
"examples": "Step-by-step examples",
"examples": "Step-by-step Examples",
"playground": "Live Playground",
"bundling": "Bundling",
"FAQ": "FAQ"
"bundling": "Bundling & Troubleshooting",
"api": "TypeDoc Reference"
}

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