diff --git a/.gitignore b/.gitignore index ede0a9f7b2..fc762c45d9 100644 --- a/.gitignore +++ b/.gitignore @@ -83,5 +83,6 @@ test-tutorials/ # pnpm .pnpm-store/ +tmp/ # operator tools -scripts/nym-node-setup/auto-bond/nodes.csv \ No newline at end of file +scripts/nym-node-setup/auto-bond/nodes.csv diff --git a/documentation/docs/components/landing-page.tsx b/documentation/docs/components/landing-page.tsx index c1013c6618..4f2d4437ad 100644 --- a/documentation/docs/components/landing-page.tsx +++ b/documentation/docs/components/landing-page.tsx @@ -310,6 +310,24 @@ const sdks = [ "fetch()-compatible API that routes HTTP(S) requests through the Mixnet. Browsers and Node.js.", href: "/developers/mix-fetch", }, + { + name: "mix-tunnel", + description: + "Owns the shared Mixnet tunnel that mix-fetch, mix-dns, and mix-websocket ride on. One IPR connection and userspace TCP/IP stack for all three.", + href: "/developers/mix-tunnel", + }, + { + name: "mix-dns", + description: + "Resolves hostnames to IPs through the Mixnet. UDP DNS via an IPR exit, no TCP or TLS.", + href: "/developers/mix-dns", + }, + { + name: "mix-websocket", + description: + "WebSocket-compatible class for ws and wss traffic routed through the Mixnet via an IPR exit.", + href: "/developers/mix-websocket", + }, ]; export const LandingPage = () => { diff --git a/documentation/docs/components/mix-fetch.tsx b/documentation/docs/components/mix-fetch.tsx deleted file mode 100644 index 0fb6298e37..0000000000 --- a/documentation/docs/components/mix-fetch.tsx +++ /dev/null @@ -1,282 +0,0 @@ -import React, { useState, useRef, useEffect } from "react"; -import CircularProgress from "@mui/material/CircularProgress"; -import Button from "@mui/material/Button"; -import TextField from "@mui/material/TextField"; -import Typography from "@mui/material/Typography"; -import Box from "@mui/material/Box"; -import { mixFetch, createMixFetch } from "@nymproject/mix-fetch-full-fat"; -import Stack from "@mui/material/Stack"; -import Paper from "@mui/material/Paper"; -import type { SetupMixFetchOps } from "@nymproject/mix-fetch-full-fat"; - -const defaultUrl = - "https://nymtech.net/.wellknown/network-requester/exit-policy.txt"; -const args = { mode: "unsafe-ignore-cors" }; -const mixFetchOptions: SetupMixFetchOps = { - clientId: "docs-mixfetch-demo", // explicit ID - preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1", - mixFetchOverride: { - requestTimeoutMs: 60_000, - }, - forceTls: true, // force WSS -}; - -// Log entry type for the visible log panel -type LogLevel = "info" | "error" | "send" | "receive"; -type LogEntry = { timestamp: string; message: string; level: LogLevel }; - -// Color map for log levels -const logColors: Record = { - info: "gray", - error: "red", - send: "blue", - receive: "green", -}; - -// Label map for log levels -const logLabels: Record = { - info: "INFO", - error: "ERROR", - send: "SEND", - receive: "RECV", -}; - -export const MixFetch = () => { - // MixFetch initialization state - const [status, setStatus] = useState<"idle" | "starting" | "ready" | "error">( - "idle" - ); - const [errorMsg, setErrorMsg] = useState(null); - - // Log panel state - const [logs, setLogs] = useState([]); - - // Single fetch state - const [url, setUrl] = useState(defaultUrl); - const [html, setHtml] = useState(); - const [busy, setBusy] = useState(false); - - // Concurrent fetch state - const [concurrentResults, setConcurrentResults] = useState([]); - const [concurrentBusy, setConcurrentBusy] = useState(false); - - // Auto-scroll within the log panel when new entries are added (without scrolling the page) - const logContainerRef = useRef(null); - useEffect(() => { - if (logContainerRef.current) { - logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; - } - }, [logs]); - - // Helper to add a timestamped log entry - const addLog = (message: string, level: LogLevel) => { - const timestamp = new Date().toISOString().substring(11, 23); // HH:MM:SS.mmm - setLogs((prev) => [...prev, { timestamp, message, level }]); - }; - - // Initialize MixFetch explicitly via createMixFetch - const handleStart = async () => { - try { - setStatus("starting"); - setErrorMsg(null); - addLog("Starting MixFetch...", "info"); - await createMixFetch(mixFetchOptions); - setStatus("ready"); - addLog("MixFetch is ready!", "info"); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - setStatus("error"); - setErrorMsg(msg); - addLog(`Error: ${msg}`, "error"); - } - }; - - // Single URL fetch — mixFetch reuses the existing singleton - const handleFetch = async () => { - try { - setBusy(true); - setHtml(undefined); - addLog(`Sending request to ${url}...`, "send"); - const response = await mixFetch(url, args, mixFetchOptions); - const resHtml = await response.text(); - setHtml(resHtml); - addLog(`Response received (${resHtml.length} bytes)`, "receive"); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - addLog(`Fetch error: ${msg}`, "error"); - } finally { - setBusy(false); - } - }; - - // Send 5 concurrent requests to different URLs on the same domain - const handleConcurrentFetch = async () => { - const baseUrl = "https://jsonplaceholder.typicode.com/posts/"; - const count = 5; - try { - setConcurrentBusy(true); - setConcurrentResults([]); - addLog( - `Starting ${count} concurrent requests to ${baseUrl}1-${count}...`, - "send" - ); - // Fire off all requests concurrently using Promise.all - const requests = Array.from({ length: count }, (_, i) => { - const targetUrl = `${baseUrl}${i + 1}`; - return mixFetch(targetUrl, args, mixFetchOptions) - .then((res) => res.json()) - .then((json: { id: number; title: string }) => { - const entry = `[${json.id}] ${json.title}`; - addLog(entry, "receive"); - return entry; - }); - }); - const results = await Promise.all(requests); - setConcurrentResults(results); - addLog(`All ${count} concurrent requests completed!`, "info"); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - addLog(`Concurrent fetch error: ${msg}`, "error"); - } finally { - setConcurrentBusy(false); - } - }; - - // Are fetch controls enabled? - const isReady = status === "ready"; - - // Status text + color for the startup indicator - const statusText: Record = { - idle: "Not started", - starting: "Starting...", - ready: "Ready", - error: `Error: ${errorMsg}`, - }; - const statusColor: Record = { - idle: "#9e9e9e", - starting: "orange", - ready: "#85E89D", - error: "#ff6b6b", - }; - - return ( - - - - {/* --- Start MixFetch Section --- */} - - - {status === "starting" && } - - {statusText[status]} - - - - {/* --- Fetch Controls (disabled until ready) --- */} - - {/* Single fetch */} - - setUrl(e.target.value)} - size="small" - /> - - - {busy && ( - - - - )} - {html && ( - <> - - Response - - - - {html} - - - - )} - - {/* Concurrent fetch demo */} - - Concurrent Requests - - - - - {concurrentBusy && ( - - - - )} - {concurrentResults.length > 0 && ( - - {concurrentResults.map((result, i) => ( - - {result} - - ))} - - )} - - - - - {/* --- Log Panel --- */} - {logs.length > 0 && ( - - Log - {logs.map((entry, i) => ( - - {entry.timestamp} [{logLabels[entry.level]}] {entry.message} - - ))} - - )} - - ); -}; diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json b/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json index 90066ee346..c92e17125c 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/nodes-count.json @@ -1,6 +1,6 @@ { - "nodes": 652, + "nodes": 659, "locations": 75, - "mixnodes": 239, - "exit_gateways": 405 + "mixnodes": 238, + "exit_gateways": 413 } diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index 7a7ea7775d..1e8cf87112 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Monday, June 8th 2026, 11:52:06 UTC +Tuesday, June 9th 2026, 08:23:52 UTC diff --git a/documentation/docs/components/playground/MixPlayground.tsx b/documentation/docs/components/playground/MixPlayground.tsx new file mode 100644 index 0000000000..8bae9061e9 --- /dev/null +++ b/documentation/docs/components/playground/MixPlayground.tsx @@ -0,0 +1,749 @@ +// Single interactive playground for the mix-* TypeScript SDK, modelled on +// wasm/smolmix/internal-dev but driving the published @nymproject/mix-* packages +// and trimmed/adapted for a docs audience. One shared tunnel, several sections +// (DNS, GET, WebSocket, stress, download), each with a verbose timeline log and +// and, where it teaches something, a tunnel-vs-clearnet comparison. +// +// The package import is dynamic (see ./lib.ts loadModules) so the multi-MB wasm +// loads only when the visitor clicks Setup, not on page render. + +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { + loadModules, + randomClientId, + clampSurbs, + formatSize, + formatRate, + hexPreview, + sha256hex, + saveFile, + dohResolve, + generateRequests, + type PlaygroundMods, + type MixWebSocketLike, + type SetupOpts, +} from './lib'; +import { + useLogs, + LogPanel, + StatusText, + Spinner, + Button, + box, + row, + input, + num, + legend, + sub, + type Status, +} from './ui'; + +const VERIFY_TEXT_URL = 'https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt'; + +// Default IPR exit for the playground. Users can switch to auto-discovery +// with the "Use random IPR" toggle. +const DEFAULT_IPR = + '6B6iuWX4bQP4GVA4Yq7XmZencaaGw6BaPY6xJWYSwsbF.6g6LRx1fgU2Q2A4ZPKonYHtfBARh1GPMe1LtXk6vpRR8@q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1'; + +function eqBytes(a: Uint8Array, b: Uint8Array): boolean { + if (a.byteLength !== b.byteLength) return false; + for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false; + return true; +} + +export function MixPlayground() { + const { log, lines } = useLogs(); + const [mods, setMods] = useState(null); + const [connected, setConnected] = useState(false); + const [busy, setBusy] = useState(false); // setup/disconnect in flight + const [tunnelStatus, setTunnelStatus] = useState({ text: 'Not started', colour: 'gray' }); + + // Connection form. + const [useRandomIpr, setUseRandomIpr] = useState(false); + const [iprAddress, setIprAddress] = useState(DEFAULT_IPR); + const [clientId, setClientId] = useState(''); + const [forceTls, setForceTls] = useState(true); + const [disablePoisson, setDisablePoisson] = useState(false); + const [disableCover, setDisableCover] = useState(false); + const [openSurbs, setOpenSurbs] = useState(10); // matches SurbsConfig::default (ipr.rs) + const [dataSurbs, setDataSurbs] = useState(2); // matches SurbsConfig::default (ipr.rs) + const [primaryDns, setPrimaryDns] = useState(''); + const [fallbackDns, setFallbackDns] = useState(''); + const [debug, setDebug] = useState(true); + + // Section inputs. + const [dnsHost, setDnsHost] = useState('example.com'); + const [getUrl, setGetUrl] = useState('https://httpbin.org/get'); + const [wsUrl, setWsUrl] = useState('wss://echo.websocket.org'); + const [wsMessage, setWsMessage] = useState('Hello from the mixnet!'); + const [wsStatus, setWsStatus] = useState({ text: 'Not connected', colour: 'gray' }); + const [wsConnected, setWsConnected] = useState(false); + const [burstCount, setBurstCount] = useState(10); + const [burstMin, setBurstMin] = useState(64); + const [burstMax, setBurstMax] = useState(1024); + const [burstBusy, setBurstBusy] = useState(false); + const [stressCount, setStressCount] = useState(10); + const [stressMode, setStressMode] = useState<'uniform' | 'mixed' | 'drip'>('mixed'); + const [stressUrl, setStressUrl] = useState('https://jsonplaceholder.typicode.com/posts/'); + const [stressTimeout, setStressTimeout] = useState(60); + const [stressBusy, setStressBusy] = useState(false); + const [stressStatus, setStressStatus] = useState({ text: '' }); + const [downloadUrl, setDownloadUrl] = useState( + 'https://web.cs.ucdavis.edu/~rogaway/papers/moral-fn.pdf', + ); + const [textBusy, setTextBusy] = useState(false); + const [textStatus, setTextStatus] = useState({ text: '' }); + const [textOutput, setTextOutput] = useState(null); + const [pdfBusy, setPdfBusy] = useState(false); + const [pdfStatus, setPdfStatus] = useState({ text: '' }); + const [pdfInfo, setPdfInfo] = useState<{ size: number; hash: string } | null>(null); + const [filePreview, setFilePreview] = useState<{ url: string; isImage: boolean } | null>(null); + const [bothStatus, setBothStatus] = useState({ text: '' }); + + const wsRef = useRef(null); + const wsSendQueue = useRef([]); + const burstRef = useRef<{ + payloads: Uint8Array[]; + received: number; + verified: number; + mismatches: number; + rtts: number[]; + expected: number; + resolve: () => void; + } | null>(null); + const cachedPdf = useRef(null); + + // Generate the client id after mount (not at render) to keep SSG and client + // hydration in agreement; see randomClientId in ./lib. + useEffect(() => { + setClientId((c) => c || randomClientId()); + }, []); + + // Revoke the previous object URL when the download changes or on unmount. + useEffect(() => () => { if (filePreview) URL.revokeObjectURL(filePreview.url); }, [filePreview]); + + // Connection ------------------------------------------------------------- + + async function setup() { + setBusy(true); + const cid = clientId || randomClientId(); + if (cid !== clientId) setClientId(cid); + setTunnelStatus({ text: 'Loading wasm...', colour: 'orange' }); + let m = mods; + try { + if (!m) { + const t0 = performance.now(); + m = await loadModules(); + setMods(m); + log('master', `Modules loaded (${(performance.now() - t0).toFixed(0)} ms)`); + } + } catch (e) { + setTunnelStatus({ text: 'Failed to load wasm', colour: 'red' }); + log('master', `module load failed: ${e}`, 'red'); + setBusy(false); + return; + } + + if (!useRandomIpr && !iprAddress.trim()) { + setTunnelStatus({ text: "IPR address required (or check 'random')", colour: 'red' }); + setBusy(false); + return; + } + + const opts: SetupOpts = { + ...(useRandomIpr ? {} : { preferredIpr: iprAddress.trim() }), + clientId: cid, + forceTls, + disablePoissonTraffic: disablePoisson, + disableCoverTraffic: disableCover, + openReplySurbs: clampSurbs(openSurbs), + dataReplySurbs: clampSurbs(dataSurbs), + primaryDns: primaryDns.trim() || undefined, + fallbackDns: fallbackDns.trim() || undefined, + debug, + }; + log( + 'master', + `setupMixTunnel (clientId=${cid}, IPR: ${ + useRandomIpr ? 'auto-discover' : iprAddress.trim().slice(0, 28) + '...' + })`, + ); + setTunnelStatus({ text: 'Connecting to mixnet...', colour: 'orange' }); + // The gateway/IPR/smoltcp detail is printed by the Rust client straight to + // the worker's console; it can't be forwarded to this panel. Point the user + // there rather than silently dropping it. + log( + 'master', + debug + ? 'Connecting... (gateway, IPR discovery and smoltcp logs are in the browser console)' + : 'Connecting... (tick "Verbose transport logs" for the gateway/IPR detail in the console)', + 'gray', + ); + + try { + const t0 = performance.now(); + const st = await m.getTunnelState(); + if (st.state === 'ready') { + log('master', 'Tunnel already up; reusing it.', 'green'); + } else { + await m.setupMixTunnel(opts); + log('master', `setupMixTunnel OK: tunnel ready in ${((performance.now() - t0) / 1000).toFixed(1)}s`, 'green'); + } + const final = await m.getTunnelState(); + log('master', `tunnel state: ${final.state}${final.reason ? ` (${final.reason})` : ''}`); + setConnected(true); + setTunnelStatus({ text: 'Connected', colour: 'green' }); + } catch (e) { + const msg = String(e); + if (/already initialised/.test(msg)) { + setTunnelStatus({ text: 'Tunnel spent; reload the page to reconnect', colour: 'red' }); + log('master', 'tunnel already initialised but not ready; reload the page', 'red'); + } else { + setTunnelStatus({ text: `Failed: ${msg}`, colour: 'red' }); + log('master', `setupMixTunnel failed: ${msg}`, 'red'); + } + } finally { + setBusy(false); + } + } + + async function disconnect() { + if (!mods) return; + setBusy(true); + log('master', 'Disconnecting...'); + try { + await mods.disconnectMixTunnel(); + log('master', 'Disconnected. Reload the page to reconnect (the wasm tunnel is one-shot).', 'green'); + setConnected(false); + setTunnelStatus({ text: 'Disconnected', colour: 'gray' }); + } catch (e) { + log('master', `disconnect failed: ${e}`, 'red'); + } finally { + setBusy(false); + } + } + + // DNS -------------------------------------------------------------------- + + async function dnsTunnel() { + if (!mods) return; + const h = dnsHost.trim(); + if (!h) return log('dns', 'Hostname is required', 'red'); + log('dns', `tunnel resolve ${h}`); + const t0 = performance.now(); + try { + const ip = await mods.mixDNS(h); + log('dns', `tunnel ${h} => ${ip} (${(performance.now() - t0).toFixed(0)} ms)`, 'green'); + } catch (e) { + log('dns', `tunnel resolve failed: ${e}`, 'red'); + } + } + + async function dnsClearnet() { + const h = dnsHost.trim(); + if (!h) return log('dns', 'Hostname is required', 'red'); + log('dns', `clearnet DoH resolve ${h}`); + const t0 = performance.now(); + try { + const ip = await dohResolve(h); + log( + 'dns', + `clearnet ${h} => ${ip} (${(performance.now() - t0).toFixed(0)} ms); visible in DevTools Network`, + 'green', + ); + } catch (e) { + log('dns', `clearnet DoH failed: ${e}`, 'red'); + } + } + + // GET -------------------------------------------------------------------- + + async function getTunnel() { + if (!mods) return; + const u = getUrl.trim(); + if (!u) return log('get', 'URL is required', 'red'); + log('get', `tunnel GET ${u}`); + const t0 = performance.now(); + try { + const resp = await mods.mixFetch(u, {}); + log('get', `tunnel ${resp.status} ${resp.statusText} (${(performance.now() - t0).toFixed(0)} ms)`, 'green'); + } catch (e) { + log('get', `tunnel GET failed: ${e}`, 'red'); + } + } + + async function getClearnet() { + const u = getUrl.trim(); + if (!u) return log('get', 'URL is required', 'red'); + log('get', `clearnet GET ${u}`); + const t0 = performance.now(); + try { + const resp = await window.fetch(u, { mode: 'cors' }); + log( + 'get', + `clearnet ${resp.status} ${resp.statusText} (${(performance.now() - t0).toFixed(0)} ms); visible in DevTools Network`, + 'green', + ); + } catch (e) { + log('get', `clearnet fetch failed: ${e}`, 'red'); + } + } + + // WebSocket -------------------------------------------------------------- + + const onWsMessage = useCallback( + (ev: Event) => { + const e = ev as MessageEvent; + let rtt: number | null = null; + if (wsSendQueue.current.length) rtt = performance.now() - (wsSendQueue.current.shift() as number); + + const b = burstRef.current; + if (b) { + if (rtt != null) b.rtts.push(rtt); + const recvBuf = new Uint8Array(e.data as ArrayBuffer); + const sent = b.payloads[b.received]; + if (sent && eqBytes(recvBuf, sent)) b.verified++; + else b.mismatches++; + b.received++; + if (b.received >= b.expected) b.resolve(); + return; + } + + const data = e.data; + let preview: string; + if (typeof data === 'string') preview = data.length <= 200 ? data : data.slice(0, 200) + '...'; + else if (data instanceof ArrayBuffer) preview = `[binary ${data.byteLength} bytes] ${hexPreview(data)}`; + else preview = `[${typeof data}]`; + log('ws', rtt != null ? `recv (${rtt.toFixed(0)} ms RTT): ${preview}` : `recv: ${preview}`, 'green'); + }, + [log], + ); + + async function wsConnect() { + if (!mods) return; + const url = wsUrl.trim(); + if (!url) return log('ws', 'WebSocket URL is required', 'red'); + if (wsRef.current && wsRef.current.readyState !== 3) await wsRef.current.close().catch(() => {}); + + setWsStatus({ text: 'Connecting...', colour: 'orange' }); + wsSendQueue.current = []; + log('ws', `connecting to ${url}`); + const t0 = performance.now(); + + const ws = new mods.MixWebSocket(url); + ws.addEventListener('message', onWsMessage); + ws.addEventListener('close', (ev) => { + const e = ev as CloseEvent; + log('ws', `closed: ${e.code} ${e.reason || ''}${e.wasClean ? '' : ' (unclean)'}`, 'orange'); + setWsStatus({ text: 'Closed', colour: 'gray' }); + setWsConnected(false); + wsRef.current = null; + }); + ws.addEventListener('error', () => { + log('ws', 'error', 'red'); + setWsStatus({ text: 'Error', colour: 'red' }); + }); + + try { + await ws.opened(); + const ms = (performance.now() - t0).toFixed(0); + log('ws', `connected in ${ms} ms (protocols=${ws.protocols.join(',') || 'none'})`, 'green'); + setWsStatus({ text: `Connected (${ms} ms)`, colour: 'green' }); + setWsConnected(true); + wsRef.current = ws; + } catch (e) { + log('ws', `connect failed: ${e}`, 'red'); + setWsStatus({ text: 'Error', colour: 'red' }); + } + } + + async function wsSend() { + const ws = wsRef.current; + if (!ws || ws.readyState !== 1) return; + wsSendQueue.current.push(performance.now()); + await ws.send(wsMessage); + log('ws', `send: ${wsMessage}`); + } + + async function wsClose() { + const ws = wsRef.current; + if (!ws) return; + log('ws', 'closing...'); + await ws.close(1000, 'user requested'); + } + + async function wsBurst() { + const ws = wsRef.current; + if (!ws || ws.readyState !== 1) return; + if (burstCount < 1 || burstCount > 500) return log('ws', 'burst count must be 1-500', 'red'); + if (burstMin < 1 || burstMax < burstMin) return log('ws', 'invalid size range', 'red'); + + const payloads: Uint8Array[] = []; + let totalBytes = 0; + for (let i = 0; i < burstCount; i++) { + const size = burstMin === burstMax ? burstMin : burstMin + Math.floor(Math.random() * (burstMax - burstMin + 1)); + const buf = new Uint8Array(size); + crypto.getRandomValues(buf); + payloads.push(buf); + totalBytes += size; + } + log('ws', `echo burst: ${burstCount} msgs, ${formatSize(burstMin)}-${formatSize(burstMax)} (${formatSize(totalBytes)} total)`); + setBurstBusy(true); + + const done = new Promise((resolve) => { + burstRef.current = { payloads, received: 0, verified: 0, mismatches: 0, rtts: [], expected: burstCount, resolve }; + }); + const t0 = performance.now(); + for (let i = 0; i < burstCount; i++) { + wsSendQueue.current.push(performance.now()); + ws.send(payloads[i]); // fire in order; Comlink preserves FIFO to the worker + } + await done; + const totalMs = performance.now() - t0; + const b = burstRef.current!; + burstRef.current = null; + + const rtts = b.rtts.slice().sort((a, c) => a - c); + const pick = (q: number) => (rtts.length ? rtts[Math.min(rtts.length - 1, Math.floor(rtts.length * q))].toFixed(0) : 'n/a'); + const avg = rtts.length ? (rtts.reduce((a, c) => a + c, 0) / rtts.length).toFixed(0) : 'n/a'; + const msgPerSec = (burstCount / (totalMs / 1000)).toFixed(1); + + log('ws', `burst done: ${burstCount} msgs in ${(totalMs / 1000).toFixed(2)}s (${msgPerSec} msg/s, ${formatRate(totalBytes, totalMs)})`, 'green'); + log('ws', `verify: ${b.verified}/${burstCount} OK${b.mismatches ? `, ${b.mismatches} MISMATCH` : ''}`, b.mismatches === 0 ? 'green' : 'red'); + log('ws', `RTT: min=${pick(0)} avg=${avg} p50=${pick(0.5)} p95=${pick(0.95)} max=${pick(1)} ms`); + setBurstBusy(false); + } + + // Stress ----------------------------------------------------------------- + + async function oneStress(req: { id: number; url: string; label: string }) { + const start = performance.now(); + try { + const resp = await mods!.mixFetch(req.url, {}); + const body = await resp.text(); + const el = ((performance.now() - start) / 1000).toFixed(2); + log('stress', `[#${req.id} ${req.label}] ${resp.status} OK ${el}s (${body.length}B)`, 'green'); + return { ok: true, id: req.id, label: req.label }; + } catch (e) { + const el = ((performance.now() - start) / 1000).toFixed(2); + log('stress', `[#${req.id} ${req.label}] FAIL ${el}s: ${e}`, 'red'); + return { ok: false, id: req.id, label: req.label }; + } + } + + async function runStress() { + if (!mods) return; + setStressBusy(true); + setStressStatus({ text: 'Running...', colour: 'orange' }); + const reqs = generateRequests(stressCount, stressMode, stressTimeout, stressUrl.trim()); + if (stressMode !== 'uniform') { + const bd: Record = {}; + reqs.forEach((r) => (bd[r.label] = (bd[r.label] || 0) + 1)); + log('stress', `${stressCount} requests, ${stressMode} mode, profiles: ${JSON.stringify(bd)}`); + } else { + log('stress', `${stressCount} requests, uniform mode`); + } + const t0 = performance.now(); + const settled = await Promise.allSettled(reqs.map((r) => oneStress(r))); + const totalSec = ((performance.now() - t0) / 1000).toFixed(2); + const ok = settled.filter((s) => s.status === 'fulfilled' && s.value.ok).length; + const fail = stressCount - ok; + log('stress', `done: ${ok}/${stressCount} OK, ${fail} failed (${totalSec}s total)`, fail === 0 ? 'green' : 'red'); + setStressStatus({ text: `Done: ${ok}/${stressCount} OK, ${fail} failed (${totalSec}s)`, colour: fail === 0 ? 'green' : 'red' }); + setStressBusy(false); + } + + // Download --------------------------------------------------------------- + + async function verifyText() { + if (!mods) return; + setTextBusy(true); + setTextStatus({ text: 'Fetching...', colour: 'orange' }); + log('download', `GET ${VERIFY_TEXT_URL} over the tunnel... (live transport logs in the browser console)`, 'orange'); + const t0 = performance.now(); + try { + const resp = await mods.mixFetch(VERIFY_TEXT_URL, {}); + const text = await resp.text(); + const ms = (performance.now() - t0).toFixed(0); + setTextStatus({ text: `${formatSize(text.length)} in ${ms} ms`, colour: 'green' }); + setTextOutput(text); + log('download', `UTF-8 demo: ${formatSize(text.length)} in ${ms} ms`, 'green'); + } catch (e) { + setTextStatus({ text: `Failed: ${e}`, colour: 'red' }); + log('download', `UTF-8 demo FAILED: ${e}`, 'red'); + } finally { + setTextBusy(false); + } + } + + async function fetchFile() { + if (!mods) return; + const url = downloadUrl.trim(); + if (!url) return log('download', 'Download URL is required', 'red'); + setPdfBusy(true); + cachedPdf.current = null; + setPdfInfo(null); + setFilePreview(null); + setPdfStatus({ text: 'Fetching...', colour: 'orange' }); + log('download', `GET ${url} over the tunnel... (live transport logs in the browser console)`, 'orange'); + const t0 = performance.now(); + try { + const resp = await mods.mixFetch(url, {}); + const buf = await resp.arrayBuffer(); + const ms = performance.now() - t0; + const hash = await sha256hex(buf); + cachedPdf.current = buf; + setPdfInfo({ size: buf.byteLength, hash }); + const contentType = resp.headers.get('content-type') || ''; + const isImage = contentType.startsWith('image/') || /\.(jpe?g|png|gif|webp|avif|svg)(\?|$)/i.test(url); + const objectUrl = URL.createObjectURL(new Blob([buf], contentType ? { type: contentType } : undefined)); + setFilePreview({ url: objectUrl, isImage }); + setPdfStatus({ text: `${formatSize(buf.byteLength)} in ${(ms / 1000).toFixed(1)}s`, colour: 'green' }); + log( + 'download', + `${formatSize(buf.byteLength)} in ${(ms / 1000).toFixed(1)}s (${formatRate(buf.byteLength, ms)}); SHA-256: ${hash.slice(0, 16)}...`, + 'green', + ); + } catch (e) { + setPdfStatus({ text: `Failed: ${e}`, colour: 'red' }); + log('download', `FAILED: ${e}`, 'red'); + } finally { + setPdfBusy(false); + } + } + + function savePdf() { + const buf = cachedPdf.current; + if (!buf) return; + const filename = decodeURIComponent(downloadUrl.trim().split('/').pop()?.split('?')[0] || 'download'); + saveFile(buf, filename, 'application/octet-stream'); + } + + async function runBoth() { + setBothStatus({ text: 'Running...', colour: 'orange' }); + const t0 = performance.now(); + await Promise.allSettled([verifyText(), fetchFile()]); + setBothStatus({ text: `Done in ${((performance.now() - t0) / 1000).toFixed(1)}s`, colour: 'green' }); + } + + // Render ----------------------------------------------------------------- + + return ( +
+ {/* Connection */} +
+
Connection
+
+ + setIprAddress(e.target.value)} + placeholder="" + disabled={useRandomIpr} + /> +
+ +
+ Advanced options +
+
+ + + +
+
+ + setClientId(e.target.value)} /> +
+
+ + setOpenSurbs(+e.target.value)} /> + + setDataSurbs(+e.target.value)} /> +
+
+ + setPrimaryDns(e.target.value)} placeholder="8.8.8.8:53" /> + + setFallbackDns(e.target.value)} placeholder="1.1.1.1:53" /> +
+
+
+ +
+ + + + +
+ +
+ One-shot per page: after disconnectMixTunnel you must reload to reconnect, and each load uses a fresh client identity. +
+
+ This timeline shows the API-level events your code sees; the Rust client's deep transport logs (gateway, IPR discovery, smoltcp) go to the browser console behind Verbose transport logs. +
+
+ + {/* DNS */} +
+
DNS resolve: tunnel vs clearnet
+
+ setDnsHost(e.target.value)} placeholder="example.com" /> + + +
+
The clearnet DoH query appears in DevTools Network; the tunnel resolution does not.
+
Resolve the same hostname twice: the second answer comes from the in-wasm DNS cache, served locally with no mixnet round-trip.
+ +
+ + {/* GET */} +
+
GET: tunnel vs clearnet
+
+ setGetUrl(e.target.value)} placeholder="https://..." /> + + +
+
Both buttons request the same URL, but the clearnet one reaches the server from your own IP and the tunnel one from the IPR's exit gateway.
+
The clearnet button is a normal browser request, so some hosts block it with CORS while the tunnel request to the same URL succeeds; the defaults here are CORS-permissive.
+
The first tunnel request to a host runs a full TCP + TLS handshake (visible in the browser console with debug logging on). The HTTPS connection is then pooled, so a second request to the same host skips the handshake; the log timings show the difference.
+ +
+ + {/* WebSocket */} +
+
WebSocket
+
+ setWsUrl(e.target.value)} placeholder="wss://..." /> + + + +
+
+ setWsMessage(e.target.value)} /> + +
+
+ + setBurstCount(+e.target.value)} /> + + setBurstMin(+e.target.value)} /> + + setBurstMax(+e.target.value)} /> + bytes + +
+
Connecting runs a TCP handshake (plus a TLS handshake for wss://) inside the worker, visible in the browser console with debug logging on.
+ +
+ + {/* Stress */} +
+
Stress test
+
+ + setStressCount(+e.target.value)} /> + + + + +
+ {stressMode === 'uniform' && ( +
+ + setStressUrl(e.target.value)} /> +
+ )} + {stressMode === 'mixed' &&
Random mix of 128 B / 1 KB / 10 KB / 100 KB / 1 MB responses (httpbin.org/bytes).
} + {stressMode === 'drip' && ( +
+ + setStressTimeout(+e.target.value)} /> + safe / boundary / over / slow-start, relative to this timeout (httpbin.org/drip). +
+ )} +
Requests to the same host share one pooled TCP + TLS connection, so only the first pays the handshake cost.
+ +
+ + {/* Download */} +
+
File download
+
+ + {textBusy ? : } +
+ {textOutput != null && ( +
+            {textOutput}
+          
+ )} +
+ setDownloadUrl(e.target.value)} /> + + + + + {pdfBusy ? : } +
+ {pdfInfo && ( +
+ Size: {pdfInfo.size.toLocaleString()} bytes · SHA-256: {pdfInfo.hash} +
+ )} + {filePreview?.isImage && ( + File downloaded over the mixnet + )} +
Fetches a real file over the tunnel and reports its size and SHA-256. Fetch it twice and the second download reuses the pooled HTTPS connection, skipping the handshake.
+ +
+
+ ); +} diff --git a/documentation/docs/components/playground/lib.ts b/documentation/docs/components/playground/lib.ts new file mode 100644 index 0000000000..268ef21d53 --- /dev/null +++ b/documentation/docs/components/playground/lib.ts @@ -0,0 +1,189 @@ +// Pure helpers + package loader for the mix playground. No React here: this is +// the logic ported from `wasm/smolmix/internal-dev/index.js`, adapted to drive +// the *published* @nymproject/mix-* packages instead of internal-dev's own +// worker. The differences that matter: +// - published `mixFetch` returns a real `Response` (internal-dev returned a +// raw `{body,status,statusText,headers}` and wrapped it); +// - there is no live `setDebugLogging`; debug is a `setupMixTunnel` opt; +// - the WebSocket is the EventTarget-based `MixWebSocket` (async send/close, +// `opened()`, binaryType fixed to arraybuffer). + +// Local mirror of the published `SetupMixTunnelOpts` (subset we surface). +export interface SetupOpts { + preferredIpr?: string; + clientId?: string; + forceTls?: boolean; + disablePoissonTraffic?: boolean; + disableCoverTraffic?: boolean; + openReplySurbs?: number; + dataReplySurbs?: number; + primaryDns?: string; + fallbackDns?: string; + debug?: boolean; +} + +export interface TunnelState { + state: string; + reason?: string; +} + +// Mirror of the published `MixWebSocket` runtime surface. +export interface MixWebSocketLike extends EventTarget { + send(data: string | ArrayBuffer | Uint8Array): Promise; + close(code?: number, reason?: string): Promise; + opened(): Promise; + readonly readyState: number; + readonly protocols: string[]; +} + +// The slice of the three packages the playground uses. They share one tunnel +// (mix-tunnel is deduped), so setup/disconnect/state are taken from mix-fetch. +export interface PlaygroundMods { + setupMixTunnel(opts?: SetupOpts): Promise; + disconnectMixTunnel(): Promise; + getTunnelState(): Promise; + mixFetch(url: string, init?: RequestInit): Promise; + mixDNS(hostname: string): Promise; + MixWebSocket: new (url: string, protocols?: string | string[]) => MixWebSocketLike; +} + +// Lazy-load the three facades. Literal specifiers keep webpack code-splitting +// the wasm into async chunks (loaded only when the user clicks Setup). The +// `@ts-ignore`s are harmless once installed; they keep a fresh checkout +// type-checking before `pnpm install`. +export async function loadModules(): Promise { + const [f, d, w] = await Promise.all([ + // @ts-ignore -- @nymproject/mix-fetch resolves at runtime; lazy wasm chunk + import('@nymproject/mix-fetch'), + // @ts-ignore -- @nymproject/mix-dns resolves at runtime; lazy wasm chunk + import('@nymproject/mix-dns'), + // @ts-ignore -- @nymproject/mix-websocket resolves at runtime; lazy wasm chunk + import('@nymproject/mix-websocket'), + ]); + return { + setupMixTunnel: f.setupMixTunnel, + disconnectMixTunnel: f.disconnectMixTunnel, + getTunnelState: f.getTunnelState, + mixFetch: f.mixFetch, + mixDNS: d.mixDNS, + MixWebSocket: w.MixWebSocket, + } as unknown as PlaygroundMods; +} + +// Fresh client-storage id per page load so a reload gets a clean identity and +// doesn't collide with the gateway connection from the previous load (which +// lingers in the gateway's post-disconnect grace window). See tunnel.rs: +// "Randomise per session to get a clean client". +// +// Called from a post-mount effect, never at module/render time: Math.random at +// render would differ between SSG and the client and trip React hydration. +export const randomClientId = () => `smolmix-playground-${Math.random().toString(36).slice(2, 8)}`; + +export const clampSurbs = (n: number) => Math.min(50, Math.max(0, n)); + +export function formatSize(bytes: number): string { + if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB'; + if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return bytes + ' B'; +} + +export function formatRate(bytes: number, ms: number): string { + return (bytes / 1024 / (ms / 1000)).toFixed(1) + ' KB/s'; +} + +export function hexPreview(data: Uint8Array | ArrayBuffer, maxBytes = 64): string { + const bytes = data instanceof Uint8Array ? data : new Uint8Array(data); + const len = Math.min(bytes.length, maxBytes); + const hex = Array.from(bytes.slice(0, len), (b) => b.toString(16).padStart(2, '0')).join(' '); + return bytes.length > maxBytes ? `${hex} ...` : hex; +} + +export async function sha256hex(buf: ArrayBuffer): Promise { + const hash = await crypto.subtle.digest('SHA-256', buf); + return Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, '0')).join(''); +} + +export function saveFile(buf: ArrayBuffer, filename: string, mimeType: string): void { + const blob = new Blob([buf], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + +// The browser exposes no raw DNS API; "clearnet DNS" from JS is DoH (HTTPS) to +// a public resolver. Google's JSON API is CORS-friendly and returns +// { Status, Answer: [{ name, type, TTL, data }] } where type=1 is an A record. +// The request shows up in DevTools Network as a plain HTTPS fetch. +export async function dohResolve(hostname: string): Promise { + const resp = await window.fetch( + `https://dns.google/resolve?name=${encodeURIComponent(hostname)}&type=A`, + { mode: 'cors' }, + ); + const json = await resp.json(); + if (json.Status !== 0) throw new Error(`DoH status=${json.Status}`); + const a = (json.Answer as Array<{ type: number; data: string }> | undefined)?.find( + (x) => x.type === 1, + ); + if (!a) throw new Error('no A record'); + return a.data; +} + +// Stress-test request generation (uniform / mixed / drip), ported verbatim. +export interface StressRequest { + id: number; + url: string; + label: string; +} + +const SIZE_PROFILES = [ + { label: 'tiny', bytes: 128 }, + { label: 'small', bytes: 1024 }, + { label: 'medium', bytes: 10240 }, + { label: 'large', bytes: 102400 }, + { label: 'xlarge', bytes: 1048576 }, +]; + +function buildDripProfiles(timeoutSec: number) { + return [ + { label: 'safe', duration: Math.round(timeoutSec * 0.5), delay: 0, bytes: 100 }, + { label: 'boundary', duration: Math.round(timeoutSec * 0.92), delay: 0, bytes: 100 }, + { label: 'over', duration: Math.round(timeoutSec * 1.08), delay: 0, bytes: 100 }, + { + label: 'slow-start', + duration: Math.round(timeoutSec * 0.83), + delay: Math.round(timeoutSec * 0.17), + bytes: 100, + }, + ]; +} + +export function generateRequests( + count: number, + mode: 'uniform' | 'mixed' | 'drip', + timeoutSec: number, + baseUrl: string, +): StressRequest[] { + const requests: StressRequest[] = []; + if (mode === 'uniform') { + for (let i = 1; i <= count; i++) requests.push({ id: i, url: `${baseUrl}${i}`, label: 'uniform' }); + } else if (mode === 'mixed') { + for (let i = 1; i <= count; i++) { + const p = SIZE_PROFILES[Math.floor(Math.random() * SIZE_PROFILES.length)]; + requests.push({ id: i, url: `https://httpbin.org/bytes/${p.bytes}`, label: p.label }); + } + } else { + const profiles = buildDripProfiles(timeoutSec); + for (let i = 1; i <= count; i++) { + const p = profiles[Math.floor(Math.random() * profiles.length)]; + requests.push({ + id: i, + url: `https://httpbin.org/drip?duration=${p.duration}&numbytes=${p.bytes}&delay=${p.delay}&code=200`, + label: p.label, + }); + } + } + return requests; +} diff --git a/documentation/docs/components/playground/messaging-section.tsx b/documentation/docs/components/playground/messaging-section.tsx new file mode 100644 index 0000000000..74299fd1cd --- /dev/null +++ b/documentation/docs/components/playground/messaging-section.tsx @@ -0,0 +1,139 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { box, row, legend, sub, input, Button, LogPanel, StatusText, useLogs, type Status } from './ui'; + +// Raw mixnet messaging demo, styled to match the mix-* playground sections. +// Uses @nymproject/sdk-full-fat (a separate wasm client from the smolmix tunnel) +// to create a Nym client, send a message to a Nym address, and receive it. +// +// The SDK is imported dynamically on Connect, not at module scope: that keeps +// the page SSR/SSG-safe and means the second wasm runtime + gateway connection +// only load when the visitor opts in. + +const nymApiUrl = 'https://validator.nymtech.net/api'; +const preferredGateway = 'q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1'; + +// Minimal shape of the bits of NymMixnetClient we use, to type the `any` from +// the dynamic import. +interface MessagingClient { + client: { + start(opts: { + clientId: string; + nymApiUrl: string; + forceTls?: boolean; + preferredGateway?: string; + }): Promise; + stop(): Promise; + send(args: { payload: { message: string; mimeType: string }; recipient: string }): Promise; + }; + events: { + subscribeToLoaded(cb: (e: { args: unknown }) => void): void; + subscribeToConnected(cb: (e: { args: { address: string } }) => void): void; + subscribeToTextMessageReceivedEvent(cb: (e: { args: { payload: string } }) => void): void; + }; +} + +export function MessagingDemo() { + const { log, lines } = useLogs(); + const [status, setStatus] = useState({ text: 'Not connected', colour: 'gray' }); + const [connected, setConnected] = useState(false); + const [busy, setBusy] = useState(false); + const [selfAddress, setSelfAddress] = useState(''); + const [recipient, setRecipient] = useState(''); + const [message, setMessage] = useState('hello through the mixnet'); + const clientRef = useRef(null); + + useEffect(() => { + return () => { + clientRef.current?.client.stop().catch(() => {}); + }; + }, []); + + async function connect() { + setBusy(true); + setStatus({ text: 'Loading SDK...', colour: 'orange' }); + log('msg', 'Loading @nymproject/sdk-full-fat (wasm)...'); + try { + // @ts-ignore -- published separately; dynamic import keeps the wasm off SSR and lazy + const mod = await import('@nymproject/sdk-full-fat'); + const nym = (await mod.createNymMixnetClient()) as unknown as MessagingClient; + clientRef.current = nym; + + nym.events.subscribeToLoaded(() => log('msg', 'client wasm loaded', 'green')); + nym.events.subscribeToConnected((e) => { + const addr = e.args.address; + setSelfAddress(addr); + setRecipient((r) => r || addr); // default to sending to yourself + setConnected(true); + setStatus({ text: 'Connected', colour: 'green' }); + log('msg', `connected; self address: ${addr}`, 'green'); + }); + nym.events.subscribeToTextMessageReceivedEvent((e) => { + log('msg', `received: ${e.args.payload}`, 'green'); + }); + + log('msg', 'Starting client and connecting to a gateway...'); + setStatus({ text: 'Connecting to mixnet...', colour: 'orange' }); + await nym.client.start({ clientId: crypto.randomUUID(), nymApiUrl, forceTls: true, preferredGateway }); + } catch (err) { + setStatus({ text: 'Failed', colour: 'red' }); + log('msg', `error: ${err instanceof Error ? err.message : String(err)}`, 'red'); + setBusy(false); + } + setBusy(false); + } + + async function send() { + const nym = clientRef.current; + if (!nym || !recipient || !message) return; + try { + await nym.client.send({ payload: { message, mimeType: 'text/plain' }, recipient }); + log('msg', `sent: ${message}`); + } catch (err) { + log('msg', `send failed: ${err instanceof Error ? err.message : String(err)}`, 'red'); + } + } + + return ( +
+
Raw mixnet messaging
+
+ Creates a client with @nymproject/sdk-full-fat and sends a message to a Nym + address through the mixnet (defaults to your own address). This loads a separate wasm runtime + and connects its own client, so it is opt-in. +
+
+ + +
+ {selfAddress && ( +
+ Your address: {selfAddress} +
+ )} +
+ setRecipient(e.target.value)} + placeholder="recipient Nym address" + disabled={!connected} + /> +
+
+ setMessage(e.target.value)} + placeholder="message" + disabled={!connected} + /> + +
+ +
+ ); +} diff --git a/documentation/docs/components/playground/ui.tsx b/documentation/docs/components/playground/ui.tsx new file mode 100644 index 0000000000..361de69882 --- /dev/null +++ b/documentation/docs/components/playground/ui.tsx @@ -0,0 +1,145 @@ +// Shared presentational primitives for the playground sections (MixPlayground +// and the raw-messaging demo) so they share one look. Theme-neutral inline +// styles (rgba greys read on light and dark Nextra themes), a per-section log +// store, and an autoscrolling log panel. + +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +export type Colour = 'green' | 'red' | 'orange' | 'gray' | undefined; +export const COLOURS: Record = { + green: '#16a34a', + red: '#dc2626', + orange: '#d97706', + gray: '#9ca3af', +}; + +export interface LogEntry { + ts: string; + msg: string; + colour?: Colour; +} + +// One append-only buffer per section, keyed by section name. Red entries mirror +// to console.error so they sit alongside the Rust-side `[smolmix] ...` logs. +export function useLogs() { + const [store, setStore] = useState>({}); + const log = useCallback((section: string, msg: string, colour?: Colour) => { + const ts = new Date().toISOString().slice(11, 23); + if (colour === 'red') console.error(`[smolmix-playground:${section}]`, msg); + setStore((s) => ({ ...s, [section]: [...(s[section] ?? []), { ts, msg, colour }] })); + }, []); + const lines = useCallback((section: string) => store[section] ?? [], [store]); + return { log, lines }; +} + +export const box: React.CSSProperties = { + border: '1px solid rgba(127,127,127,0.3)', + borderRadius: 8, + padding: '1rem', + margin: '1rem 0', +}; +export const row: React.CSSProperties = { + display: 'flex', + gap: '0.5rem', + alignItems: 'center', + flexWrap: 'wrap', + marginBottom: '0.5rem', +}; +export const btn: React.CSSProperties = { + padding: '0.35rem 0.8rem', + borderRadius: 6, + border: '1px solid rgba(127,127,127,0.4)', + background: 'transparent', + cursor: 'pointer', + fontSize: 14, +}; +export const input: React.CSSProperties = { + padding: '0.35rem 0.6rem', + borderRadius: 6, + border: '1px solid rgba(127,127,127,0.4)', + background: 'transparent', + fontSize: 14, + flex: '1 1 14rem', + minWidth: 0, +}; +export const num: React.CSSProperties = { ...input, flex: '0 0 5rem' }; +export const legend: React.CSSProperties = { fontWeight: 600, marginBottom: '0.6rem' }; +export const sub: React.CSSProperties = { fontSize: 12, opacity: 0.65 }; + +export function Button(props: React.ButtonHTMLAttributes) { + return ( + - {status === "starting" && } - - {status === "idle" ? "Not started" : - status === "starting" ? "Starting..." : - status === "ready" ? "Ready" : - `Error: ${errorMsg}`} - - - - - {/* Fetch controls (disabled until MixFetch is ready) */} - - {/* Single fetch */} - - setUrl(e.target.value)} - /> - - - {busy && } - {html && ( - <> - Response - - {html} - - - )} - - {/* Concurrent fetch */} - - Concurrent Requests - - - - - {concurrentBusy && } - {concurrentResults.length > 0 && ( - - {concurrentResults.map((result, i) => ( - {result} - ))} - - )} - - - {/* Log Panel */} - {logs.length > 0 && ( - - Log - {logs.map((entry, i) => ( - - {entry.timestamp} [{logLabels[entry.level]}] {entry.message} - - ))} -
- - )} -
- ); -}; -``` diff --git a/documentation/docs/pages/developers/typescript/examples/mixnet.mdx b/documentation/docs/pages/developers/typescript/examples/mixnet.mdx deleted file mode 100644 index 6d0810c4f6..0000000000 --- a/documentation/docs/pages/developers/typescript/examples/mixnet.mdx +++ /dev/null @@ -1,179 +0,0 @@ ---- -title: "TypeScript Mixnet Client Example" -description: "Send and receive private messages in the browser using the Nym TypeScript SDK. Includes setup, SURB anonymous replies, and environment configuration." -schemaType: "TechArticle" -section: "Developers" -lastUpdated: "2026-03-15" ---- - -import { Callout } from 'nextra/components' - -# Mixnet Client - -The [`SDK Client`](https://www.npmjs.com/package/@nymproject/sdk) lets you send and receive messages over the Nym mixnet. - -The client is message-based: it sends one-way messages to another client's address. Replying can be achieved in two ways: -- Reveal the sender's address to the recipient (as part of the payload) -- Use a SURB (single use reply block) that lets the recipient reply without compromising the identity of either party - -## Environment Setup - -Create a new project with Vite: - -```bash -npm create vite@latest -``` - -Choose React + TypeScript, then: - -```bash -cd -npm i -npm run dev -``` - -## Installation - -```bash -npm install @nymproject/sdk-full-fat -``` - -## Full Example - -This example creates a Mixnet client, connects to a gateway, and provides a UI for sending and receiving messages through the mixnet. - - -For this example we use the `full-fat` version of the ESM SDK. If you use the unbundled ESM variant, make sure your [bundler configuration](/developers/typescript/bundling/bundling) copies the WASM and web worker files to the output bundle. - - -```ts copy filename="App.tsx" -import React, { useEffect, useState } from "react"; -import { - createNymMixnetClient, - NymMixnetClient, - Payload, -} from "@nymproject/sdk-full-fat"; -import Box from "@mui/material/Box"; -import CircularProgress from "@mui/material/CircularProgress"; -import Paper from "@mui/material/Paper"; -import Typography from "@mui/material/Typography"; -import Stack from "@mui/material/Stack"; -import TextField from "@mui/material/TextField"; -import Button from "@mui/material/Button"; - -const nymApiUrl = "https://validator.nymtech.net/api"; - -export const Traffic = () => { - const [nym, setNym] = useState(); - const [selfAddress, setSelfAddress] = useState(); - const [recipient, setRecipient] = useState(); - const [payload, setPayload] = useState(); - const [receivedMessage, setReceivedMessage] = useState(); - const [buttonEnabled, setButtonEnabled] = useState(false); - - const init = async () => { - const client = await createNymMixnetClient(); - setNym(client); - - // start the client and connect to a gateway - await client?.client.start({ - clientId: crypto.randomUUID(), - nymApiUrl, - forceTls: true, // force WSS - }); - - // check when is connected and set the self address - client?.events.subscribeToConnected((e) => { - const { address } = e.args; - setSelfAddress(address); - }); - - // show whether the client is ready or not - client?.events.subscribeToLoaded((e) => { - console.log("Client ready: ", e.args); - }); - - // show message payload content when received - client?.events.subscribeToTextMessageReceivedEvent((e) => { - console.log(e.args.payload); - setReceivedMessage(e.args.payload); - }); - }; - - const stop = async () => { - await nym?.client.stop(); - }; - - const send = () => - payload && recipient && nym?.client.send({ payload, recipient }); - - useEffect(() => { - init(); - return () => { - stop(); - }; - }, []); - - useEffect(() => { - if (recipient && payload) { - setButtonEnabled(true); - } else { - setButtonEnabled(false); - } - }, [recipient, payload]); - - if (!nym || !selfAddress) { - return ( - - - - ); - } - - return ( - - - - My self address is: - {selfAddress || "loading"} - Communication through the Mixnet - setRecipient(e.target.value)} - size="small" - /> - - setPayload({ message: e.target.value, mimeType: "text/plain" }) - } - size="small" - /> - - - {receivedMessage && ( - - Message Received! - {receivedMessage} - - )} - - - ); -}; -``` - - -If you encounter a Gateway client error that persists even after a hard refresh, open your browser console, navigate to the "Application" tab, and delete the databases listed under "IndexedDB". - diff --git a/documentation/docs/pages/developers/typescript/playground/_meta.json b/documentation/docs/pages/developers/typescript/playground/_meta.json deleted file mode 100644 index 09149f2a76..0000000000 --- a/documentation/docs/pages/developers/typescript/playground/_meta.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mixfetch": "1. Use mixFetch", - "traffic": "2. Send traffic over the mixnet", - "mixnodes": "3.1. Query mixnet contract for a list of mix nodes", - "wallet": "3.2. Basic wallet operations", - "cosmos-kit": "4. Cosmos Kit" -} diff --git a/documentation/docs/pages/developers/typescript/playground/cosmos-kit.mdx b/documentation/docs/pages/developers/typescript/playground/cosmos-kit.mdx deleted file mode 100644 index 4a6b1d8ccd..0000000000 --- a/documentation/docs/pages/developers/typescript/playground/cosmos-kit.mdx +++ /dev/null @@ -1,32 +0,0 @@ -import "@interchain-ui/react/styles" -import { CosmosKit } from "../../../../components/cosmos-kit"; -import FormattedCosmoskitExampleCode from '../../../../code-examples/sdk/typescript/cosmoskit-example-code.mdx'; - -# Cosmos Kit - -import { Callout } from 'nextra/components' - -Sign a transaction using [CosmosKit](https://cosmoskit.com/) wallet adapters. This demo connects a [Keplr](https://www.keplr.app/) browser wallet or [Ledger](https://www.ledger.com/) hardware wallet and signs a test message; no transaction is broadcast. - -**Try it:** Click Connect to link your wallet, then click Sign to sign a test message. The resulting signature hash is displayed. - - - - - No transactions will be broadcast. You will only be signing a transaction. - - -## Ledger setup - -If you are using a Ledger hardware wallet: - -- Install the `cosmoshub` app on the Ledger -- Connect it to your computer and unlock it -- Open the Cosmos Hub app -- Grant browser permissions when prompted (try another browser if no prompt appears) - -## How this works - -The component uses CosmosKit to manage wallet connections across different wallet providers (Keplr, Ledger). When you click Sign, it constructs a message, requests your wallet to sign it, and displays the signature hash. The same pattern works for any web application that interacts with the Nyx blockchain. - - diff --git a/documentation/docs/pages/developers/typescript/playground/mixfetch.mdx b/documentation/docs/pages/developers/typescript/playground/mixfetch.mdx deleted file mode 100644 index f75896a00b..0000000000 --- a/documentation/docs/pages/developers/typescript/playground/mixfetch.mdx +++ /dev/null @@ -1,22 +0,0 @@ -# mixFetch - -import { MixFetch } from '../../../../components/mix-fetch' -import Box from '@mui/material/Box'; -import FormattedMixFetchExampleCode from '../../../../code-examples/sdk/typescript/mixfetch-example-code.mdx'; -import { Callout } from 'nextra/components' - -Fetch a URL through the Nym Mixnet. This demo creates a mixFetch client in your browser, routes your HTTP request through the Mixnet, and displays the response. Your IP address is hidden from the destination server. - -**Try it:** Enter a URL and click Send. The default URL fetches a plaintext exit policy file. Try replacing it with any public HTTP endpoint. - - - - - Open your browser's console to see the connection and send/receive logging for this example. - - -## How this works - -The component calls `createMixFetch()` to initialise a Mixnet client in the browser, then uses the returned `mixFetch()` function as a replacement for `window.fetch()`. The request is routed through the Mixnet to a Network Requester, which makes the HTTP request on your behalf and returns the response anonymously. - - diff --git a/documentation/docs/pages/developers/typescript/playground/mixnodes.mdx b/documentation/docs/pages/developers/typescript/playground/mixnodes.mdx deleted file mode 100644 index 9745757dc3..0000000000 --- a/documentation/docs/pages/developers/typescript/playground/mixnodes.mdx +++ /dev/null @@ -1,18 +0,0 @@ -# Query for Mixnodes - -import { Mixnodes } from '../../../../components/mixnodes'; -import Box from '@mui/material/Box'; -import FormattedExampleCode from '../../../../code-examples/sdk/typescript/mixnodes-example-code.mdx'; -import { Callout } from 'nextra/components' - -Query the Nym Mixnet smart contract for a live list of registered Mix Nodes. This demo uses `@nymproject/contract-clients` to read on-chain data directly from your browser, no backend required. - -**Try it:** Click the button to fetch the current page of Mix Nodes from the Mixnet Contract. Each entry shows the node's identity key, owner address, and bonding details. - - - -## How this works - -The component creates a `MixnetContractClient` connected to the Nyx blockchain and calls `getMixNodesPaged()` to retrieve a paginated list of bonded Mix Nodes. This is a read-only query; no wallet or signing is needed. - - diff --git a/documentation/docs/pages/developers/typescript/playground/traffic.mdx b/documentation/docs/pages/developers/typescript/playground/traffic.mdx deleted file mode 100644 index de0a194396..0000000000 --- a/documentation/docs/pages/developers/typescript/playground/traffic.mdx +++ /dev/null @@ -1,22 +0,0 @@ -# Traffic - -import { Callout } from 'nextra/components' -import { Traffic } from '../../../../components/traffic'; -import Box from '@mui/material/Box'; -import FormattedTrafficExampleCode from '../../../../code-examples/sdk/typescript/traffic-example-code.mdx'; - -Send and receive messages through the Nym Mixnet directly in your browser. This demo creates a Mixnet client, connects to the network, and lets you send a message to yourself (or any Nym address) to see the full round-trip. - -**Try it:** Click Connect, wait for the client to initialise, then send a message. You'll see it arrive back through the Mixnet after traversing 5 hops. - - - Open your browser's console to see the connection and send/receive logging for this example. - - - - -## How this works - -The component creates a `NymMixnetClient`, subscribes to incoming messages via event listeners, and uses `client.send()` to dispatch messages through the Mixnet. When sending to your own address, the message travels through the full 5-hop route (Entry Gateway → 3 Mix Nodes → Exit Gateway) before arriving back at your client. - - diff --git a/documentation/docs/pages/developers/typescript/playground/wallet.mdx b/documentation/docs/pages/developers/typescript/playground/wallet.mdx deleted file mode 100644 index c0749c42d9..0000000000 --- a/documentation/docs/pages/developers/typescript/playground/wallet.mdx +++ /dev/null @@ -1,40 +0,0 @@ -# Wallet - -import Box from '@mui/material/Box'; -import { WalletContextProvider } from '../../../../components/wallet/utils/wallet.context'; -import { ConnectWallet } from '../../../../components/wallet/connect'; -import { SendTokes } from '../../../../components/wallet/sendTokens'; -import { Delegations } from '../../../../components/wallet/delegations'; -import FormattedWalletConnectCode from '../../../../code-examples/sdk/typescript/wallet-connect-code.mdx'; -import FormattedWalletSendTokensCode from '../../../../code-examples/sdk/typescript/wallet-sendTokens-code.mdx'; -import FormattedWalletDelegationsCode from '../../../../code-examples/sdk/typescript/wallet-delegations-code.mdx'; -import { Callout } from 'nextra/components' - -Interactive wallet operations on the Nyx Sandbox testnet. This demo walks through connecting to the chain, sending tokens, and querying delegations, all from the browser using `@nymproject/contract-clients`. - - -This connects to the **Sandbox testnet**. No real tokens are involved. - - - - ## Connect - - Create or restore a wallet from a mnemonic to connect to the Nyx Sandbox testnet. - - - - - ## Send Tokens - - Transfer testnet tokens between accounts. - - - - - ## Delegations - - Query staking delegations for an account. - - - - diff --git a/documentation/docs/pages/developers/typescript/quick-start.mdx b/documentation/docs/pages/developers/typescript/quick-start.mdx new file mode 100644 index 0000000000..d5ac8d77cf --- /dev/null +++ b/documentation/docs/pages/developers/typescript/quick-start.mdx @@ -0,0 +1,86 @@ +--- +title: "TypeScript SDK Quick Start" +description: "Send and receive messages over the Nym mixnet using @nymproject/sdk. Vanilla TypeScript, no framework." +schemaType: "HowTo" +section: "Developers" +lastUpdated: "2026-05-29" +--- + +# Quick start + +import { Callout } from 'nextra/components' + +A minimal, framework-free example. Connect to the mixnet, subscribe to incoming messages, send a message to yourself, disconnect. + +## Installation + +```bash +npm install @nymproject/sdk-full-fat +``` + +`sdk-full-fat` inlines the WASM and Web Worker as Base64, so no bundler configuration is needed. For smaller bundles, install `@nymproject/sdk` instead and follow the [Bundling guide](/developers/typescript/bundling/bundling). + +## Send and receive + +```ts +import { createNymMixnetClient } from '@nymproject/sdk-full-fat'; + +const nymApiUrl = 'https://validator.nymtech.net/api'; + +const nym = await createNymMixnetClient(); + +nym.events.subscribeToConnected((e) => { + console.log('Connected:', e.args.address); +}); + +nym.events.subscribeToTextMessageReceivedEvent((e) => { + console.log('Received:', e.args.payload); +}); + +await nym.client.start({ + clientId: crypto.randomUUID(), + nymApiUrl, + forceTls: true, // WSS to entry gateway, required on HTTPS pages +}); + +const selfAddress = nym.client.selfAddress(); +await nym.client.send({ + payload: { message: 'hello mixnet', mimeType: 'text/plain' }, + recipient: selfAddress, +}); + +// ... receive event fires, message logged ... + +await nym.client.stop(); +``` + +That's the whole loop: subscribe, start, send, receive, stop. The mixnet handshake takes a few seconds on first run; subsequent calls in the same session reuse the client. + +## Anonymous replies (SURBs) + +Every outgoing message carries Single Use Reply Blocks by default. The recipient gets an opaque `sender_tag` and can reply without learning the sender's address: + +```ts +nym.events.subscribeToTextMessageReceivedEvent(async (e) => { + if (e.args.senderTag) { + await nym.client.replyWithSurb({ + senderTag: e.args.senderTag, + payload: { message: 'hello back, anonymously', mimeType: 'text/plain' }, + }); + } +}); +``` + + +The Nym mixnet has no persistent connections and no guaranteed ordering. The SDK abstracts this, but it shapes the patterns you can build: request/response works (via SURBs), but assumptions like "the third message will arrive third" don't hold. + + +## Where to go next + +- [TypeScript SDK landing](/developers/typescript): package variants, smart contracts, full options surface. +- [TypeDoc reference](/developers/typescript/api/sdk/globals): generated API docs for `@nymproject/sdk`. +- [Bundling](/developers/typescript/bundling/bundling): Webpack and esbuild configurations for the non-full-fat variants. + + +If you encounter a persistent gateway client error, open your browser console, go to the "Application" tab, and delete the databases listed under "IndexedDB". The SDK persists client identity there; clearing it forces a fresh handshake. + diff --git a/documentation/docs/pages/developers/typescript/examples/nym-smart-contracts.mdx b/documentation/docs/pages/developers/typescript/smart-contracts.mdx similarity index 100% rename from documentation/docs/pages/developers/typescript/examples/nym-smart-contracts.mdx rename to documentation/docs/pages/developers/typescript/smart-contracts.mdx diff --git a/documentation/docs/pages/network/overview/choosing-a-mode.md b/documentation/docs/pages/network/overview/choosing-a-mode.md index 9523b3c1aa..aa9e75a787 100644 --- a/documentation/docs/pages/network/overview/choosing-a-mode.md +++ b/documentation/docs/pages/network/overview/choosing-a-mode.md @@ -52,4 +52,4 @@ Your App --> Entry --> Mix Nodes --> Exit --> Internet Your App --> Entry --> Mix Nodes --> Exit --> Nym Client ``` -See the [integration overview](/developers/integrations) for guidance on choosing between them. +See the [developer overview](/developers) for guidance on choosing between them. diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index 27084582fe..e8b07ba953 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -313,7 +313,7 @@ The outcome of [NIP-10: Nym Exit Policy Update – Opening Ports for Dash, SIP, - [New documentation logic to `network/`, `developers/` and `apis/`](https://github.com/nymtech/nym/pull/6494) according to the [diataxis.fr](https://diataxis.fr/) framework, making basis for adding Lewes Protocol documentation. Additionally developer docs now include tutorials for the [Rust SDK modules](/developers/rust), and documentation on the `stream` [Mixnet module](/developers/rust/mixnet). See the pages at: - [Network docs](/network) - - [Developer docs](/developers/integrations) + - [Developer docs](/developers) - [APIs docs](/apis/introduction) #### Update Nym exit policy @@ -1006,7 +1006,7 @@ cargo Profile: release - [Typescript SDK 1.4.1](https://github.com/nymtech/nym/pull/6146): This PR is a new release of the Typescript SDK, `mixFetch` and `WASM` client. It also removes the Harbour Master client from `mixFetch`, replacing it with the Nym API's described endpoint for nym-nodes -- [Overhauled **developer integrations** pages](/developers/integrations) explaining the different restrictions for the different SDK options on offer +- [Overhauled **developer integrations** pages](/developers) explaining the different restrictions for the different SDK options on offer - [Fixed `mixFetch` and `WASM Client` playground + examples](/developers/typescript): new versions of the Typescript SDK and `mixFetch` have been published, examples and live playground have been updated accordingly diff --git a/documentation/docs/pnpm-lock.yaml b/documentation/docs/pnpm-lock.yaml index 39200508fb..ae3f4bb682 100644 --- a/documentation/docs/pnpm-lock.yaml +++ b/documentation/docs/pnpm-lock.yaml @@ -40,7 +40,7 @@ importers: version: 2.18.1 '@cosmos-kit/keplr': specifier: ^2.6.9 - version: 2.17.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.23.7)(starknet@8.9.2)(typescript@5.9.3)(zod@3.25.76) + version: 2.17.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.11.0)(starknet@8.9.2)(typescript@5.9.3)(zod@3.25.76) '@cosmos-kit/keplr-extension': specifier: ^2.7.9 version: 2.17.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(starknet@8.9.2) @@ -77,9 +77,18 @@ importers: '@nymproject/contract-clients': specifier: '>=1.2.4-rc.2 || ^1' version: 1.4.1 - '@nymproject/mix-fetch-full-fat': - specifier: ^1.4.3 - version: 1.4.3 + '@nymproject/mix-dns': + specifier: ^0.1.0 + version: 0.1.0 + '@nymproject/mix-fetch': + specifier: ^2.0.0 + version: 2.0.0 + '@nymproject/mix-tunnel': + specifier: ^0.1.0 + version: 0.1.0 + '@nymproject/mix-websocket': + specifier: ^0.1.0 + version: 0.1.0 '@nymproject/sdk-full-fat': specifier: '>=1.5.1-rc.0 || ^1.4.1' version: 1.4.1 @@ -1775,8 +1784,17 @@ packages: '@nymproject/contract-clients@1.4.1': resolution: {integrity: sha512-HuJZ4Hv+Rl6ZZEtCHKgurNLJapM+QQRJlGkevFH2a4UdqUqF9omUkUi3AVes4679dPoSFgvA7plyVSDBdbgV6w==} - '@nymproject/mix-fetch-full-fat@1.4.3': - resolution: {integrity: sha512-r3WVZDDFv+eFWPxhkMDg2VvLWd3ws1OK6kFT6bEt6/qTpfA21vV2MouuuyVkBy8DEUKqyU4z/8MiVFxfpkWlsg==} + '@nymproject/mix-dns@0.1.0': + resolution: {integrity: sha512-JIbd71MwaorMz1QJ1UfGVSdWHTayCmNvsjA1D5I7rhSZGCrul7wJ7lvVP8zv2PBu3L+DXF4TfzkVDe1kkc9uOQ==} + + '@nymproject/mix-fetch@2.0.0': + resolution: {integrity: sha512-dTHPMpd1Zhj4ZauN8vnxOq7gzO3N/ikaQoNvWwl1Xq6niQxqoLw/gKbsKxwrgvbRodhjkKrCqW++0zumTfi7pw==} + + '@nymproject/mix-tunnel@0.1.0': + resolution: {integrity: sha512-olm+nue1rW7PyR9hMHbOdEZ/ytRXjEqHh1kGPfw8ZWKV/e+5Gh43mngyxFrZYqlQMVntlRXwkiJkB9bYWFTxxw==} + + '@nymproject/mix-websocket@0.1.0': + resolution: {integrity: sha512-h0xFrhUrNjJ4xF337fU/xx8cG64EJyivqOXhNT4jNoqBMFmO/p6jYaNleJZ6NdzabV/JfqHovCUnt/9qPdQc/A==} '@nymproject/sdk-full-fat@1.4.1': resolution: {integrity: sha512-dh5bvMUj3m8nEssvO8Nl66WpcJAjwRZrGNwqfczJWLG4nX3Vt95tPLv4v0/Z1W3DQWQFW6WmEPPYHNjl18V/fA==} @@ -7449,16 +7467,16 @@ snapshots: - uploadthing - utf-8-validate - '@cosmos-kit/keplr-mobile@2.17.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.23.7)(starknet@8.9.2)(typescript@5.9.3)(zod@3.25.76)': + '@cosmos-kit/keplr-mobile@2.17.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.11.0)(starknet@8.9.2)(typescript@5.9.3)(zod@3.25.76)': dependencies: '@chain-registry/keplr': 1.74.507 '@cosmjs/amino': 0.32.4 '@cosmjs/proto-signing': 0.32.4 '@cosmos-kit/core': 2.18.1 '@cosmos-kit/keplr-extension': 2.17.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(starknet@8.9.2) - '@cosmos-kit/walletconnect': 2.15.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/types@2.23.7)(typescript@5.9.3)(zod@3.25.76) + '@cosmos-kit/walletconnect': 2.15.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/types@2.11.0)(typescript@5.9.3)(zod@3.25.76) '@keplr-wallet/provider-extension': 0.12.313(starknet@8.9.2) - '@keplr-wallet/wc-client': 0.12.313(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.23.7)(starknet@8.9.2) + '@keplr-wallet/wc-client': 0.12.313(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.11.0)(starknet@8.9.2) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -7489,10 +7507,10 @@ snapshots: - utf-8-validate - zod - '@cosmos-kit/keplr@2.17.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.23.7)(starknet@8.9.2)(typescript@5.9.3)(zod@3.25.76)': + '@cosmos-kit/keplr@2.17.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.11.0)(starknet@8.9.2)(typescript@5.9.3)(zod@3.25.76)': dependencies: '@cosmos-kit/keplr-extension': 2.17.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(starknet@8.9.2) - '@cosmos-kit/keplr-mobile': 2.17.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.23.7)(starknet@8.9.2)(typescript@5.9.3)(zod@3.25.76) + '@cosmos-kit/keplr-mobile': 2.17.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.11.0)(starknet@8.9.2)(typescript@5.9.3)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -7634,13 +7652,13 @@ snapshots: - uploadthing - utf-8-validate - '@cosmos-kit/walletconnect@2.15.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/types@2.23.7)(typescript@5.9.3)(zod@3.25.76)': + '@cosmos-kit/walletconnect@2.15.1(@cosmjs/amino@0.32.4)(@cosmjs/proto-signing@0.32.4)(@walletconnect/types@2.11.0)(typescript@5.9.3)(zod@3.25.76)': dependencies: '@cosmjs/amino': 0.32.4 '@cosmjs/proto-signing': 0.32.4 '@cosmos-kit/core': 2.18.1 '@walletconnect/sign-client': 2.23.7(typescript@5.9.3)(zod@3.25.76) - '@walletconnect/types': 2.23.7 + '@walletconnect/types': 2.11.0 '@walletconnect/utils': 2.23.7(typescript@5.9.3)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: @@ -8171,12 +8189,12 @@ snapshots: big-integer: 1.6.52 utility-types: 3.11.0 - '@keplr-wallet/wc-client@0.12.313(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.23.7)(starknet@8.9.2)': + '@keplr-wallet/wc-client@0.12.313(@walletconnect/sign-client@2.23.7(typescript@5.9.3)(zod@3.25.76))(@walletconnect/types@2.11.0)(starknet@8.9.2)': dependencies: '@keplr-wallet/provider': 0.12.313(starknet@8.9.2) '@keplr-wallet/types': 0.12.313(starknet@8.9.2) '@walletconnect/sign-client': 2.23.7(typescript@5.9.3)(zod@3.25.76) - '@walletconnect/types': 2.23.7 + '@walletconnect/types': 2.11.0 buffer: 6.0.3 deepmerge: 4.3.1 long: 5.3.2 @@ -9533,7 +9551,19 @@ snapshots: '@nymproject/contract-clients@1.4.1': {} - '@nymproject/mix-fetch-full-fat@1.4.3': {} + '@nymproject/mix-dns@0.1.0': + dependencies: + '@nymproject/mix-tunnel': 0.1.0 + + '@nymproject/mix-fetch@2.0.0': + dependencies: + '@nymproject/mix-tunnel': 0.1.0 + + '@nymproject/mix-tunnel@0.1.0': {} + + '@nymproject/mix-websocket@0.1.0': + dependencies: + '@nymproject/mix-tunnel': 0.1.0 '@nymproject/sdk-full-fat@1.4.1': {} diff --git a/documentation/docs/public/llms-full.txt b/documentation/docs/public/llms-full.txt index 34147b560c..2d8a0d5cfe 100644 --- a/documentation/docs/public/llms-full.txt +++ b/documentation/docs/public/llms-full.txt @@ -1,8 +1,8 @@ # Nym Documentation @version: 1.20.4 -@generated: 2026-05-11 -@pages: 148 +@generated: 2026-06-09 +@pages: 163 @source: https://github.com/nymtech/nym/tree/develop/documentation/docs --- @@ -13,7 +13,7 @@ url: https://nym.com/docs/network # The Nym Network -The Nym Network is decentralized privacy infrastructure that protects against **network-level** surveillance. Unlike tools that focus on encrypting message content, Nym protects the metadata surrounding communication: who talks to whom, when, how often, and how much. This metadata is sufficient for observers to map relationships and build behavioural profiles even without access to any message content. See [The Privacy Problem](/network/overview/privacy-problem) for a fuller treatment. +The Nym Network is decentralised privacy infrastructure that protects against **network-level** surveillance. Unlike tools that focus on encrypting message content, Nym protects the metadata surrounding communication: who talks to whom, when, how often, and how much. This metadata is sufficient for observers to map relationships and build behavioural profiles even without access to any message content. See [The Privacy Problem](/network/overview/privacy-problem) for a fuller treatment. Nym offers two operating modes with different privacy/performance trade-offs, both available through [NymVPN](https://nymvpn.com). Developers can also integrate Mixnet mode directly via the [Nym SDKs](/developers). See [Choosing a Mode](/network/overview/choosing-a-mode) for guidance on which fits a given threat model. @@ -31,7 +31,7 @@ The [Nym SDKs](/developers) allow developers to embed mixnet functionality direc ## Paying for privacy without losing it -A fundamental weakness of traditional VPNs is that payment records can deanonymize users, since most providers link sessions to account IDs. Nym addresses this with **zk-nyms**: zero-knowledge anonymous credentials that prove payment without revealing any other information. Each credential covers a small chunk of bandwidth and is unlinkable to any other. +A fundamental weakness of traditional VPNs is that payment records can deanonymise users, since most providers link sessions to account IDs. Nym addresses this with **zk-nyms**: zero-knowledge anonymous credentials that prove payment without revealing any other information. Each credential covers a small chunk of bandwidth and is unlinkable to any other. When you pay for NymVPN, your payment is converted into a credential that can be split and re-randomized. Each Gateway connection uses a fresh, unlinkable proof; the Gateway verifies that you have paid without learning who you are. Your subscription cannot be linked to your network activity, even by infrastructure operators. @@ -49,7 +49,7 @@ url: https://nym.com/docs/network # The Nym Network -The Nym Network is decentralized privacy infrastructure that protects against **network-level** surveillance. Unlike tools that focus on encrypting message content, Nym protects the metadata surrounding communication: who talks to whom, when, how often, and how much. This metadata is sufficient for observers to map relationships and build behavioural profiles even without access to any message content. See [The Privacy Problem](/network/overview/privacy-problem) for a fuller treatment. +The Nym Network is decentralised privacy infrastructure that protects against **network-level** surveillance. Unlike tools that focus on encrypting message content, Nym protects the metadata surrounding communication: who talks to whom, when, how often, and how much. This metadata is sufficient for observers to map relationships and build behavioural profiles even without access to any message content. See [The Privacy Problem](/network/overview/privacy-problem) for a fuller treatment. Nym offers two operating modes with different privacy/performance trade-offs, both available through [NymVPN](https://nymvpn.com). Developers can also integrate Mixnet mode directly via the [Nym SDKs](/developers). See [Choosing a Mode](/network/overview/choosing-a-mode) for guidance on which fits a given threat model. @@ -67,7 +67,7 @@ The [Nym SDKs](/developers) allow developers to embed mixnet functionality direc ## Paying for privacy without losing it -A fundamental weakness of traditional VPNs is that payment records can deanonymize users, since most providers link sessions to account IDs. Nym addresses this with **zk-nyms**: zero-knowledge anonymous credentials that prove payment without revealing any other information. Each credential covers a small chunk of bandwidth and is unlinkable to any other. +A fundamental weakness of traditional VPNs is that payment records can deanonymise users, since most providers link sessions to account IDs. Nym addresses this with **zk-nyms**: zero-knowledge anonymous credentials that prove payment without revealing any other information. Each credential covers a small chunk of bandwidth and is unlinkable to any other. When you pay for NymVPN, your payment is converted into a credential that can be split and re-randomized. Each Gateway connection uses a fresh, unlinkable proof; the Gateway verifies that you have paid without learning who you are. Your subscription cannot be linked to your network activity, even by infrastructure operators. @@ -95,7 +95,7 @@ The Nym Network is a privacy infrastructure that protects metadata: not just mes ## Network Components -All traffic-routing infrastructure runs on [Nym Nodes](/network/infrastructure/nym-nodes), a single binary that operators configure to serve as an Entry Gateway, Mix Node, or Exit Gateway depending on their setup. Network coordination, token bonding, and the distributed credential system all live on the [Nyx blockchain](/network/infrastructure/nyx), a Cosmos SDK chain whose on-chain topology registry eliminates the need for a centralised directory server. +All traffic-routing infrastructure runs on [Nym Nodes](/network/infrastructure/nym-nodes), a single binary that operators configure to serve as an Entry Gateway, Mix Node, or Exit Gateway depending on their setup. Network coordination, token bonding, and the distributed credential system all live on the [Nyx blockchain](/network/infrastructure/nyx), a Cosmos SDK chain whose on-chain topology registry removes the need for a centralised directory server. --- title: The Privacy Problem: Why Metadata Matters @@ -153,7 +153,7 @@ Both modes run on the same Nym infrastructure but defend against different threa - Latency matters: browsing, streaming, downloads, video calls - Your concern is ISPs, advertisers, and websites tracking you, not nation-state surveillance -- You want decentralized trust and payment privacy without the overhead of mixing +- You want decentralised trust and payment privacy without the overhead of mixing ## Use Mixnet mode when @@ -177,7 +177,7 @@ Your App --> Entry --> Mix Nodes --> Exit --> Internet Your App --> Entry --> Mix Nodes --> Exit --> Nym Client ``` -See the [integration overview](/developers/integrations) for guidance on choosing between them. +See the [developer overview](/developers) for guidance on choosing between them. --- title: Nym vs VPNs, Tor, I2P, and E2EE @@ -203,7 +203,7 @@ Nym's mixnet mode goes further by adding timing obfuscation and cover traffic, w Nym's mixnet addresses this by adding random delays at each Mix Node to break timing correlations, cover traffic so observers can't tell when real communication is occurring, per-packet routing rather than Tor's per-session circuits (so there's no long-lived path to observe), and a blockchain-based topology instead of Tor's centralised directory authority. -The tradeoff is latency: Tor is faster because it doesn't add mixing delays, so it may be a better fit for general browsing where timing protection isn't needed. Nym's mixnet is designed for threat models where the adversary can perform traffic analysis. +The trade-off is latency: Tor is faster because it doesn't add mixing delays, so it may be a better fit for general browsing where timing protection isn't needed. Nym's mixnet is designed for threat models where the adversary can perform traffic analysis. ## Nym vs I2P @@ -229,13 +229,13 @@ For a practical breakdown of when to use dVPN vs Mixnet mode, see [Choosing a Mo --- title: dVPN Mode -description: How Nym's decentralized VPN mode routes traffic through two independent gateways, splitting trust so no single operator sees both your identity and destination. +description: How Nym's decentralised VPN mode routes traffic through two independent gateways, splitting trust so no single operator sees both your identity and destination. url: https://nym.com/docs/network/dvpn-mode --- # dVPN Mode -dVPN mode is a 2-hop decentralized VPN available through [NymVPN](https://nymvpn.com). Traffic is routed through two independent gateways rather than a single VPN provider's server, so no single operator ever sees both who you are and what you're doing. +dVPN mode is a 2-hop decentralised VPN available through [NymVPN](https://nymvpn.com). Traffic is routed through two independent gateways rather than a single VPN provider's server, so no single operator ever sees both who you are and what you're doing. ## How it works @@ -267,14 +267,12 @@ Added latency is comparable to traditional VPNs, and WireGuard keeps cryptograph --- title: dVPN Protocol Stack and Encryption -description: Technical details of Nym dVPN mode's protocol layers: nested WireGuard tunnels, split-knowledge architecture, and packet format tradeoffs. +description: Technical details of Nym dVPN mode's protocol layers: nested WireGuard tunnels, split-knowledge architecture, and packet format trade-offs. url: https://nym.com/docs/network/dvpn-mode/protocol --- # dVPN Protocol -Cryptographic details on this page will be updated for the Lewes Protocol release. For the current algorithm overview, see the [Nym Trust Center: Cryptography](https://nym.com/trust-center/cryptography). - This page covers the technical details of dVPN mode's protocol stack and encryption. ## Protocol layers @@ -301,7 +299,7 @@ Both tunnels use standard WireGuard cryptography: Curve25519 for key exchange, C ## Packet format -dVPN mode uses standard WireGuard packet framing: packets are not padded to a uniform size. Packet sizes may vary and could in principle leak information about content types (video streams have different size patterns than text messages). This is a deliberate tradeoff: uniform padding would add overhead and reduce throughput, which conflicts with dVPN mode's goal of low-latency, high-throughput connectivity. For uniform packet sizes, use [mixnet mode](/network/mixnet-mode), which wraps all traffic in fixed-size Sphinx packets. +dVPN mode uses standard WireGuard packet framing: packets are not padded to a uniform size. Packet sizes may vary and could in principle leak information about content types (video streams have different size patterns than text messages). This is a deliberate trade-off: uniform padding would add overhead and reduce throughput, which conflicts with dVPN mode's goal of low-latency, high-throughput connectivity. For uniform packet sizes, use [mixnet mode](/network/mixnet-mode), which wraps all traffic in fixed-size Sphinx packets. ## Connection lifecycle @@ -404,8 +402,6 @@ Each Mix Node strips one layer of [Sphinx](/network/cryptography/sphinx) encrypt The three mixing layers add additional latency. This is acceptable for messaging, file transfers, and most API calls, but unsuitable for real-time applications like video calling. For those, [dVPN mode](/network/dvpn-mode) is more appropriate. -Updated latency measurements will be published after the Lewes Protocol release. - ## Further reading The following pages cover mixnet internals in detail: @@ -424,7 +420,7 @@ url: https://nym.com/docs/network/mixnet-mode/loopix # Loopix Design -The Nym mixnet is based on the [Loopix](https://arxiv.org/pdf/1703.00536) design, with modifications for decentralized operation and economic incentives. +The Nym mixnet is based on the [Loopix](https://arxiv.org/pdf/1703.00536) design, with modifications for decentralised operation and economic incentives. ## The insight @@ -454,7 +450,7 @@ Loop traffic ensures minimum anonymity even when few users are active, hides whe ## Nym's modifications -The Nym implementation extends Loopix in several ways: replacing the trusted directory server with the Nyx blockchain for decentralized topology management, incentivising node operation with NYM token rewards rather than relying on volunteers, and adding zk-nyms for privacy-preserving payment, which the original academic design did not address. +The Nym implementation extends Loopix in several ways: replacing the trusted directory server with the Nyx blockchain for decentralised topology management, incentivising node operation with NYM token rewards rather than relying on volunteers, and adding zk-nyms for privacy-preserving payment, which the original academic design did not address. ## Security guarantees @@ -638,7 +634,7 @@ Mix nodes also generate their own cover traffic, ensuring minimum traffic levels Traffic follows a Poisson process with a configurable rate parameter. Inter-packet times are exponentially distributed: random, but with a known average rate. This distribution provides maximum entropy (uncertainty) for a given mean rate, which translates to optimal privacy properties. -## Tradeoffs +## Trade-offs More cover traffic provides better unobservability but uses more bandwidth and, when zk-nyms are enabled, more credential value. Less cover traffic reduces costs but may allow some inference about activity patterns. @@ -646,7 +642,7 @@ The default parameters balance privacy and resource usage. Applications with hei ## What cover traffic defeats -Cover traffic prevents volume analysis (how much you communicate), timing analysis (when you communicate), and behavioral profiling (your communication patterns over time). Combined with packet mixing, it ensures that even an adversary watching the entire network cannot learn about your communication behavior with currently known methods. +Cover traffic prevents volume analysis (how much you communicate), timing analysis (when you communicate), and behavioural profiling (your communication patterns over time). Combined with packet mixing, this means that even an adversary watching the entire network cannot learn about your communication behaviour with currently known methods. --- title: Packet Mixing and Random Delays @@ -696,8 +692,6 @@ With three Mix Node layers, each applying random delays, the overall effect is t These delays account for the additional latency of mixnet mode relative to dVPN mode. -Updated latency measurements will be published after the Lewes Protocol release. - ## Combined with cover traffic Mixing and cover traffic are complementary. Cover traffic ensures there are always packets to mix, even during low activity, while mixing ensures that real and cover packets become interleaved and indistinguishable. Together they provide both unlinkability and unobservability. @@ -730,7 +724,7 @@ SURB validity is tied to key rotation. Node keys rotate on an odd/even schedule ## SURB replenishment -If Bob's reply is larger than the available SURBs can carry, he uses one SURB to request more. Alice receives the request, generates additional SURBs, and sends them to Bob. This adds round-trip latency but ensures conversations can continue regardless of reply size. +If Bob's reply is larger than the available SURBs can carry, he uses one SURB to request more. Alice receives the request, generates additional SURBs, and sends them to Bob. This adds round-trip latency but lets conversations continue regardless of reply size. ```mermaid --- @@ -755,7 +749,7 @@ sequenceDiagram ## Sender tags -For sessions with multiple messages, Alice includes a randomly generated sender tag with her SURBs. This helps Bob organize SURBs from multiple conversations without revealing anything about Alice's identity; the tag is random and unlinkable to her address. +For sessions with multiple messages, Alice includes a randomly generated sender tag with her SURBs. This helps Bob organise SURBs from multiple conversations without revealing anything about Alice's identity; the tag is random and unlinkable to her address. ## Security considerations @@ -777,7 +771,7 @@ The Nym Network relies on several cryptographic systems working together. This s ## What's covered -[Sphinx Packets](/network/cryptography/sphinx) explains the packet format that enables layered encryption and anonymous routing. Each Sphinx packet contains routing information encrypted in layers, where each hop can only decrypt its own layer. +[Sphinx Packets](/network/cryptography/sphinx) explains the packet format used for layered encryption and anonymous routing. Each Sphinx packet contains routing information encrypted in layers, where each hop can only decrypt its own layer. [zk-nyms](/network/cryptography/zk-nym) covers the anonymous credential system that separates payment from usage. This is how you can pay for network access without that payment being linkable to your activity. @@ -789,7 +783,7 @@ url: https://nym.com/docs/network/cryptography/sphinx # Sphinx -Sphinx is the cryptographic packet format used for all mixnet traffic. It provides layered encryption where each hop can only decrypt its own routing information, ensuring that no single node knows both the source and destination of a packet. +Sphinx is the cryptographic packet format used for all mixnet traffic. It provides layered encryption where each hop can only decrypt its own routing information, so no single node knows both the source and destination of a packet. ## How Sphinx works @@ -801,7 +795,7 @@ At each hop, the node uses its private key to decrypt its layer, revealing the a All Sphinx packets have a fixed payload size of 2048 bytes. This uniformity is critical: if packets varied in size, nodes could infer their position in the route or correlate packets by size. -The packet contains a header with encrypted routing information for each hop, HMACs to verify integrity at each layer, and the encrypted payload. The header uses a clever "onion" structure where processing at each hop reveals only the next hop's information while maintaining constant size through padding. +The packet contains a header with encrypted routing information for each hop, HMACs to verify integrity at each layer, and the encrypted payload. The header uses an "onion" structure where processing at each hop reveals only the next hop's information while maintaining constant size through padding. ## Integrity verification @@ -838,9 +832,9 @@ url: https://nym.com/docs/network/cryptography/zk-nym The zk-nym scheme enables the creation and use of unlinkable, rerandomisable anonymous access credentials that are 'spent' with Gateways in order to anonymously prove that someone has paid for Mixnet access. This implementation incorporates elements of both the [Coconut Credential](https://arxiv.org/pdf/1802.07344) and [Offline Ecash](https://arxiv.org/pdf/2303.08221) schemes. -As outlined in the [overview](./zk-nym/zk-nym-overview) on the next page, zk-nyms allow for users to pay for Mixnet access in a manner that is **unlinkable to their payment account**; even with pseudonymous cryptocurrencies or fiat. This solves one of the fundamental privacy problems with the majority of VPNs and dVPNs in production today: the linkability of a user's session with their payment information, which can in the majority of cases be easily used to deanonymise them, either at the behest of an authority or by the service operators themselves. +As outlined in the [overview](./zk-nym/zk-nym-overview) on the next page, zk-nyms allow users to pay for Mixnet access in a way that is **unlinkable to their payment account**, even with pseudonymous cryptocurrencies or fiat. This solves one of the fundamental privacy problems with most VPNs and dVPNs in production today: the linkability of a user's session with their payment information, which can in most cases be used to deanonymise them, either at the behest of an authority or by the service operators themselves. -> The current zk-nym scheme is non-generic in that it is only used for gating Mixnet access. A generic scheme based on zk-nyms is being actively researched in order to facilitate more generic and customisable anonymous credentials for other applications and services. +> The current zk-nym scheme is non-generic in that it is only used for gating Mixnet access. A generic scheme based on zk-nyms is being actively researched, to support more generic and customisable anonymous credentials for other applications and services. ## Motivations Most of the time, when we build system security, we think of _who_ questions: @@ -861,7 +855,7 @@ The zk-nym scheme allows for this move to take place. Credentials are generated ### Re-randomisation vs pseudonymity We stand on the shoulders of giants. Ten years ago, Bitcoin showed the way forward by allowing people to control resource access without recourse to _who_ questions. Rather, in Bitcoin and succeeding blockchains, a private key proves a _right to use_. -But as we can now see, private keys in blockchain systems act only as a minor barrier to finding out _who_ is accessing resources. A Bitcoin or Ethereum private key is effectively a long-lived pseudonym which is easily traceable through successive transactions. +But as we can now see, private keys in blockchain systems act only as a minor barrier to finding out _who_ is accessing resources. A Bitcoin or Ethereum private key is a long-lived pseudonym that is easily traceable through successive transactions. **zk-nyms allows us to build truly private systems rather than pseudonymous ones.** @@ -874,11 +868,11 @@ Let's say you have a `message` with the content `This credential controls X` in 2. _Re-randomizable signatures_ - take a signature, and generate a brand new signature that is valid for the same underlying message `This credential controls X`. The new bitstring in the re-randomized signature is equivalent to the original signature but not linkable to it. So a user can generate multiple zk-nyms from a single credential source, unlinkable to any previous "shown" zk-nym. But the underlying content of the re-randomized credential is the same (including for things like double-spend protection). This once again protects the user against the signer, because the signer can't trace the signed message that they gave back to the user when it is presented. It also protects the user against the relying party that accepts the signed credential. The user can generate multiple re-randomized credentials repeatedly, and although the underlying message is the same in all cases, there's no way of tracking them by watching the user present the same credential multiple times. -3. _Selective disclosure of attributes_ - allows someone with the public key to verify some, but not all, parts of a message. So you could for instance selectively reveal parts of a signed message to some people, but not to others. This is a very powerful property of the scheme which is to be explored more in future work, potentially leading to diverse applications: voting systems, anonymous currency, privacy-friendly KYC systems, etc. +3. _Selective disclosure of attributes_ - allows someone with the public key to verify some, but not all, parts of a message. So you could for instance selectively reveal parts of a signed message to some people, but not to others. This property of the scheme is to be explored more in future work, with potential applications including voting systems, anonymous currency, and privacy-friendly KYC systems. -4. _[Threshold issuance](https://en.wikipedia.org/wiki/Threshold_cryptosystem)_ - allows signature generation to be split up across multiple nodes and decentralized, so that either all signers need to sign (_n of n_ where _n_ is the number of signers) or only a threshold number of signers need to sign a message (_t of n_ where _t_ is the threshold value). +4. _[Threshold issuance](https://en.wikipedia.org/wiki/Threshold_cryptosystem)_ - allows signature generation to be split up across multiple nodes and decentralised, so that either all signers need to sign (_n of n_ where _n_ is the number of signers) or only a threshold number of signers need to sign a message (_t of n_ where _t_ is the threshold value). -Taken together, these properties provide privacy for applications when it comes to generating and using signatures for cryptographic claims. If you compare it to existing tech, the closest analogy in conventional systems is a decentralized, privacy-preserving [JWT](https://jwt.io/). +Taken together, these properties provide privacy for applications when generating and using signatures for cryptographic claims. The closest analogy in conventional systems is a decentralised, privacy-preserving [JWT](https://jwt.io/). --- title: Generating and using zk-nym anonymous credentials @@ -887,7 +881,7 @@ url: https://nym.com/docs/network/cryptography/zk-nym/zk-nym-overview # Generating and using zk-nym anonymous credentials - zk-nyms are already used in production by [NymVPN](https://nymvpn.com) to unlink subscription payments from network activity. The entire credential lifecycle described on this page (key generation, issuance, spending) happens transparently within the NymVPN application. SDK integrations currently connect to the Mixnet without requiring credentials. + zk-nyms are already used in production by [NymVPN](https://nymvpn.com) to unlink subscription payments from network activity. The entire credential lifecycle described on this page (key generation, issuance, spending) happens inside the NymVPN application without user involvement. SDK integrations currently connect to the Mixnet without requiring credentials. Generation of zk-nyms involves the following actors / pieces of infrastructure: - **Requester needing a zk-nym** for example a single user using the NymVPN app, or a company purchasing zk-nyms to distribute to their app users, in the instance of an app integrating a Mixnet client via one of the SDKs. The Requester is represented by a Bech32 address on the Nyx blockchain. @@ -899,13 +893,13 @@ Generation happens in 3 distinct stages: - Issue credential - Generate unlinkable zk-nyms for Nym Network access -From the Requester's perspective this happens transparently, producing an unlinkable, rerandomisable anonymous proof-of-payment credential (a zk-nym) that grants Mixnet access without linking usage to payment information. A single credential can be split into multiple smaller zk-nyms, so a Requester purchases bandwidth in bulk and spends it incrementally across different ingress Gateways as needed. +From the Requester's perspective this happens without user involvement, producing an unlinkable, rerandomisable anonymous proof-of-payment credential (a zk-nym) that grants Mixnet access without linking usage to payment information. A single credential can be split into multiple smaller zk-nyms, so a Requester purchases bandwidth in bulk and spends it incrementally across different ingress Gateways as needed. ## Key Generation & Payment -- First, a Cosmos [Bech32 address](https://docs.cosmos.network/sdk/latest/guides/reference/bech32#performance-address-caching) is created for the Requester. This is used to identify themselves when interacting with the OrderAPI via signed authentication tokens. **This is the only identity that the OrderAPI is able to see, and is not able to link this to the zk-nyms that will be generated.** This identity never leaves the Requester's device and there is no email or any personal details needed for signup. If a Requester is simply 'topping up' their subscription, the creation of the address is skipped as it already exists. +- First, a Cosmos [Bech32 address](https://docs.cosmos.network/sdk/latest/guides/reference/bech32#performance-address-caching) is created for the Requester. This is used to identify themselves when interacting with the OrderAPI via signed authentication tokens. **This is the only identity the OrderAPI sees, and it cannot link this to the zk-nyms that will be generated.** This identity never leaves the Requester's device and there is no email or personal details needed for signup. If a Requester is 'topping up' their subscription, the creation of the address is skipped as it already exists. - The Requester also generates an ed25519 keypair: this is used to identify and authenticate them in the case of using zk-nyms across several devices as an individual user. However, **this is never used in the clear**: these keys are used as private attribute values within generated credentials which are verified via zero-knowledge and not publicly exposed. -- The Requester can then interact with various payment backends to pay for their zk-nyms with crypto, fiat options, or natively with NYM tokens. +- The Requester can then interact with payment backends to pay for their zk-nyms with crypto, fiat options, or natively with NYM tokens. - Payment options will trigger the OrderAPI. This will: - Create a swap for `` to `NYM` tokens. - Deposit these tokens with the NymAPI Quorum via a CosmWasm smart contract deployed on the Nyx blockchain. @@ -943,9 +937,9 @@ Each ticket will not be valid for the entire amount of data that the ticketbook The `nym-cli` examples below are for illustration only and do not reflect how credentials are accessed in production. The specific figures (ticket counts, bandwidth amounts) are illustrative; production values may differ, though individual ticket sizes are uniform across the network. ## Why a 'ticketbook', not individual 'tickets', and why not spend them all at once? -This is to account for the need for a client to change their ingress Gateway, either because the Gateway itself has gone down / is not offering the required bandwidth, or because a user might simply want to split their traffic across multiple Gateways for extra privacy. +This is to account for the need for a client to change their ingress Gateway, either because the Gateway itself has gone down or is not offering the required bandwidth, or because a user might want to split their traffic across multiple Gateways for extra privacy. -Clients are therefore not tied to a particular Gateway they have spent their entire subscription with. If an ingress Gateway goes down, or the client simply wants to use a different one, remaining tickets can be spent with any other Gateway. +Clients are therefore not tied to a particular Gateway they have spent their entire subscription with. If an ingress Gateway goes down, or the client wants to use a different one, remaining tickets can be spent with any other Gateway. Going back to the `nym-cli` tool to illustrate this; we can generate multiple unlinkable tickets from a single ticketbook aggregated from PSCs: @@ -986,11 +980,11 @@ url: https://nym.com/docs/network/cryptography/zk-nym/unlinkability # Unlinkability -Each time a credential is requested by an ingress Gateway to prove that a client has purchased data to send through the Mixnet the Requester's device will produce a ticket. This is a rerandomised value that is able to be verified as being legitimate (in that it was created by a valid root ticketbook) but **not linked to any other tickets**, either previously generated or to be generated in the future. This feature also allows for a single ticketbook to allow access to be split across multiple ingress Gateways / connections and [incrementally spent](./rerandomise) over time. +Each time a credential is requested by an ingress Gateway to prove that a client has purchased data to send through the Mixnet, the Requester's device produces a ticket. This is a rerandomised value that can be verified as legitimate (in that it was created by a valid root ticketbook) but is **not linked to any other tickets**, either previously generated or to be generated in the future. This also allows a single ticketbook to be split across multiple ingress Gateways or connections and [incrementally spent](./rerandomise) over time. The functionality included in the following code block examples were added to the [nym-cli tool](/developers/tools/nym-cli) for illustrative purposes only: this is not necessarily how credentials will be accessed in the future. -The numbers used in this high level overview are for illustration purposes only. The figures used in production will potentially vary. Note that individual zkNym sizes will be uniform across the Network. +The numbers used in this high level overview are for illustration only. The figures used in production may vary. Individual zkNym sizes are uniform across the Network. ```sh ❯ ./nym-cli ecash generate-ticket --credential-storage storage.db --provider 6qidVK21zpHD298jdDa1RRpbRozP29ENVyqcSbm6hQrG --ticket-index=3 @@ -1003,7 +997,7 @@ PAYMENT FOR TICKET 3: VfZAuVRRHekQYMvFevNAZmPPuwMAfEhTBY8TXatBysbrNXAg8euEGPpJvdbhNfQSznBb9nRSeBUSVoNTToSA6Uj5dXmJ7oE2rCB439DarLMWHWYfQNhw6yhWJhcg6bt7ebBYTs3vVeQgSB5kYuifzJF4QQmK6uJyTNPvpV1J6V8M32PBkGT3JpVB3GUGZiksETf7TaF9wAhMo2QAMxw5ZvaQVve5ea7Mane6cfb2Gx69SRff5zDfEQvKqKnyyZje4SGZgWUeHWVLhRjg4KMTJ3JcsHxEqj2k5qeGeyBbgzcuEtCpYvaytsz7nuZGJsT4Z87gB5Zq4NGuDmekuN977eRJvua2dASNWeHiAzVyvnS7ARN5cdUjjYKYiWgHaYrHGsv26WTDeiu4U3sdJMrLHGFY5ihX7f8sTZqD6Wx5AWjQNbEtKaVHymDogfLcwGCC42gQ2yhKfPUaWJ8H4yMB65YBDXGjATaUzcDmJcZKx8g31j2uTVNSFUesd5CRNEEcTNW7cSFFCishCD3T4eV9SuyZyEXAZ48pazPzc1BysBNHEXQNUEtEAZTKmpghC2pihhfDub6LnMJPo9DDdhCULCbcWbGAPc1vPekPaWvk7wrUTGwp5xoNUhQLW3MeJzMvrMSsqLdursCKB4h4Tk272WCStCPQwAKMYoxjWvMzxoUTTWCkhLKHruMtsehRnai4vhu13jbui6ji1F389gfazm4ctth2s4Yw3H3SaPtRETBfZNvZ7n5UV1MD6Q3qin92gT65iqXEi4zRN3woYcK6ZehiSvgUksdEFAUSxNMgNXKtHEYDS6kA37tn5JdBa2Ex2jLudFfhg6JBM226ZKyj65o6feYPgbJAR3jMCmQRHe6DSFb4aH895EowNMjfGUhwhmnbYB1djp7iFXxPP7575NAerhxEQ1WFnxTfoX7pu1Vc9YZb5priCAVbATCaDkECJsdedM45Vx96Jc6E5NWqD98RhMsPimVJkSfYJmRxH9qugica6WonFFb2YLvXYyhoBA1VHBcRqZJ5KHitS5AegYSoYprUfubMzcYo2hGVEQkGKAsFq6jZgCsbJoGLXt3No317vcowB5f3hqT9FjASHAzW2j8uJ9RRzX7XtrPhArwx4EyPgYzrvgG7xcenoSgQt8poa7aYky56eZTKHVUZgUEt6St32MjcivMvmNdWiAHHDc2ZxzTJHgeuCckX7n19vQ3XNLuXv9oGKNNCi8kHnT4tUnnGXNAWXWuyBgZKWUL8u3y41iW6dLYK3Pw5zfpKZTrq3q3bTLJRN5LnnUuFVnWsC3SNqa6VAAvhTGR9PzxLk8C6HeLP2AsYPpqeQwbaL3Ks6tvPdob3tQPWRBGL4uiKtNZ23tRYZGZLYFWZK7psRSZg5AETejKxztVzAuYovpVUiDq71o331tjqWWV1SzWT13Rd1uwz6nHtsjgao2863YaizKARcYr1j9MKtNfDs483yho6i7tbCRR9M4CPLqdiKEaRyVC1FP4F3sejA6nZTuAA35JWUzX6BBj7wgdypMLdMmmtcCZm3bRrF3GvJJs67U8JWRc6dnoGUDaD7rUu ``` -Now lets generate another ticket to spend either topping up once the previous one's data allowance has been used, or with another Gateway. Notice that the `ticket-index` is the same: this is generated from the same aggregated credential as the one above! +Generating another ticket, to spend either when topping up after the previous one's data allowance has been used or with another Gateway. The `ticket-index` is the same: this is generated from the same aggregated credential as the one above. ```sh ❯ ./nym-cli ecash generate-ticket --credential-storage storage.db --provider 6qidVK21zpHD298jdDa1RRpbRozP29ENVyqcSbm6hQrG --ticket-index=3 @@ -1016,7 +1010,7 @@ PAYMENT FOR TICKET 3: Vev3SmwWtH5vbnejX5Zzc1EcxXAgveqHpKNN8arxXaWLhFcEpdcZ6n7qr3NrQUNURWsK2AsUiX8aSiGSjMPEY3iDE3aDYnjYERVow8RKUmQiYSKvz7v9cEJxt97JAHBfu9WYNHXTnLFSJwWuFtBdzY5dzPdzGckFenGCysa1ZBHGADHChDVXKoPHXxpn5qyJxmi48coUQDptR64QgkCeQ8RRZ396Lxw2NKFSjqavCMMDVm3g1rW7cYyPanBhkoAUzPU9KXX1rtmhD6F9gV89mGZ8fm7ByDuKuYU28seLQ7GkVKkhNeRW9XxbjSiyscTnMUzJ24R5VbSdr141BaquUHezdUTzmA2EjAtcyyiVrCMV13cc96CRbMXENP2soUzckFnh1qPnrfKCvX4JYkztq7UgPT2mZEnSTDW4C6Z2NVCNBPNLqUSYrU4id8Jzcp1mBxqJjdYcQ7P5fWJbT5Q9NAq44PCgfXpsUkNoj35QVQvKXKLb5oNGqnua5YC1WBPcENcpS7ZPWpk2hwe8VK4gNgnwQtWH2RPmWbvBREAV97vS1vKNHJyry9sD2PiMJGSmBnb1bKsGxR9UQN3YvRsdGHzyJHzAMTzxbFJBqMPmxjSHJR4UdwzhB81Ludu1RAffTvecWFxmWH5bNymCQjw3wey7Uequcxgyy8KAWYDzvHGwCZQbHQXghsYREiqquZWaa8hX3iTNBFUtEk8PRVT78MoFNdeBWNjsLr8zyZ5EGnf4kqmw3a91g5p5vywf6e3LgMu19VHjPSNtKMNXiatkPEVjsCuCppmV4sB7FsdKKWcMUSWLsdmrDBg9PStHr7NaJRzLL5E91gvysmB36Nob9cHeHSZj3wM4NVVjFfZeRqQf4bi7ahfXjeeBetgDpqx7JcbU6tTN4JpcGUpp7fp4MhTq7MeVQMLweGUVLqewKgAGzCvEmrK6dzLd3U1P9vkAAVZ3cCAKUywnHGxoxDeEfexP1g1EqJLtKNZVKPf7hSMWqGhoQ36K7y5GnyZ5YhQ7jcDME9orm5w4StoxoDdCPcjbakKG7UaTHuhd7tU1mUffXcEvVerkXoQK9SEaKvGks21RBhW86aHUzJWVbkiDzdaqjJWbmzLV8FKvNxNyzucoH2rq8LiHRMZfV1H3SkVSa4j2Ktw7ZGoQfdj8DgekxXSR2nHPfhybzKYXTBqFo2ACisxkjR4rXr9Xo6eYywQhQ1MP6aYgYCAXFGHPoFf7kx7Jns5sWvHRBdaMF65zeFF2m5NDuMWETtLgFfsyNgR84vfSqTfzj2gsUykRei7q9N4LKmiDwBALTAEcTvZpLtXBjc8JaB9PUeBw7DoSiSK376sGrQ9F6ZGTngXACNz1TbvYhtau4bDa6KC2Qn7wmoyrphpn7TtM1jdwGBxLcaEEWZKQHvWVfTyL2itjqnrcAZkxYdCj56oQYwpWfKQk3zJEUA6SYHqyJjaLNVK6u25j7969EWjdpTsJ8qSsZgXi3T7dQqiwintZbUUUKRq7egN1SGVnA6Wup91uKrYUWEWMqVu4g8ipmRsLD9iXHHr3yA21Cka7pqk1FxR9BFTAnkk1 ``` -These are both generated by the _same_ underlying ticketbook and used in a way that they cannot be tied to each other. An ingress Gateway might (for instance) get 100 connection requests from 100 Nym clients, each validated with a ticket. It has no way of knowing whether these are all from the same single subscription, or 100 different ones. +These are both generated by the same underlying ticketbook and used in a way that they cannot be tied to each other. An ingress Gateway might (for instance) get 100 connection requests from 100 Nym clients, each validated with a ticket. It has no way of knowing whether these are all from the same single subscription, or 100 different ones. --- title: Double Spend Protection @@ -1035,7 +1029,7 @@ Double spend protection in the context of zk-nym is a balancing act between spee ## Offline Approach: Pros & Cons The advantages of the offline approach are manifold: - Immediate access to the Nym network upon zk-nym submission, eliminating any delays in service provisioning until payments are deposited and verified as would occur in the online approach. -- Alleviates performance strain on ingress Gateways and Quorum members, serving as a more efficient method compared to the online counterpart. By moving computationally intense work to the Quorum, this means that Gateway nodes are able to be run on less powerful machines, meaning more operators can more easily run them (and cover their costs) and thus increase the overall number and spread of Gateways around the globe. +- Reduces load on ingress Gateways and Quorum members compared to the online approach. Moving the compute-heavy work to the Quorum means Gateway nodes can run on less capable machines, so more operators can run them (and cover their costs), increasing the overall number and spread of Gateways around the globe. - Moreover, the offline approach can circumvent the potential issue of overwhelming the blockchain with the serial numbers of spent coins. However, the offline approach introduces certain limitations. @@ -1043,10 +1037,10 @@ However, the offline approach introduces certain limitations. - Any potential repercussions against double spenders can only be implemented once the user requests a new credential for their zk-nym Generator (aka they have to 'top up' and buy more bandwidth allowance), assuming they haven't altered their identifier (the Bech32 address). An exploitable scenario arises from these limitations: -- A malicious user purchases bandwidth and aggregates a valid zk-nym credential in the standard way, worth $10 of crypto/fiat. Subsequently, the malicious user proceeds to sell the credential to 100 users for $1 each, allowing each user to generate zk-nym tickets of 100MB from this **valid** credential. Under the offline approach, entry nodes forego double-spending checks; so long as the clients all used different ingress Gateways, all 100 users could access the network without obtaining a subscription. As bandwidth consumption is tracked locally between client and ingress node, and each zk-nym ticket is rerandomised, there is no way that ingress Gateways would know that the zk-credential used by the client has been shared with other parties. This loophole highlights the need for stringent measures to counter such potential abuses within the system, without creating either speed bottlenecks (in the case of the Online model) or impacting the anonymity of the system. We can, however, mitigate this problem without doing either of these things. +- A malicious user purchases bandwidth and aggregates a valid zk-nym credential in the standard way, worth $10 of crypto/fiat. The malicious user then sells the credential to 100 users for $1 each, allowing each user to generate zk-nym tickets of 100MB from this **valid** credential. Under the offline approach, entry nodes skip double-spending checks; so long as the clients all used different ingress Gateways, all 100 users could access the network without obtaining a subscription. As bandwidth consumption is tracked locally between client and ingress node, and each zk-nym ticket is rerandomised, there is no way that ingress Gateways would know that the zk-credential used by the client has been shared with other parties. This loophole calls for measures to counter such abuses without creating either speed bottlenecks (as in the Online model) or harming the anonymity of the system. We can mitigate this problem without doing either. ## Solution to Offline Double Spending -To efficiently prevent the fraudulent use of tickets within the Nym network, a two-tiered solution is in place that combines (1) the immediate detection of double-spending attempts at the level of individuals ingress Gateways and (2) subsequent identification and blacklisting of offending clients at the Quorum level. +To prevent fraudulent use of tickets within the Nym network, a two-tiered solution combines (1) immediate detection of double-spending attempts at individual ingress Gateways and (2) subsequent identification and blacklisting of offending clients at the Quorum level. ### Entry Node Implementation: Real-Time Ticket Validation Each spent zk-nym ticket contains as an attribute a unique serial number, which is revealed in plaintext to the respective ingress Gateway. Each Gateway has a copy of a [Bloom Filter](https://www.geeksforgeeks.org/bloom-filters-introduction-and-python-implementation/) - on receiving a ticket, it will check against its copy of a local database to check whether this serial number has already been seen. If so, it rejects the ticket as being double-spent and the client's connection request is rejected. If not, it will add the serial number to its local DB. @@ -1055,7 +1049,7 @@ Each spent zk-nym ticket contains as an attribute a unique serial number, which Each Gateway will periodically share their serial numbers with the Quorum and refresh their copy of the Bloom Filters from the Quorum, in order to refresh the global list shared by all ingress Gateways and the Quorum. See the step below for more on this. -> Crucially, ingress Gateways refrain from extensive computations to identify the original ticket owner, and avoids broadcasting information about the double-spending attempt to other ingress Gateways. The entry node is also not involved in any global blacklisting process of the clients. The sole purpose of this check is to swiftly identify any attempts at double-spending and add the seen ticket's serial number to the local DB cache. +> Crucially, ingress Gateways do not perform extensive computations to identify the original ticket owner, and do not broadcast information about the double-spending attempt to other ingress Gateways. The entry node is also not involved in any global blacklisting of clients. The sole purpose of this check is to quickly identify double-spending attempts and add the seen ticket's serial number to the local DB cache. ### Nym-API Implementation: Blacklisting and Penalties for Double-Spenders All Gateways periodically forward the collected tickets to the Quorum, enabling them to pinpoint and blacklist any clients who double spend. Upon receiving the tickets, the Quorum appends all the incoming serial numbers to the global list of spend zk-nym serial numbers and proceed with the identification process for any malicious users engaging in double-spending. @@ -1064,13 +1058,13 @@ This identification phase involves looking for instances of double spending, ide --- title: Nym Network Infrastructure -description: Overview of the Nym Network's decentralized infrastructure: independently operated nodes coordinated by the Nyx blockchain for routing, key management, and credential issuance. +description: Overview of the Nym Network's decentralised infrastructure: independently operated nodes coordinated by the Nyx blockchain for routing, key management, and credential issuance. url: https://nym.com/docs/network/infrastructure --- # Infrastructure -The Nym Network runs on decentralized infrastructure: a set of independently operated nodes coordinated by the Nyx blockchain, where no single party controls routing, key management, or credential issuance. +The Nym Network runs on decentralised infrastructure: a set of independently operated nodes coordinated by the Nyx blockchain, where no single party controls routing, key management, or credential issuance. ## In this section @@ -1140,7 +1134,7 @@ To run a node, see the [Operator Documentation](/operators/introduction). ## Unified binary -The various components were originally separate binaries but have been consolidated into a single `nym-node` binary where the role is specified at runtime. This simplifies operation and makes configuration consistent across roles. +These components were originally separate binaries but have been consolidated into a single `nym-node` binary where the role is specified at runtime. This simplifies operation and makes configuration consistent across roles. In the future, nodes will automatically switch modes based on network conditions. Operators won't need to manually set whether a node is a Gateway or Mix Node; the network will assign modes dynamically each epoch. @@ -1158,7 +1152,7 @@ The current deployment includes {stats.nodes} active nodes across {stats.locatio --- title: Exit Gateway Services: Network Requester & IP Packet Router -description: The two proxy services running on Nym Exit Gateways, the Network Requester (SOCKS proxy) and the IP Packet Router (raw IP tunneling), how they work, what they see, and who uses them. +description: The two proxy services running on Nym Exit Gateways: the Network Requester (SOCKS proxy) and the IP Packet Router (raw IP tunneling). How they work, what they see, and who uses them. url: https://nym.com/docs/network/infrastructure/exit-services --- @@ -1176,8 +1170,8 @@ Both services run on every Exit Gateway. Which one handles your traffic depends The Network Requester is a SOCKS4/4a/5 proxy. Clients send SOCKS-formatted requests through the mixnet, and the NR makes the corresponding connection on their behalf: resolving hostnames, opening TCP connections, and relaying data. ```text -Client → mixnet → Exit Gateway (NR) → SOCKS connect → destination - ← relay response ← +Client → Entry Gateway → Mixnodes1..3 → Exit Gateway (NR) → SOCKS connect → destination + ← relay response ← ``` Because it operates at the application layer, the NR: @@ -1186,15 +1180,15 @@ Because it operates at the application layer, the NR: - Can enforce allow/deny lists on destination hosts and ports - Sees the destination hostname and port, but not the contents if TLS is used -**Used by:** the [SDK's SOCKS client](/developers/rust/mixnet), [standalone SOCKS5 client](/developers/clients/socks5), and [mixFetch](/developers/typescript#mixfetch) (which wraps SOCKS requests in a browser-friendly `fetch` API). +**Used by:** the [SDK's SOCKS client](/developers/rust/mixnet), [standalone SOCKS5 client](/developers/clients/socks5), and [mixFetch](/developers/mix-fetch) (which wraps SOCKS requests in a browser-friendly `fetch` API). ## IP Packet Router The IP Packet Router operates at the IP layer. Instead of proxying individual connections, it allocates a virtual IP address to the client and routes raw IP packets between the client and the internet, functioning as a tunnel endpoint. ```text -Client → mixnet → Exit Gateway (IPR) → raw IP packets → destination - ← raw IP packets ← +Client → Entry Gateway → Mixnodes1..3 → Exit Gateway (IPR) → raw IP packets → destination + ← raw IP packets ← ``` On connection, the IPR: @@ -1205,11 +1199,11 @@ On connection, the IPR: Because it operates at the IP layer, the IPR: - Does not resolve DNS; the client handles its own DNS (either via clearnet or by sending DNS queries as UDP packets through the tunnel) -- Handles any IP protocol, not just TCP: UDP, ICMP, etc. +- Handles any IP protocol: TCP, UDP, ICMP, etc. - Sees raw IP packets, including destination IPs and ports - Does not see contents if the client uses TLS or another encryption layer -In both services, traffic between the Exit Gateway and the destination travels as **normal internet traffic**. The mixnet protects sender anonymity (the destination sees the gateway's IP, not yours), but does not encrypt the final hop. Use TLS or another end-to-end encryption layer to protect payload confidentiality. +In both services, traffic between the Exit Gateway and the destination travels over the public internet, exactly as it would from any other server. The mixnet protects sender anonymity (the destination sees the gateway's IP, not yours), but does not encrypt the payload past the gateway. Use TLS or another application-layer cipher to protect payload confidentiality, just as you would on a direct connection. **Used by:** [NymVPN anonymous mode](/network/dvpn-mode/protocol) (5-hop mixnet routing to the IPR), and [`smolmix`](/developers/smolmix) (programmatic `TcpStream`/`UdpSocket` access to the IPR via the Rust SDK). @@ -1293,17 +1287,17 @@ For persistent identity across sessions, store your keypairs and re-register wit --- title: Epochs in the Nym Network -description: How epochs organize time in the Nym Network: reward distribution, topology reshuffling, SURB validity windows, and future automatic role assignment. +description: How epochs organise time in the Nym Network: reward distribution, topology reshuffling, SURB validity windows, and future automatic role assignment. url: https://nym.com/docs/network/reference/epochs --- # Epochs -Time in the Nym Network is organized into epochs: discrete periods during which certain network operations occur. The current epoch length is one hour. +Time in the Nym Network is organised into epochs: discrete periods during which certain network operations occur. The current epoch length is one hour. ## What happens at epoch boundaries -**Reward distribution** calculates performance metrics for each node and distributes NYM token rewards based on routing reliability and uptime, ensuring that nodes successfully forwarding packets earn more than those with poor performance. +**Reward distribution** calculates performance metrics for each node and distributes NYM token rewards based on routing reliability and uptime, so that nodes successfully forwarding packets earn more than those with poor performance. **Topology rerandomization** shuffles the arrangement of nodes in each layer. This prevents long-term route prediction attacks and limits the damage from any compromised nodes. Nodes may also enter or leave the active set based on uptime monitoring and stake changes. @@ -1345,9 +1339,7 @@ Acknowledgements operate hop-by-hop between adjacent nodes. They confirm that pa ## Implementation -This is handled entirely by the Nym binaries. Developers and operators don't need to implement or configure acknowledgements; the system handles packet loss transparently. - -**Lewes Protocol:** The upcoming Lewes release will introduce changes to how acknowledgements are handled. The current hop-by-hop ACK mechanism described above may be revised as part of broader protocol improvements. Details will be documented here once the changes are finalised. +This is handled entirely by the Nym binaries. Developers and operators don't need to implement or configure acknowledgements; the system handles packet loss without any application involvement. --- title: Licensing @@ -1390,149 +1382,238 @@ Private harassment is also unacceptable. No matter who you are, if you feel you Likewise any spamming, trolling, flaming, baiting or other attention-stealing behaviour is not welcome. --- -title: Nym Developer Portal: SDKs & Tools -description: Developer documentation for building privacy-enhanced applications on the Nym mixnet. Covers Rust SDK, TypeScript SDK, blockchain interaction & CLI tools. +title: Overview +description: Choose a Nym integration path by runtime and approach, then find the crate or package: nym-sdk, smolmix, mix-tunnel, mix-fetch, mix-dns, mix-websocket, and the TypeScript SDK. url: https://nym.com/docs/developers --- -# Developer Documentation +# Overview -Build applications that protect user metadata using the Nym Mixnet. This section covers SDK integration, blockchain interaction, and developer tools. +Every Nym integration sends its traffic through the mixnet via a Nym client. Which crate or package you use comes down to two questions: -## Where to start +1. **Runtime**: where does your code run? +2. **Approach**: do you control both sides of the connection (**end-to-end**), or are you reaching a third-party service through the mixnet (**proxy**)? -**Choosing an integration approach:** read [Integrations](/developers/integrations) to understand the architectural trade-offs (native SDK vs proxy vs mixFetch), then pick your path: +The table below maps those two answers to a package. -- **[Rust SDK](/developers/rust):** full-featured SDK with message passing, `AsyncRead`/`AsyncWrite` streams, and client pooling. Start with the [Tour](/developers/rust/tour). -- **[`smolmix`](/developers/smolmix):** standalone crate providing `TcpStream` and `UdpSocket` over the Mixnet via a userspace IP stack. Drop-in compatible with `tokio-rustls`, `hyper`, `tungstenite`, and the rest of the async Rust ecosystem. Also serves as the core for companion crates that plug into specific frameworks (e.g. hyper connectors, DNS resolvers). -- **[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. +## Choosing a package -## Blockchain interaction +| Runtime | End-to-end (both sides run Nym) | Proxy (exit to clearnet) | +|---|---|---| +| **Native Rust** (desktop / CLI / server) | [`nym-sdk`](/developers/rust): Mixnet, Stream, Client Pool | [`smolmix`](/developers/smolmix): `TcpStream` / `UdpSocket` · [`nym-sdk` SOCKS](/developers/rust) | +| **Browser / WebView** (JS + WASM) | [TypeScript SDK](/developers/typescript): `@nymproject/sdk` raw messaging | [`mix-fetch`](/developers/mix-fetch) HTTP/S · [`mix-dns`](/developers/mix-dns) DNS · [`mix-websocket`](/developers/mix-websocket) WS/WSS | -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). +**Mobile is a host, not a runtime.** The same phone can run either row. Compile the Rust SDK to a native library (`uniffi` plus [`cargo-swift`](https://github.com/antoniusnaumann/cargo-swift) for an iOS XCFramework, or [`cargo-ndk`](https://github.com/bbqsrc/cargo-ndk) for Android `jniLibs/`), or load the WASM packages inside a WebView (Capacitor, Cordova, Ionic, WKWebView, Android WebView). The SDK ships [FFI bindings](/developers/rust/ffi) for Go and C/C++ only; for Swift or Kotlin you generate your own from the [`sdk/ffi/shared`](https://github.com/nymtech/nym/tree/develop/sdk/ffi) uniffi crate. The WebView path needs no Nym-specific native code. On Android the native path has a [TLS bootstrap gotcha](/developers/rust/mixnet/troubleshooting#android-mixnet-bootstrap-fails-with-a-certificate-revoked--ocsp-error). -## API reference +## End-to-end or proxy -Auto-generated API specs for Nym infrastructure endpoints are in the [APIs section](/apis/introduction). +The runtime axis is about where your code runs: a native process has raw sockets and a filesystem, so it runs the full Rust client; a browser or WebView has neither (only WebSockets and `fetch`, under mixed-content rules), so it runs a WASM client inside a Web Worker. The approach axis is about who runs Nym at the other end. ---- -title: Nym Developer Portal: SDKs & Tools -description: Developer documentation for building privacy-enhanced applications on the Nym mixnet. Covers Rust SDK, TypeScript SDK, blockchain interaction & CLI tools. -url: https://nym.com/docs/developers ---- - -# 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). -- **[`smolmix`](/developers/smolmix):** standalone crate providing `TcpStream` and `UdpSocket` over the Mixnet via a userspace IP stack. Drop-in compatible with `tokio-rustls`, `hyper`, `tungstenite`, and the rest of the async Rust ecosystem. Also serves as the core for companion crates that plug into specific frameworks (e.g. hyper connectors, DNS resolvers). -- **[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). - ---- -title: Integrating With Nym -description: Choose an integration path for sending application traffic through the Nym mixnet, depending on your runtime environment and architecture. -url: https://nym.com/docs/developers/integrations ---- - -# Integrating With Nym - -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**. - -## Environment - -Different runtimes have different transport constraints: a browser cannot open raw sockets or access the filesystem, while a desktop app can. - -- **Native / Desktop**: full access to system networking and persistent storage. Use the [Rust SDK](/developers/rust) or [`smolmix`](/developers/smolmix). -- **Browser**: restricted to WebSockets, Web Transport, and `fetch`, with HTTPS-only mixed content rules and no filesystem access. Use the [TypeScript SDK](/developers/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. For Rust, [`smolmix`](/developers/smolmix) provides `TcpStream` and `UdpSocket` types that work as drop-in replacements for their tokio equivalents. - -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. - -See the [Native / Desktop](/developers/native) and [Browser](/developers/browsers) pages for the specific modules available in each environment. - ---- -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. -url: https://nym.com/docs/developers/native ---- - -# Native / Desktop Apps - -Desktop apps and CLIs integrate with two broad approaches: embedding Nym clients on both sides of the communication (E2E), or using the Mixnet as a proxy to reach external services. - -## Mixnet End-To-End -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. +**End-to-end**: both sides run a Nym client. Traffic stays Sphinx-encrypted the whole way ([what this protects](/developers/concepts/exit-security#proxy-mode-or-end-to-end)). Use it for peer-to-peer setups or anywhere you control both endpoints. ![](/images/developers/nym-arch-client-to-client.png) -| SDK Module | What it does | Status | Links | -|---|---|---|---| -| **Stream** | `AsyncRead + AsyncWrite` byte streams multiplexed over the mixnet, the closest analogue to TCP sockets | Recommended | [docs](/developers/rust/stream) · [tutorial](/developers/rust/stream/tutorial) | -| **Mixnet** | Raw message API and `MixnetClient` for full control over the communication model | Stable | [docs](/developers/rust/mixnet) · [tutorial](/developers/rust/mixnet/tutorial) | -| **Client Pool** | Pre-connected client pool for bursty workloads | Stable | [docs](/developers/rust/client-pool) | -| **TcpProxy** | Localhost TCP sockets that proxy traffic through the mixnet | Unmaintained | [docs](/developers/rust/tcpproxy) | - -**TcpProxy is unmaintained.** Use the [Stream module](/developers/rust/stream) for new projects. - -## Mixnet-As-Proxy -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. +**Proxy**: only your side runs Nym. Traffic exits the mixnet at an Exit Gateway and continues to the destination over the public internet. The mixnet anonymises the sender; protecting the payload (TLS, Noise) is your application's job, exactly as on a direct connection. Use it for third-party services such as blockchain RPCs or external APIs. ![](/images/developers/nym-arch-ip-routing.png) -Traffic is Sphinx-encrypted until the Exit Gateway, where it's unwrapped into HTTPS ([Network Requester](/network/infrastructure/exit-services#network-requester)) or raw IP ([IP Packet Router](/network/infrastructure/exit-services#ip-packet-router)). The last hop to the remote host **travels as normal internet traffic**. Use TLS or another encryption layer to protect the final hop. +Past the Exit Gateway, traffic travels the public internet like any other connection. The mixnet anonymises the sender but does not encrypt the payload beyond the gateway. Use TLS or another application-layer cipher. See [Exit security](/developers/concepts/exit-security) for what the exit can and cannot observe. -| Standalone Crate | What it does | Links | -|---|---|---| -| **`smolmix`** | Userspace IP tunnel providing `TcpStream` and `UdpSocket` over the mixnet, compatible with the entire async Rust ecosystem. Also serves as the core for companion crates that plug into specific frameworks (e.g. hyper connectors, DNS resolvers) | [docs](/developers/smolmix) | - -| SDK Module | What it does | Links | -|---|---|---| -| **SOCKS Client** | SOCKS4/4a/5 proxy via the Exit Gateway's Network Requester. Works with any SOCKS-capable application without code changes, just point it at the local proxy | [docs](/developers/rust/mixnet) | - ---- -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. -url: https://nym.com/docs/developers/browsers ---- - -# Browser-Based Apps - -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. +In a browser or WebView, your app talks to that WASM client through JS bindings rather than direct calls. The mixnet behaviour is identical in both modes, only the integration shape differs. See [mix-* architecture](/developers/mix-architecture) for the full picture. ![](/images/developers/nym-browser-arch.png) -| Module | What it does | Links | +## Packages + +### Rust + +| Crate | Use it for | +|---|---| +| [`nym-sdk`](/developers/rust) | End-to-end mixnet messaging, `AsyncRead`/`AsyncWrite` byte streams, client pooling. Start with the [Tour](/developers/rust/tour). | +| [`smolmix`](/developers/smolmix) | `TcpStream` and `UdpSocket` over the mixnet via a userspace IP stack. Compatible with `tokio-rustls`, `hyper`, `tokio-tungstenite`, and the rest of the async Rust ecosystem. | + +### TypeScript + +The four mix-* packages share one tunnel ([`mix-tunnel`](/developers/mix-tunnel)) and one WASM instance; install only what you need. See [mix-* architecture](/developers/mix-architecture) for how they're wired. + +| Package | Use it for | +|---|---| +| [`mix-tunnel`](/developers/mix-tunnel) | The shared tunnel the three feature packages build on. Most apps don't import it directly. | +| [`mix-fetch`](/developers/mix-fetch) | Drop-in `fetch()` for HTTP and HTTPS through the mixnet. | +| [`mix-dns`](/developers/mix-dns) | Hostname-to-IP resolution through the mixnet. UDP DNS via the IPR. | +| [`mix-websocket`](/developers/mix-websocket) | WebSocket-like class for WS and WSS through the mixnet. | +| [TypeScript SDK](/developers/typescript) | `@nymproject/sdk`: end-to-end raw messaging when you control both ends. Smart contracts via `@nymproject/contract-clients`. | + +### Standalone and other + +| Resource | Use it for | +|---|---| +| [SOCKS5 / WebSocket clients](/developers/clients) | Language-agnostic binaries for piping traffic through the mixnet without an SDK. | +| [Chain interaction](/developers/chain) | Query Nyx state, submit transactions, call Nym smart contracts. | +| [APIs](/apis/introduction) | Auto-generated reference for Nym infrastructure HTTP endpoints. | + +--- +title: Overview +description: Choose a Nym integration path by runtime and approach, then find the crate or package: nym-sdk, smolmix, mix-tunnel, mix-fetch, mix-dns, mix-websocket, and the TypeScript SDK. +url: https://nym.com/docs/developers +--- + +# Overview + +Every Nym integration sends its traffic through the mixnet via a Nym client. Which crate or package you use comes down to two questions: + +1. **Runtime**: where does your code run? +2. **Approach**: do you control both sides of the connection (**end-to-end**), or are you reaching a third-party service through the mixnet (**proxy**)? + +The table below maps those two answers to a package. + +## Choosing a package + +| Runtime | End-to-end (both sides run Nym) | Proxy (exit to clearnet) | |---|---|---| -| **mixFetch** | Drop-in `fetch` replacement. Routes HTTP(S) requests via Exit Gateways with an embedded CA store for browser-to-destination TLS over the Mixnet | [docs](/developers/typescript#mixfetch) · [example](/developers/typescript/playground/mixfetch) | -| **WASM Client** | Sphinx packets and cover traffic in WASM, sent over WebSocket to the Entry Gateway. Messaging mode only (text/binary payloads), runs in a web worker | [docs](/developers/typescript#mixnet-client) · [example](/developers/typescript/playground/traffic) | +| **Native Rust** (desktop / CLI / server) | [`nym-sdk`](/developers/rust): Mixnet, Stream, Client Pool | [`smolmix`](/developers/smolmix): `TcpStream` / `UdpSocket` · [`nym-sdk` SOCKS](/developers/rust) | +| **Browser / WebView** (JS + WASM) | [TypeScript SDK](/developers/typescript): `@nymproject/sdk` raw messaging | [`mix-fetch`](/developers/mix-fetch) HTTP/S · [`mix-dns`](/developers/mix-dns) DNS · [`mix-websocket`](/developers/mix-websocket) WS/WSS | -`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. +**Mobile is a host, not a runtime.** The same phone can run either row. Compile the Rust SDK to a native library (`uniffi` plus [`cargo-swift`](https://github.com/antoniusnaumann/cargo-swift) for an iOS XCFramework, or [`cargo-ndk`](https://github.com/bbqsrc/cargo-ndk) for Android `jniLibs/`), or load the WASM packages inside a WebView (Capacitor, Cordova, Ionic, WKWebView, Android WebView). The SDK ships [FFI bindings](/developers/rust/ffi) for Go and C/C++ only; for Swift or Kotlin you generate your own from the [`sdk/ffi/shared`](https://github.com/nymtech/nym/tree/develop/sdk/ffi) uniffi crate. The WebView path needs no Nym-specific native code. On Android the native path has a [TLS bootstrap gotcha](/developers/rust/mixnet/troubleshooting#android-mixnet-bootstrap-fails-with-a-certificate-revoked--ocsp-error). -The WASM Client does not support IP packet routing (IPR) or stream-like APIs. For HTTP(S) requests from the browser, use `mixFetch`. Standard browser CSP and mixed content restrictions (HTTPS only) apply to the WebSocket connection. +## End-to-end or proxy + +The runtime axis is about where your code runs: a native process has raw sockets and a filesystem, so it runs the full Rust client; a browser or WebView has neither (only WebSockets and `fetch`, under mixed-content rules), so it runs a WASM client inside a Web Worker. The approach axis is about who runs Nym at the other end. + +**End-to-end**: both sides run a Nym client. Traffic stays Sphinx-encrypted the whole way ([what this protects](/developers/concepts/exit-security#proxy-mode-or-end-to-end)). Use it for peer-to-peer setups or anywhere you control both endpoints. + +![](/images/developers/nym-arch-client-to-client.png) + +**Proxy**: only your side runs Nym. Traffic exits the mixnet at an Exit Gateway and continues to the destination over the public internet. The mixnet anonymises the sender; protecting the payload (TLS, Noise) is your application's job, exactly as on a direct connection. Use it for third-party services such as blockchain RPCs or external APIs. + +![](/images/developers/nym-arch-ip-routing.png) + +Past the Exit Gateway, traffic travels the public internet like any other connection. The mixnet anonymises the sender but does not encrypt the payload beyond the gateway. Use TLS or another application-layer cipher. See [Exit security](/developers/concepts/exit-security) for what the exit can and cannot observe. + +In a browser or WebView, your app talks to that WASM client through JS bindings rather than direct calls. The mixnet behaviour is identical in both modes, only the integration shape differs. See [mix-* architecture](/developers/mix-architecture) for the full picture. + +![](/images/developers/nym-browser-arch.png) + +## Packages + +### Rust + +| Crate | Use it for | +|---|---| +| [`nym-sdk`](/developers/rust) | End-to-end mixnet messaging, `AsyncRead`/`AsyncWrite` byte streams, client pooling. Start with the [Tour](/developers/rust/tour). | +| [`smolmix`](/developers/smolmix) | `TcpStream` and `UdpSocket` over the mixnet via a userspace IP stack. Compatible with `tokio-rustls`, `hyper`, `tokio-tungstenite`, and the rest of the async Rust ecosystem. | + +### TypeScript + +The four mix-* packages share one tunnel ([`mix-tunnel`](/developers/mix-tunnel)) and one WASM instance; install only what you need. See [mix-* architecture](/developers/mix-architecture) for how they're wired. + +| Package | Use it for | +|---|---| +| [`mix-tunnel`](/developers/mix-tunnel) | The shared tunnel the three feature packages build on. Most apps don't import it directly. | +| [`mix-fetch`](/developers/mix-fetch) | Drop-in `fetch()` for HTTP and HTTPS through the mixnet. | +| [`mix-dns`](/developers/mix-dns) | Hostname-to-IP resolution through the mixnet. UDP DNS via the IPR. | +| [`mix-websocket`](/developers/mix-websocket) | WebSocket-like class for WS and WSS through the mixnet. | +| [TypeScript SDK](/developers/typescript) | `@nymproject/sdk`: end-to-end raw messaging when you control both ends. Smart contracts via `@nymproject/contract-clients`. | + +### Standalone and other + +| Resource | Use it for | +|---|---| +| [SOCKS5 / WebSocket clients](/developers/clients) | Language-agnostic binaries for piping traffic through the mixnet without an SDK. | +| [Chain interaction](/developers/chain) | Query Nyx state, submit transactions, call Nym smart contracts. | +| [APIs](/apis/introduction) | Auto-generated reference for Nym infrastructure HTTP endpoints. | + +--- +title: Exit Security: What the Mixnet Protects and What It Doesn't +description: The canonical security model for traffic that leaves the Nym mixnet at an IPR exit gateway. Applies to smolmix, mix-tunnel, mix-fetch, mix-dns, and mix-websocket alike. +url: https://nym.com/docs/developers/concepts/exit-security +--- + +# Exit security + +Every tool that reaches an external service through the Nym mixnet shares the same security model, whether it's the Rust [`smolmix`](/developers/smolmix) crate or the mix-* packages built on [`mix-tunnel`](/developers/mix-tunnel) ([`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), [`mix-websocket`](/developers/mix-websocket)). They all exit the mixnet at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) gateway, so they inherit the same properties and the same single caveat. This page is the canonical statement of that model; the package pages link here rather than restating it. + +## The one-sentence version + +The mixnet hides **who** you are from the destination and **where** you're going from the network, but the exit gateway sees your **destination** and any payload you didn't encrypt yourself. + +## Proxy mode or end-to-end? + +This page is about **proxy mode**: your traffic leaves the mixnet at an IPR exit and continues to a third-party server over clearnet, where the security trade-offs apply. + +If both ends run a Nym client (**end-to-end**), traffic never exits the mixnet. It stays Sphinx-encrypted from your client to the other client, there is no IPR, and the [encrypt-your-own-payload](#encrypt-your-own-payload) concern below does not arise: the mixnet is the encrypted channel. What still applies to end-to-end traffic is everything that is not exit-specific: the [trust boundaries](#trust-boundaries) (unlinkability is statistical, not absolute) and [what the mixnet does not protect](#what-the-mixnet-does-not-protect) (application identity, fingerprinting, traffic analysis). For the end-to-end wiring itself, see [`nym-sdk`](/developers/rust) and the [TypeScript SDK](/developers/typescript). + +## What each hop sees + +```text + you + │ Sphinx + ▼ + entry gateway + │ Sphinx + ▼ + 3 mix layers + │ Sphinx + ▼ + IPR exit gateway + │ plain IP (Sphinx removed here) + ▼ + destination +``` + +| Segment | Mixnet encryption | What's visible | +|---|---|---| +| Your machine → mixnet entry | Sphinx (layered) | Entry gateway sees your IP but not the destination | +| Inside the mixnet (entry gateway + 3 mix layers) | Sphinx (layered) | Each node only knows its previous and next hop | +| Exit gateway (IPR) | Sphinx removed, raw IP packet exposed | IPR sees destination IP + port. Payload depends on your application layer (see below). | +| IPR → remote host | None (Sphinx is mixnet-only; your own TLS, if any, still applies) | Remote host sees the IPR's IP, not yours | + +The Sphinx encryption is the **mixnet transport layer**: it protects packets as they traverse the mix nodes. At the exit gateway the Sphinx layers are stripped and the original IP packet is forwarded to the destination. This is analogous to how a Tor exit node or VPN endpoint unwraps its tunnel. + +## Encrypt your own payload + +Because the IPR removes the Sphinx layers, whatever is inside that IP packet is visible to the exit unless you encrypted it yourself. + +- **Application-layer encryption closes the gap.** TLS, the Noise Protocol, or any authenticated encryption keeps the payload as ciphertext to the IPR. It still sees the destination IP and port, but not the content. Over TLS the IPR only ever handles ciphertext bound for that destination; the bytes inside stay opaque to it. +- **Unencrypted payloads are fully visible.** Plain HTTP, unencrypted WebSocket (`ws://`), and plain UDP DNS are readable in full at the exit. The mixnet still hides your identity, so the exit reads the content without being able to attribute it to you. + +## Trust boundaries + +- You trust the mixnet to provide unlinkability between sender and receiver. Sphinx provides this cryptographically at the per-packet level: a node cannot read addressing beyond its own hop. Unlinkability of your *traffic pattern* over time is weaker, and statistical rather than absolute. It comes from mixing and cover traffic, and degrades with low network traffic, with cover traffic or Poisson timing disabled, and against an adversary that can observe a large fraction of the network. +- You trust the IPR exit gateway in the same way you trust a VPN exit or Tor exit node: it can inspect your raw IP packets. The difference is that the IPR doesn't know who is sending the traffic (the mixnet hides your identity). + +Treat the IPR exactly as you would a VPN exit or a Tor exit node: it can inspect your raw IP packets. The difference Nym adds is that the IPR doesn't know who's sending the traffic. Protect the payload with TLS or equivalent, and pin a trusted exit (`preferredIpr`) if the exit operator matters to you. + +## What the mixnet does not protect + +The mixnet operates at the **network layer**: it hides your IP and unlinks sender from receiver. It does nothing at the **application layer**, so anything you reveal in the content of your traffic is yours to manage: + +- **Application identity.** If you log in, send a cookie, or include an API token, the destination knows who you are regardless of the network path. The mixnet anonymises the pipe, not what you put through it. +- **Fingerprinting.** A stable request pattern, a distinctive TLS or HTTP fingerprint, or a recognisable account correlates your traffic across sessions. `mix-fetch`'s [default headers](/developers/mix-fetch/guides#default-request-headers) reduce trivial fingerprinting but do not make you indistinguishable from a real browser. + +Separately, the network-layer guarantee itself is not absolute: + +- **Statistical traffic analysis.** Unlinkability is probabilistic, not a guarantee. It is strong by default but weakens with low network traffic, with cover traffic and Poisson timing turned off, and against an adversary observing a large fraction of the network. + +If you need anonymity at the application layer too, design for it explicitly: fresh identities, no cross-session correlators, and no logged-in accounts you also use over clearnet. + +## Comparison with other privacy tools + +| | Nym (mixnet) | Tor | VPN | +|---|---|---|---| +| **Exit node sees traffic?** | Yes (encrypt it) | Yes (encrypt it) | Yes (encrypt it) | +| **Exit node knows sender?** | No (mixnet hides identity) | No (onion routing) | Yes (VPN provider knows) | +| **Timing analysis resistance** | Strong with defaults (mixing, cover traffic) | Weak (low-latency) | None | +| **UDP support** | Yes | No (TCP only) | Yes | + +The timing-analysis rating assumes the defaults. Cover traffic and Poisson timing can be turned off to trade that resistance for latency and bandwidth, moving Nym's row toward Tor's. The named switches for this (`disableCoverTraffic` / `disablePoissonTraffic`) are specific to the browser/wasm packages ([`mix-tunnel`](/developers/mix-tunnel/guides#configuration) and the feature packages built on it); the native `smolmix` crate does not expose them by those names. The UDP row reflects a design difference, not a ranking: Tor is TCP-only by design, while the Nym IPR routes raw IP. + +## Read more + +The package pages add the parts specific to their transport (where TLS terminates, what the resolver sees, WSS vs `ws://`): + +- [Exit Gateway Services](/network/infrastructure/exit-services#ip-packet-router): how the IPR allocates addresses and routes raw IP packets, and how it differs from the SOCKS-based Network Requester. +- The per-package "Security model" section on [mix-fetch](/developers/mix-fetch/concepts#security-model), [mix-dns](/developers/mix-dns/guides#security-model), and [mix-websocket](/developers/mix-websocket/concepts#security-model) for the transport-specific exposure. --- title: Nym Client Message Queue and Cover Traffic @@ -1542,7 +1623,7 @@ url: https://nym.com/docs/developers/concepts/message-queue # Message Queue - Although useful for understanding how the Nym Client works internally, this information is only of practical use if you are using the [`Mixnet`](/developers/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`](/developers/rust/stream) module (`AsyncRead + AsyncWrite` channels) and the [`TcpProxy`](/developers/rust/tcpproxy) module (TCP tunnelling with message ordering). + Useful for understanding how the Nym Client works internally, but only of practical interest if you are using the [`Mixnet`](/developers/rust/mixnet) module of the Rust SDK and interacting with the client at a low level. The [`Stream`](/developers/rust/stream) module (`AsyncRead + AsyncWrite` channels) abstracts most of this away. ## 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/mixnet-mode/cover-traffic) at a constant rate defined by a Poisson process. This is part of the network's mitigation of timing attacks. @@ -1610,16 +1691,16 @@ sequenceDiagram ## What does `send()` do then? -When passing a message to a client (however you do it, either piping messages from an app to a standalone client or via one of the `send` functions exposed by the SDKs), you are **putting that message into the queue** to be source encrypted and sent in the future, in order to ensure that traffic leaving the client does so in a manner that to an external observer is uniform / does not create any 'burst' or change in traffic timings that could aid traffic analysis. +When passing a message to a client (however you do it, either piping messages from an app to a standalone client or via one of the `send` functions exposed by the SDKs), you are **putting that message into the queue** to be source-encrypted and sent later, so that traffic leaving the client stays uniform to an external observer and creates no burst or timing change that could aid traffic analysis. ## Note on Client Shutdown -Accidentally dropping a client before your message has been sent is something that is possible and should be avoided (see the [troubleshooting guide](/developers/rust/mixnet/troubleshooting) for more on this) but is easy to avoid simply by remembering to: +Accidentally dropping a client before your message has been sent is possible and should be avoided (see the [troubleshooting guide](/developers/rust/mixnet/troubleshooting) for more on this). To avoid it: - keep your client process alive, even if you are not expecting a reply to your message -- (in the case of the SDKs) properly disconnecting your client in order to make sure that the message queue is flushed of Sphinx packets with actual payloads. +- (with the SDKs) disconnect your client properly so that the message queue is flushed of Sphinx packets with real payloads. --- title: smolmix: TCP/UDP Over the Nym Mixnet -description: A userspace IP tunnel that provides standard TcpStream and UdpSocket types over the Nym mixnet. Drop-in compatible with async tokio Rust ecosystem. +description: A userspace IP tunnel that provides standard TcpStream and UdpSocket types over the Nym mixnet. Compatible with the async tokio Rust ecosystem. url: https://nym.com/docs/developers/smolmix --- @@ -1644,10 +1725,12 @@ Traffic exits the mixnet at an [IPR (Internet Packet Router)](/network/infrastru ### Other runtimes `smolmix` currently requires `tokio`. The internal pipeline is tokio-based: the bridge task that shuttles packets to the mixnet, the Nym SDK's `MixnetClient`, and the [`tokio-smoltcp`](https://docs.rs/tokio-smoltcp) reactor that drives the userspace TCP/IP stack all run on the tokio runtime. -This means `smolmix` is not compatible with alternative async runtimes like [`smol`](https://docs.rs/smol) or [`async-std`](https://docs.rs/async-std) out of the box. If you need to use `smolmix` from another runtime, the [`async-compat`](https://docs.rs/async-compat) crate can bridge the gap. +This means `smolmix` is not directly compatible with alternative async runtimes like [`smol`](https://docs.rs/smol) or [`async-std`](https://docs.rs/async-std). If you need to use `smolmix` from another runtime, the [`async-compat`](https://docs.rs/async-compat) crate can bridge the gap. -### Non-native `smolmix` -A WASM version of `smolmix` is planned for a future release. +### The WebAssembly build (browsers and WebViews) +The WebAssembly build of `smolmix` powers the mix-* packages [`mix-tunnel`](/developers/mix-tunnel), [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), and [`mix-websocket`](/developers/mix-websocket). The wasm is inlined into `mix-tunnel`, so apps install one of those packages rather than building the crate themselves. + +It runs in any JavaScript runtime that provides `WebSocket`, Web Workers, and WebAssembly. That means desktop browsers and mobile WebViews alike, so hybrid app shells (Capacitor, Ionic, Cordova, `react-native-webview`) are in scope, not just web pages. It does not run in a bare Node.js or React Native Hermes/JSC runtime, which lack Web Workers (and, for Hermes, WebAssembly). For native mobile or server targets, compile the Rust crate itself rather than using the wasm packages. ## Installation @@ -1655,17 +1738,19 @@ Add `smolmix` to your `Cargo.toml`: ```toml [dependencies] -smolmix = "1.21.0" -nym-bin-common = { version = "1.21.0", features = ["basic_tracing"] } +smolmix = "1.21.1" +nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } ``` -`tokio` is a transitive dependency of smolmix, but you need to enable `rt-multi-thread` (smolmix spawns multiple tasks internally) and `macros` (for `#[tokio::main]`). +`tokio` is a transitive dependency of smolmix, but you need to enable `rt-multi-thread` (the default runtime spun up by `#[tokio::main]`) and `macros` (for the `#[tokio::main]` attribute itself). `nym-bin-common` is optional but recommended: it sets up [`tracing`](https://docs.rs/tracing) logging so you can see mixnet connection progress. **Minimum Rust version:** {RUST_MSRV}+ +`smolmix`'s API may still change between minor releases. Pin a version rather than tracking the latest, and read the changelog before bumping. + ### From Git For unreleased changes, import directly from the repository: @@ -1680,41 +1765,17 @@ smolmix = { git = "https://github.com/nymtech/nym", branch = "develop" } |---|---|---|---|---| | **Layer** | Transport (TCP/UDP) | Message (multiplexed streams) | HTTP | TCP (SOCKS proxy) | | **Controls both sides?** | No (proxy mode) | Yes (E2E) | No (proxy mode) | No (proxy mode) | -| **API** | `TcpStream`, `UdpSocket` | `AsyncRead + AsyncWrite` | `fetch()` drop-in | SOCKS4/5 protocol | +| **API** | `TcpStream`, `UdpSocket` | `AsyncRead + AsyncWrite` | `fetch()` replacement | SOCKS4/5 protocol | | **Composability** | Full: TLS, HTTP, WebSocket, DNS, etc. stack on top | Byte streams only | HTTP(S) only | Application-dependent | | **Best for** | Reaching external services from Rust with standard networking | Peer-to-peer / E2E protocols between Nym clients | Browser HTTP requests | Legacy apps with SOCKS support | ## Security model -Traffic is Sphinx-encrypted inside the mixnet, but between the exit gateway and the remote host it travels as **normal internet traffic**. Always encrypt the final hop with TLS ([`rustls`](https://docs.rs/rustls)), Noise Protocol ([`snow`](https://docs.rs/snow)), etc. +Traffic is Sphinx-encrypted inside the mixnet. Past the Exit Gateway, it travels over the public internet to the destination, the same as any other server-initiated connection. Protect the payload at the application layer with TLS ([`rustls`](https://docs.rs/rustls)), Noise Protocol ([`snow`](https://docs.rs/snow)), or equivalent, as you would on a direct connection. -### What's protected +The mixnet hides your identity from the destination and your destination from the network, but the IPR exit gateway sees the destination IP and port, and sees your payload only if you didn't encrypt it. If you connect with TLS (as in the [TCP example](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp.rs)), the IPR only sees ciphertext addressed to the destination, not its contents. Plaintext HTTP is fully readable at the exit. -| Segment | Mixnet encryption | What's visible | -|---|---|---| -| Your machine → mixnet entry | Sphinx (layered) | Entry gateway sees your IP but not the destination | -| Inside the mixnet (entry + 3 mix layers + exit) | Sphinx (layered) | Each node only knows prev/next hop | -| Exit gateway (IPR) | Sphinx removed, raw IP packet exposed | IPR sees destination IP + port. Payload depends on your application layer (see below). | -| IPR → remote host | None (Sphinx is mixnet-only) | Remote host sees IPR's IP, not yours | - -The Sphinx encryption is the **mixnet transport layer**: it protects packets as they traverse the mix nodes. At the exit gateway, the Sphinx layers are stripped and the original IP packet is forwarded to the destination. This is analogous to how a Tor exit node or VPN endpoint unwraps its tunnel. - -**What's inside that IP packet is entirely up to you.** If you connect with TLS (as in the [TCP example](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp.rs)), the IPR sees encrypted TLS ciphertext going to a destination IP: it knows *where* but not *what*. If you send plaintext HTTP, the IPR can read the full request and response. - -### Trust boundaries - -- **You trust the mixnet** to provide unlinkability between sender and receiver. This is enforced cryptographically by the Sphinx packet format and mixing. -- **You trust the IPR exit gateway** in the same way you trust a VPN exit or Tor exit node: it can inspect your raw IP packets. The difference is that the IPR doesn't know *who* is sending the traffic (the mixnet hides your identity). -- **Application-layer encryption closes the gap.** TLS, Noise Protocol, or any authenticated encryption ensures the IPR only sees ciphertext. It can see destination IP and port, but not payload content. - -### Comparison with other privacy tools - -| | smolmix | Tor | VPN | -|---|---|---|---| -| **Exit node sees traffic?** | Yes (encrypt it) | Yes (encrypt it) | Yes (encrypt it) | -| **Exit node knows sender?** | No (mixnet hides identity) | No (onion routing) | Yes (VPN provider knows) | -| **Timing analysis resistance** | Strong (mixing, cover traffic) | Weak (low-latency) | None | -| **UDP support** | Yes | No (TCP only) | Yes | +The full model (what each hop sees, trust boundaries, and a comparison with Tor and VPNs) is on [Exit security](/developers/concepts/exit-security). ## Examples @@ -1728,12 +1789,10 @@ All examples accept `--ipr
` to target a specific exit node (pass a `Re | Example | Source | What it demonstrates | |---|---|---| -| UDP | [`udp.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/udp.rs) | DNS lookup via [`hickory-proto`](https://docs.rs/hickory-proto), sending a raw UDP query to `1.1.1.1:53` through the mixnet | +| UDP | [`udp.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/udp.rs) | DNS lookup via [`hickory-proto`](https://docs.rs/hickory-proto), sending a raw UDP query to `1.1.1.1:53` through the mixnet. Runs a clearnet [`hickory-resolver`](https://docs.rs/hickory-resolver) lookup alongside it to compare resolved IPs and latency | | TCP | [`tcp.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp.rs) | HTTPS request via [`hyper`](https://docs.rs/hyper) + [`tokio-rustls`](https://docs.rs/tokio-rustls). Fetches Cloudflare's `/cdn-cgi/trace` to show that the exit IP differs from clearnet | -| WebSocket | [`websocket.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/websocket.rs) | WebSocket echo via [`tokio-tungstenite`](https://docs.rs/tokio-tungstenite) + [`tokio-rustls`](https://docs.rs/tokio-rustls). Full TCP → TLS → WebSocket stack composing over smolmix | -| UDP multi | [`udp_multi.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/udp_multi.rs) | Multiple DNS lookups with timeout handling + NTP time sync, all over mixnet UDP | +| WebSocket | [`websocket.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/websocket.rs) | WebSocket echo against `ws.postman-echo.com` via [`tokio-tungstenite`](https://docs.rs/tokio-tungstenite) + [`tokio-rustls`](https://docs.rs/tokio-rustls). Runs the same stack over clearnet first, so the only thing that changes between runs is the underlying TCP stream | | TCP download | [`tcp_download.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp_download.rs) | DNS-over-mixnet + multi-request HTTP/1.1 download over a single keep-alive connection, the full real-world pattern | -| WebSocket multi | [`websocket_multi.rs`](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/websocket_multi.rs) | Burst echo (varying message sizes) + idle survival probes with clearnet baseline. Sustained multi-message WebSocket usage over the mixnet | ## Architecture @@ -1744,14 +1803,27 @@ The internal stack (smoltcp reactor, device adapter, bridge task, packet flow) i Full API documentation is available on [docs.rs/smolmix](https://docs.rs/smolmix). --- -title: Nym Rust SDK: Privacy Apps for the Mixnet -description: Rust SDK reference for building privacy applications on the Nym mixnet. Covers the Mixnet client, Stream multiplexing, Client Pool, and code examples. +title: nym-sdk: Rust SDK for the Nym Mixnet +description: Rust SDK reference for building privacy applications on the Nym mixnet. Covers the Mixnet client, Stream multiplexing, Client Pool, FFI bindings, and code examples. url: https://nym.com/docs/developers/rust --- -# Rust SDK +# nym-sdk -All modules share a common `MixnetClient` that manages gateway connections, Sphinx packet encryption, routing, and cover traffic. +`nym-sdk` is the Rust SDK for the Nym mixnet. All modules share a common `MixnetClient` that manages gateway connections, Sphinx encryption, and cover traffic. + +```text +┌──────────────────────────────────────────────────────────────┐ +│ Your Rust app (alice) │ +│ └─ MixnetClient (Sphinx layering, cover traffic) │ +│ └─ WebSocket to entry gateway │ +│ └─ Nym mixnet (entry → 3 mix layers → exit) │ +│ └─ MixnetClient (bob) │ +│ └─ Your Rust app (bob) │ +└──────────────────────────────────────────────────────────────┘ +``` + +Both sides run a `MixnetClient`. Sphinx encryption protects every hop end-to-end; neither gateway nor any mix node can link sender to receiver. Full API reference: [**docs.rs/nym-sdk**](https://docs.rs/nym-sdk/latest/nym_sdk/) @@ -1759,15 +1831,22 @@ For an overview of what the SDK can do, see the **[Tour](./rust/tour)**. For set ## Modules -- **[Stream](./rust/stream)**: multiplexed `AsyncRead + AsyncWrite` byte streams over the Mixnet. **If you're used to TCP sockets, start here.** +| Module | What it does | Status | +|---|---|---| +| [**Stream**](./rust/stream) | Multiplexed `AsyncRead + AsyncWrite` byte streams over the Mixnet, the closest analogue to TCP sockets. | Recommended | +| [**Mixnet**](./rust/mixnet) | Raw message payloads, independently routed, no connections or ordering. Full control over the communication model. | Stable | +| [**Client Pool**](./rust/client-pool) | Keeps ready-to-use `MixnetClient` instances warm for bursty workloads. | Stable | +| [**TcpProxy**](./rust/tcpproxy) | TCP socket proxying with session management and message ordering. | Deprecated | +| [**FFI**](./rust/ffi) | Go and C/C++ bindings. | Stable | -- **[Mixnet](./rust/mixnet)**: raw message payloads, independently routed, no connections or ordering. Use this when you want full control over the communication model. +**TcpProxy is deprecated.** Use the [Stream module](./rust/stream) for new projects. -- **[Client Pool](./rust/client-pool)**: keeps ready-to-use `MixnetClient` instances warm for bursty workloads. +## Proxy-mode crates -- **[TcpProxy](./rust/tcpproxy)** *(deprecated)*: TCP socket proxying with session management and message ordering. Use Stream for new projects. +For proxy-mode integrations (reaching third-party services through an Exit Gateway), see also: -- **[FFI](./rust/ffi)**: Go and C/C++ bindings. +- [**`smolmix`**](/developers/smolmix): `TcpStream` and `UdpSocket` over the Mixnet via a userspace IP stack. Compatible with `tokio-rustls`, `hyper`, `tokio-tungstenite`, and the rest of the async Rust ecosystem. +- [**SOCKS Client**](./rust/mixnet): SOCKS4/4a/5 proxy via the Exit Gateway's Network Requester. Works with any SOCKS-capable application without code changes. --- title: Tour of the Rust SDK @@ -1778,7 +1857,7 @@ url: https://nym.com/docs/developers/rust/tour 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. -**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 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. @@ -1943,7 +2022,7 @@ url: https://nym.com/docs/developers/rust/importing ```toml [dependencies] -nym-sdk = "1.21.0" +nym-sdk = "1.21.1" ``` **Minimum Rust version:** {RUST_MSRV}+ @@ -1970,7 +2049,7 @@ url: https://nym.com/docs/developers/rust/mixnet # Mixnet Module -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. +The `mixnet` module 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. 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. @@ -2025,9 +2104,9 @@ url: https://nym.com/docs/developers/rust/mixnet/tutorial # Tutorial: Send Your First Private Message -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. +A program that sends a Sphinx-encrypted message to itself through the Nym Mixnet, receives it, and replies anonymously using SURBs. Later sections cover persistent identity and concurrent send/receive. -**You'll need:** Rust {RUST_MSRV}+ and an internet connection (clients connect to the live Mixnet). +Requires Rust {RUST_MSRV}+ and an internet connection (clients connect to the live Mixnet). ## Step 1: Set up the project @@ -2040,8 +2119,8 @@ Add dependencies to `Cargo.toml`: ```toml [dependencies] -nym-sdk = "1.21.0" -nym-bin-common = { version = "1.21.0", features = ["basic_tracing"] } +nym-sdk = "1.21.1" +nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] } tokio = { version = "1", features = ["full"] } ``` @@ -2056,7 +2135,7 @@ use nym_sdk::mixnet::{self, MixnetMessageSender}; async fn main() { nym_bin_common::logging::setup_tracing_logger(); - // connect_new() creates an ephemeral client: keys are generated in + // 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(); @@ -2098,7 +2177,7 @@ Every message includes a `sender_tag`, an opaque `AnonymousSenderTag` that lets ```rust let sender_tag = message.sender_tag.expect("should have sender tag"); - // send_reply uses the SURB: the sender's address is never revealed. + // 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 { @@ -2180,7 +2259,7 @@ async fn main() { }; println!("Reply: {}", String::from_utf8_lossy(&reply.message)); - // Always disconnect for clean shutdown: background tasks need to be + // Always disconnect for clean shutdown; background tasks need to be // stopped and state files flushed. client.disconnect().await; } @@ -2363,7 +2442,7 @@ cargo run --example | 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 | +| 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 customise 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 | @@ -2441,6 +2520,12 @@ client.disconnect().await; `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. +## Android: mixnet bootstrap fails with a certificate `Revoked` / OCSP error + +When you compile the SDK for Android (via `uniffi` + `cargo-ndk`), the client can fail to bootstrap the mixnet with a hard `Revoked` or "Certificate does not specify OCSP responder" error on certain Validator endpoints. The cause is `rustls-platform-verifier` routing certificate validation through Java's `CertPathValidator`, which enforces an OCSP check the endpoint does not satisfy. iOS and the desktop targets are unaffected; they tolerate or skip the check. + +Configure the client to use preconfigured TLS roots with `webpki_roots::TLS_SERVER_ROOTS` instead of the platform verifier to get around it. + --- 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. @@ -2451,7 +2536,7 @@ url: https://nym.com/docs/developers/rust/stream 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.** +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. 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. @@ -2459,10 +2544,10 @@ All streams are multiplexed over a single `MixnetClient`. A background router ta 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. +1. The 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. The 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. +4. 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 ┌─────────────────────────────────────────────────────────┐ @@ -2572,7 +2657,7 @@ The receiver replies via **reply SURBs** (Single Use Reply Blocks) and never lea | **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 | -**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. +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: 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. ## Next steps @@ -2588,7 +2673,7 @@ url: https://nym.com/docs/developers/rust/stream/tutorial # Tutorial: Build a Private Echo Server -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. +Two programs: a server that listens for incoming streams and echoes back what it receives, and a client that opens a stream, writes data, and reads the echo. Both communicate through the Nym Mixnet using `AsyncRead` and `AsyncWrite`, like a TCP socket pair. ## What you'll learn @@ -2615,8 +2700,8 @@ Add dependencies to `Cargo.toml`: ```toml [dependencies] -nym-sdk = "1.21.0" -nym-bin-common = { version = "1.21.0", features = ["basic_tracing"] } +nym-sdk = "1.21.1" +nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] } tokio = { version = "1", features = ["full"] } rand = "0.8" ``` @@ -2639,7 +2724,7 @@ async fn main() { let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); println!("Echo server listening at: {}", client.nym_address()); - // Create a listener: this activates stream mode. + // Create a listener; this activates stream mode. // From this point, message-based methods are disabled. let mut listener = client.listener().unwrap(); @@ -2662,7 +2747,7 @@ async fn main() { loop { let n = match stream.read(&mut buf).await { - Ok(0) => break, // EOF: stream closed + Ok(0) => break, // EOF, stream closed Ok(n) => n, Err(e) => { eprintln!("Stream {stream_id} read error: {e}"); @@ -2687,7 +2772,7 @@ async fn main() { } ``` -**`listener()` can only be called once per client.** It takes exclusive ownership of the inbound message channel. A second call returns `Error::ListenerAlreadyTaken`. +`listener()` can only be called once per client. It takes exclusive ownership of the inbound message channel; a second call returns `Error::ListenerAlreadyTaken`. ## Step 3: Build the client @@ -2723,7 +2808,7 @@ async fn main() { 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 + // 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; @@ -2815,7 +2900,7 @@ Stream 12345678 closed 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. +6. The inbound stream replies via reply SURBs, the same anonymous reply mechanism as the message API. 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. @@ -3019,7 +3104,7 @@ There is no switching back without disconnecting and creating a new client. ## Known limitations -**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. +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, so 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. @@ -3051,22 +3136,18 @@ cargo run --example | 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 | --- -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. +title: Nym TcpProxy: Route TCP via the Mixnet (Deprecated) +description: Route TCP traffic through the Nym mixnet using the TcpProxy Rust module. Deprecated in favour of the Stream module. url: https://nym.com/docs/developers/rust/tcpproxy --- # TcpProxy Module - **This module is unmaintained.** The TcpProxy is no longer actively developed in favour of the [Stream module](/developers/rust/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. +This module is unmaintained. The TcpProxy is no longer actively developed in favour of the [Stream module](/developers/rust/stream), which provides `AsyncRead + AsyncWrite` channels directly over the Mixnet without the localhost TCP socket layer. Existing users should plan to migrate. The module will continue to work but will not receive new features or bug fixes. -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 callers read and write to like any other TCP connection. The Stream module replaces this pattern with multiplexed channels on a single client and no localhost socket layer. ---- - -`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](/developers/tools/standalone-tcpproxy). +> Non-Rust/Go developers can use the [standalone binaries](/developers/tools/standalone-tcpproxy) instead. ## Examples @@ -3082,145 +3163,13 @@ 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 - -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 = "1.21.0" -nym-network-defaults = "1.21.0" -nym-bin-common = { version = "1.21.0", features = ["basic_tracing"] } -tokio = { version = "1", features = ["full"] } - -[[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() -> Result<(), Box> { - 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() -> Result<(), Box> { - nym_bin_common::logging::setup_tracing_logger(); - // Load mainnet network defaults into env vars (required by NymProxyClient's internal ClientPool) - setup_env(None::); - - let server_addr: Recipient = std::env::args() - .nth(1).expect("Usage: proxy_client ") - .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 -- -``` - -The response will take 30–60 seconds to arrive as it traverses the Mixnet in both directions. +[`docs.rs/nym-sdk/tcp_proxy`](https://docs.rs/nym-sdk/latest/nym_sdk/tcp_proxy/) covers types, methods, and the full client/server walkthrough. ## Architecture -Each sub-module handles Nym clients differently: -- **`NymProxyClient`** relies on the [Client Pool](/developers/rust/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. +`NymProxyClient` uses a [Client Pool](/developers/rust/client-pool) with one client per incoming TCP connection; if the pool runs dry it falls back to creating clients on demand. `NymProxyServer` runs 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: +Each TCP connection is wrapped in a session ID; messages within a session carry an incrementing message ID, and a final `Close` message signals that no more outbound bytes are coming. Session and message IDs are necessary because the Mixnet guarantees delivery but not ordering, and ordering matters whenever a parser cares about frame boundaries (gRPC over protobuf, HTTP, TLS). ```rust pub struct ProxiedMessage { @@ -3230,71 +3179,7 @@ pub struct ProxiedMessage { } ``` -### 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. +For the full request/response sequence diagram, see the [module source](https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/src/tcp_proxy) or the [docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/tcp_proxy/) entry. --- title: Client Pool: Pre-Connected Mixnet Clients @@ -3304,7 +3189,7 @@ url: https://nym.com/docs/developers/rust/client-pool # Client Pool -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. +The `ClientPool` keeps a configurable number of `MixnetClient` instances pre-connected in a background loop, so callers don't pay the gateway handshake, key generation, and topology fetch cost on the hot path. ## How it works @@ -3320,11 +3205,11 @@ flowchart LR 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 +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. +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). @@ -3372,7 +3257,7 @@ url: https://nym.com/docs/developers/rust/client-pool/tutorial # Tutorial: Handle Bursty Traffic with Client Pool -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. +A program that uses `ClientPool` to absorb bursts of concurrent Mixnet operations without paying client-creation latency on the hot path. The pool pre-creates clients in the background; tasks pop them under load; the tutorial also walks through what happens when demand outruns supply. ## What you'll learn @@ -3398,9 +3283,9 @@ Add dependencies to `Cargo.toml`: ```toml [dependencies] -nym-sdk = "1.21.0" -nym-network-defaults = "1.21.0" -nym-bin-common = { version = "1.21.0", features = ["basic_tracing"] } +nym-sdk = "1.21.1" +nym-network-defaults = "1.21.1" +nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] } tokio = { version = "1", features = ["full"] } ``` @@ -3462,14 +3347,14 @@ When you call `get_mixnet_client()`, the pool removes a client and returns it. T c } None => { - // Pool is empty: fall back to creating one on the fly. + // 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 + // 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}")) @@ -3488,7 +3373,7 @@ When you call `get_mixnet_client()`, the pool removes a client and returns it. T } } - // Disconnect when done: the pool will create a replacement + // Disconnect when done; the pool will create a replacement. client.disconnect().await; println!("Task {i}: done"); }); @@ -3688,7 +3573,7 @@ ffi └── shared # Shared Rust implementation ``` -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. +Core logic lives in `shared/` and is imported into language-specific wrappers. The shared layer handles thread safety and runs client operations on blocking threads on the Rust side of the FFI boundary. ## What's exposed @@ -3703,7 +3588,7 @@ The TcpProxy module is deprecated. For new projects, use the [Stream module](./s ## Quick example (Go) ```go -// Initialize an ephemeral client +// Initialise an ephemeral client bindings.InitEphemeral() // Get our Nym address @@ -3772,443 +3657,879 @@ Each language has a `build.sh` script that compiles the Rust shared library and - [`sdk/ffi` source](https://github.com/nymtech/nym/tree/develop/sdk/ffi): full source and build scripts --- -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. +title: Mixnet Playground +description: Interactive browser playground for Nym's TypeScript packages: drive fetch, DNS, WebSocket and download traffic through a mixnet tunnel, and send end-to-end messages with the raw messaging SDK. +url: https://nym.com/docs/developers/playground +--- + +# Mixnet playground + +This playground runs Nym's browser TypeScript packages against the live mixnet. It covers both integration models: + +- **Proxy**, via the [mix-* family](/developers/mix-tunnel): bring the shared tunnel up once, then drive `fetch`, DNS, WebSocket, stress and file-download traffic through it to clearnet destinations. +- **End-to-end**, via the [raw messaging SDK](/developers/typescript): Sphinx-encrypted messages between two Nym clients, with nothing exiting to the clearnet. + +Some sections send the same request over the tunnel and over the clearnet, so you can compare the two. + +## HTTPS / DNS / WebSockets + +**Watch the Network tab.** Open DevTools → Network before you connect. Once +`setupMixTunnel` reports ready, every tunnel operation here (`mixFetch`, +`mixDNS`, `MixWebSocket`) adds **no new request** to that tab: it is multiplexed +inside the single WebSocket to the entry gateway. Only the *clearnet* comparison +buttons add rows. (Setup also fetches the network topology over HTTPS and +refreshes it periodically, so those nym-api calls and the gateway WebSocket are +the only clearnet requests you will see.) Your real traffic never leaves the +browser as an identifiable, per-destination request. + +Everything here runs client-side over the live Nym mixnet. The first +`setupMixTunnel` is slow (a few seconds): it loads the WebAssembly client, +registers a fresh client identity with a gateway, and discovers an IPR exit. +Later calls reuse the tunnel. + +## Raw mixnet messaging + +The sections above share one smolmix tunnel and exit to the clearnet through an IPR. The [Messaging SDK](/developers/typescript) (`@nymproject/sdk`) is the other model: end-to-end mixnet messages between two Nym clients, where you control both ends and nothing exits to the clearnet. It runs a separate wasm client, so it loads on demand: + +## Source and examples + +- [Playground source](https://github.com/nymtech/nym/tree/develop/documentation/docs/components/playground): the React component behind this page (`MixPlayground.tsx` and `lib.ts`). +- [SDK examples](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples): standalone runnable apps, including a browser example per package ([mix-fetch](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-fetch/browser), [mix-dns](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-dns/browser), [mix-websocket](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-websocket/browser)). + +## Per-package docs + +For the API of each package, see +[mix-tunnel](/developers/mix-tunnel), [mix-fetch](/developers/mix-fetch), +[mix-dns](/developers/mix-dns), and [mix-websocket](/developers/mix-websocket). + +--- +title: mix-tunnel: Shared Mixnet Tunnel for the Browser +description: TypeScript package that owns the shared Nym mixnet tunnel in the browser. The base layer for mix-fetch, mix-dns, and mix-websocket. +url: https://nym.com/docs/developers/mix-tunnel +--- + +# mix-tunnel + +[`@nymproject/mix-tunnel`](https://www.npmjs.com/package/@nymproject/mix-tunnel) owns a single mixnet tunnel in the browser and exposes it to the feature packages built on it: [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), and [`mix-websocket`](/developers/mix-websocket). All three call into the same tunnel, so they share one IPR connection, one userspace TCP/IP stack ([`smoltcp`](https://docs.rs/smoltcp)), and one DNS cache. + +The tunnel lives in a Web Worker and is built on [smolmix-wasm](https://github.com/nymtech/nym/tree/develop/wasm/smolmix), the WebAssembly build of the [Rust `smolmix` crate](/developers/smolmix). One WASM instance per page, regardless of how many feature packages you import. See [mix-* architecture](/developers/mix-architecture) for how the worker, the Comlink boundary, and the smoltcp + rustls stack fit together. + +## When to use it directly + +Most apps don't import `mix-tunnel` directly. The feature packages re-export `setupMixTunnel`, `disconnectMixTunnel`, and `getTunnelState`, so calling `setupMixTunnel()` from `mix-fetch` brings the tunnel up for all three. + +Use `mix-tunnel` directly when you want to: + +- Configure the tunnel once at app startup, before any feature package is loaded. +- Inspect tunnel state from UI code that isn't tied to a specific feature package. +- Tear down the tunnel explicitly, for example before a page unload. + +## In this section + +- [Get started](/developers/mix-tunnel/get-started): install and bring the tunnel up. +- [Reference](/developers/mix-tunnel/guides): configuration and reading tunnel state. +- [Architecture](/developers/mix-architecture): how the shared tunnel and Web Worker are wired. +- [Exit security](/developers/concepts/exit-security): what the IPR exit can see. +- [TypeDoc reference](/developers/mix-tunnel/api/globals): generated from the source. +- [Examples](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples): the mix-fetch, mix-dns, and mix-websocket example apps all bring the tunnel up through this package. + +--- +title: Get started with mix-tunnel +description: Install @nymproject/mix-tunnel and bring up the shared mixnet tunnel. +url: https://nym.com/docs/developers/mix-tunnel/get-started +--- + +# Get started + +## Installation + +```bash +npm install @nymproject/mix-tunnel +``` + +The package ships ESM only. The bundled Web Worker and smolmix-wasm are inlined, so no separate WASM-loading config is needed in Webpack, Vite, or esbuild. + +The smolmix-family packages (`mix-tunnel`, `mix-fetch`, `mix-dns`, `mix-websocket`) are new and version together. The API is still settling: pin exact versions and expect breaking changes between releases until the family reaches a stable line. `mix-fetch` already went through a [v1 to v2 clean break](/developers/mix-fetch/migration). + +## Quick start + +```ts + +// Brings the tunnel up. Call once per page; a second call rejects. +await setupMixTunnel(); + +// { state: 'ready' } once the IPR handshake completes. +console.log(await getTunnelState()); + +// Tear down before page unload. The WASM is unusable after this until reload. +window.addEventListener('beforeunload', () => { disconnectMixTunnel(); }); +``` + +After `setupMixTunnel()` resolves, [`mixFetch()`](/developers/mix-fetch), [`mixDNS()`](/developers/mix-dns), and `new MixWebSocket()` (from [`mix-websocket`](/developers/mix-websocket)) all become usable. + +Bring the tunnel up live in the [mixnet playground](/developers/playground) and inspect its state over the live mixnet. + +--- +title: mix-tunnel guides +description: Configure the shared mixnet tunnel and read its connection state. +url: https://nym.com/docs/developers/mix-tunnel/guides +--- + +# Reference + +## Configuration + +`setupMixTunnel(opts)` accepts the full smolmix-wasm `SetupOpts` surface plus one TS-layer addition (`debug`). Pass any subset; every field has a default (listed in [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts)). + +```ts +await setupMixTunnel({ + // Pin a specific IPR (otherwise auto-discovered from the topology). + preferredIpr: 'D1rrUqJY9pesL3pTaMaxLnpZGGYQ4ZpZwpQXCqaeBXTW.6PpFkRvF...', + + // Cover traffic and Poisson timing are ON by default. Setting these disables + // them for lower latency and bandwidth, at the cost of traffic-analysis + // resistance (it drops timing-correlation resistance at the entry and exit). + disableCoverTraffic: true, + disablePoissonTraffic: true, + + // Verbose console tracing from smolmix-wasm. Useful while integrating. + debug: true, +}); +``` + +The complete option surface (IPR pinning, SURB budgets, DNS server overrides, TCP/connect timeouts, gateway selection) is documented in the [`SetupMixTunnelOpts` type reference](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts). + +Call `setupMixTunnel` once. The WASM tunnel is one-shot per page: the first call brings it up, and a second call rejects with `tunnel already initialised`. Because the feature packages share that one tunnel, calling it from `mix-fetch` is enough for `mix-dns` and `mix-websocket` too. If more than one call site might invoke it, guard with `getTunnelState()` and skip setup when the state is already `ready`. + +## Tunnel state + +`getTunnelState()` returns a tagged state for surfacing connection status in the UI. The five states are: + +| State | Meaning | +|---|---| +| `connecting` | Default before `setupMixTunnel()` completes. | +| `ready` | Tunnel is up. Feature packages can issue requests. | +| `shutting_down` | `disconnectMixTunnel()` in progress. | +| `shutdown` | Cleanly torn down. The WASM is no longer usable. | +| `failed` | Setup or runtime error. The `reason` field carries a human-readable cause. | + +```ts +const { state, reason } = await getTunnelState(); +if (state === 'failed') { + console.error('Tunnel failed:', reason); +} +``` + +`await setupMixTunnel()` resolves only once the tunnel is `ready`, so most apps never need to read the state. Poll `getTunnelState()` only to drive a separate status indicator. The transitions are coarse (no progress percentage during `connecting`); the underlying connection events are visible via `debug: true` in the console. + +--- +title: mix-fetch: fetch() Over the Nym Mixnet +description: Drop-in fetch() replacement that routes HTTP and HTTPS requests through the Nym mixnet via an IPR exit gateway. +url: https://nym.com/docs/developers/mix-fetch +--- + +# mix-fetch + +[`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch) routes HTTP and HTTPS through the Nym mixnet behind the browser [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) signature: `mixFetch(url, init)` returns the same `Response` you would get from `fetch(url, init)`. The request travels mixnet hops first, exits at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) gateway, and reaches the destination with the IPR's IP, not yours. It is not a perfect substitute for `fetch`: no cookies or credentials, no HTTP cache, no `AbortController`, and HTTPS-only in practice (plain HTTP is fully visible at the exit; see [drop-in caveats](/developers/mix-fetch/guides#drop-in-caveats)). + +```text +┌────────────────────────────────────────────────────────────────────┐ +│ Your browser app │ +│ └─ mixFetch('https://...') │ +│ └─ mix-tunnel (shared singleton, Web Worker, smolmix-wasm) │ +│ └─ smoltcp userspace TCP/IP + rustls TLS │ +│ └─ WebSocket to entry gateway │ +│ └─ Nym mixnet (3 mix layers) │ +│ └─ IPR exit gateway → destination │ +└────────────────────────────────────────────────────────────────────┘ +``` + +TLS terminates end-to-end between the WASM bundle and the destination server. The IPR sees destination IP and port; for HTTPS targets, payload is TLS ciphertext. + +## In this section + +- [Get started](/developers/mix-fetch/get-started): install and make your first mixnet request. +- [Reference](/developers/mix-fetch/guides): request shape, default headers, drop-in caveats, configuration. +- [Concepts & security](/developers/mix-fetch/concepts): what the IPR exit sees. +- [Migrating from v1.x](/developers/mix-fetch/migration): the v1 to v2 clean break. +- [TypeDoc reference](/developers/mix-fetch/api/globals): generated from the source. +- [Browser example](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-fetch/browser): a runnable example app. + +--- +title: Get started with mix-fetch +description: Install @nymproject/mix-fetch and make your first HTTP request through the Nym mixnet. +url: https://nym.com/docs/developers/mix-fetch/get-started +--- + +# Get started + +## Installation + +```bash +npm install @nymproject/mix-fetch +``` + +ESM only, with the worker and WASM inlined via [`mix-tunnel`](/developers/mix-tunnel/get-started#installation); no bundler config needed. + +## Quick start + +```ts + +// Bring the shared mixnet tunnel up. Same call works from mix-dns and mix-websocket. +await setupMixTunnel(); + +// Drop-in fetch. The Response is the real DOM Response, not a wrapper. +const res = await mixFetch('https://example.com'); +console.log(res.status, await res.text()); + +// Tear down. The WASM is unusable after this until page reload. +await disconnectMixTunnel(); +``` + +For one-shot use without an explicit setup step, the `createMixFetch` helper combines setup and fetch: + +```ts + +const mixFetch = await createMixFetch({ disableCoverTraffic: true }); +const res = await mixFetch('https://example.com'); +``` + +Call `createMixFetch` once and reuse the function it returns. It calls `setupMixTunnel` internally, so calling `createMixFetch` a second time rejects with `tunnel already initialised`; the tunnel is [one-shot per page](/developers/mix-tunnel/get-started). + +Run `mixFetch` live in the [mixnet playground](/developers/playground), with a tunnel-vs-clearnet comparison. + +--- +title: mix-fetch guides +description: Request shape, default headers, drop-in caveats, and tunnel configuration for mix-fetch. +url: https://nym.com/docs/developers/mix-fetch/guides +--- + +# Reference + +## Request shape + +The `init` argument is the standard [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). Headers, method, and body all work. `AbortController` (`signal`) is not supported: an in-flight request cannot be cancelled. + +```ts +const res = await mixFetch('https://httpbin.org/post', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hello: 'mixnet' }), +}); +console.log(await res.json()); +``` + +Binary responses come back via the standard `Response.arrayBuffer()` / `Response.blob()` methods: + +```ts +const res = await mixFetch('https://example.com/image.png'); +const blob = await res.blob(); +``` + +Repeated headers (`Set-Cookie`, `Vary`, `Link`, `WWW-Authenticate`) are preserved. The wasm side returns headers as a `[name, value]` pair sequence, which `Headers` reconstructs verbatim. + +## Default request headers + +When the caller doesn't set them, `mixFetch` injects four browser-shape headers before the request leaves the tunnel. The shim exists because many CDNs (cloudflare's bot management) and host policies (wikimedia's User-Agent policy) reject requests that look unlike a real browser. Caller-supplied values always win. + +| Header | Injected default | +|--------|------------------| +| `User-Agent` | `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36` | +| `Accept` | `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8` | +| `Accept-Language` | `en-US,en;q=0.9` | +| `Accept-Encoding` | `identity` | + +`Accept-Encoding` is forced to `identity` rather than `gzip, deflate, br` because the wasm build has no decompressor. Advertising compression would let the server return a compressed body the wasm build cannot decode, so `Response.text()` or `.json()` would see raw compressed bytes. Responses therefore arrive uncompressed, so large text or JSON bodies transfer more bytes over the slower mixnet path. + +To override any of these, set the header in the `init.headers` bag like normal: + +```ts +const res = await mixFetch('https://example.com', { + headers: { 'User-Agent': 'my-app/1.0' }, +}); +``` + +The shim does not attempt full browser impersonation. TLS fingerprint (JA3), HTTP/2, and header ordering are still distinguishable from a real Chrome request. If you need stronger blend-in, you'll need to handle that at the application or destination layer. + +## Drop-in caveats + +`mixFetch` matches the `fetch()` call signature but is not a perfect substitute. The differences are intentional and follow from running outside the browser's networking stack: + +| Difference | What it means | What to do | +|---|---|---| +| **No same-origin restriction** | Requests aren't subject to browser CORS preflight. The IPR honours its exit policy regardless of `Origin`. | Don't rely on CORS as an access-control mechanism for `mixFetch` requests; treat them as you would server-to-server calls. | +| **No cookies / credentials** | The browser cookie jar is not shared with the WASM instance. `credentials: 'include'` has no effect. | Pass auth tokens via `Authorization` or other explicit headers. | +| **No HTTP cache** | The browser HTTP cache is not consulted. Every call hits the network. | Cache responses at the application layer if needed. | +| **No service-worker interception** | Requests don't pass through any `fetch` event handlers registered by service workers. | n/a | +| **HTTPS only in practice** | The IPR sees plaintext HTTP in full. | Always target `https://` URLs. | + +## Errors + +`mixFetch` follows `fetch` semantics for HTTP status: a 4xx or 5xx response **resolves** with a `Response` carrying that status, so check `response.ok` or `response.status` yourself. The promise **rejects** only on a transport-level failure: a connection or TLS failure, a DNS failure, or the IPR refusing the destination under its exit policy. A rejection is a plain `Error` whose message describes the cause; there is no typed error class, so match on the message if you need to branch. + +## Timeouts and cancellation + +There is no per-request timeout, and `AbortController` / `signal` is ignored: an in-flight `mixFetch` cannot be cancelled. To bound how long you wait, race it against a timer. This stops you waiting but does not cancel the underlying request: + +```ts +const res = await Promise.race([ + mixFetch(url), + new Promise((_, reject) => + setTimeout(() => reject(new Error('mixFetch timeout')), 30_000), + ), +]); +``` + +Connection and DNS timeouts at the tunnel level are set once via `connectTimeoutMs` and `dnsTimeoutMs` in [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts). + +## Configuration + +`setupMixTunnel(opts)` (and `createMixFetch(opts)`) accept the shared tunnel options from [`@nymproject/mix-tunnel`](/developers/mix-tunnel/guides#configuration). The most commonly touched are: + +```ts +await setupMixTunnel({ + // Pin a specific IPR (otherwise auto-discovered from the topology). + preferredIpr: 'D1rrUqJY9pesL3pTaMaxLnpZGGYQ4ZpZwpQXCqaeBXTW.6PpFkRvF...', + + // Lower latency and bandwidth at the cost of traffic-analysis resistance. + disableCoverTraffic: true, + disablePoissonTraffic: true, +}); +``` + +The full option surface is documented under [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts). + +The first `mixFetch` call after `setupMixTunnel()` may take a few seconds: gateway handshake, IPR discovery, and the first DNS resolution all happen on demand. Subsequent calls reuse the tunnel and complete in roughly the time of a normal HTTPS request plus mixnet latency. + +--- +title: mix-fetch concepts & security +description: What the IPR exit sees when you route HTTP through mix-fetch, and what TLS keeps private. +url: https://nym.com/docs/developers/mix-fetch/concepts +--- + +# Concepts & security + +## Security model + +`mix-fetch` follows the shared [mixnet exit security model](/developers/concepts/exit-security): the IPR exit sees your destination, and you rely on TLS to keep the payload as ciphertext to it. What that means specifically for HTTP/S: + +| At the IPR exit | What's visible | +|---|---| +| HTTPS (`https://`) | Destination IP and port. Payload is TLS ciphertext, terminating at the destination rather than the IPR. | +| HTTP (`http://`) | Destination IP and port, plus the full request and response in plaintext. | + +TLS terminates inside the WASM instance (via [`rustls`](https://docs.rs/rustls) in smolmix-wasm), not in the browser. The Mozilla CA bundle is compiled into the WASM. Mixed content rules still apply at the page level, so serve your app over HTTPS. + +--- +title: mix-fetch: Migrating from v1.x to v2 +description: What changed between mix-fetch v1 and v2: removed packages, the new setup options, dropped request-init flags, and the underlying architecture change to a single WASM module exiting via an IPR. +url: https://nym.com/docs/developers/mix-fetch/migration +--- + +# Migrating from v1.x + +v2 is a clean break. The package no longer ships a Go-WASM HTTP/TLS client or a SOCKS5-shaped Network Requester request; it routes through the shared [`mix-tunnel`](/developers/mix-tunnel) and exits at an IPR. If you're starting fresh, you don't need this page; see [mix-fetch](/developers/mix-fetch). + +## Removed packages + +The five-variant publish (`@nymproject/mix-fetch`, `mix-fetch-full-fat`, `mix-fetch-commonjs`, `mix-fetch-commonjs-full-fat`, `mix-fetch-node-commonjs`) is gone. There is one package, ESM only: + +| v1.x | v2.0 | +|---|---| +| `@nymproject/mix-fetch` (ESM) | `@nymproject/mix-fetch` | +| `@nymproject/mix-fetch-full-fat` | `@nymproject/mix-fetch` (always inlined) | +| `@nymproject/mix-fetch-commonjs` | (no CJS build) | +| `@nymproject/mix-fetch-commonjs-full-fat` | (no CJS build) | +| `@nymproject/mix-fetch-node-commonjs` | (no Node build) | + +The single v2 package is ESM-only and always inlines its wasm (the v1 `full-fat` behaviour is now the default), so there is no separate bundler-config step. CommonJS and Node builds are not shipped. The Node build in particular is not just a packaging variant: the mixnet transport runs on browser globals (`WebSocket`, Web Worker) inside the wasm, so a Node target would need a runtime-compatibility layer, not a CommonJS rebuild. If you need a CJS or Node variant, open an issue describing the use case. + +## Removed options + +The v1 setup options bag covered Network Requester selection and gateway tuning. v2 replaces it with the smolmix [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts) surface: + +| v1 option | v2 equivalent | +|---|---| +| `clientId` | retained as `clientId` in `SetupMixTunnelOpts` (defaults to `smolmix-wasm` if unset) | +| `preferredGateway` | (replaced by `preferredIpr`, the exit-side pin) | +| `preferredNetworkRequester` | (no replacement: v2 exits via IPR, not Network Requester) | +| `mixFetchOverride.requestTimeoutMs` | (no replacement at TS layer: surface via smolmix-wasm config if needed) | +| `forceTls: true` | retained as `forceTls` in `SetupMixTunnelOpts` (defaults to WSS; you rarely need to set it) | +| `extra.hiddenGateways` | (no replacement) | + +## Removed request-init flags + +The v1 `mode: 'unsafe-ignore-cors'` flag is gone. v2 doesn't perform browser-side CORS checks at all (see [Drop-in caveats](/developers/mix-fetch/guides#drop-in-caveats)), so the flag has no meaning. + +## New setup + +```ts +// v1 + +const mixFetch = await createMixFetch({ + clientId: 'my-app', + preferredGateway: 'q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1', + mixFetchOverride: { requestTimeoutMs: 60_000 }, + forceTls: true, +}); + +// v2 + +const mixFetch = await createMixFetch({ + // Optional: pin an exit IPR instead of an entry gateway. + // preferredIpr: '...', +}); +``` + +## Architectural change + +v1 ran two WASM modules: a Go module with `crypto/tls` + Mozilla CA bundle, and a Rust module handling Nym's `Socks5Request` framing to a Network Requester exit. v2 runs one WASM module ([smolmix-wasm](https://github.com/nymtech/nym/tree/develop/wasm/smolmix)) with a userspace TCP/IP stack ([smoltcp](https://docs.rs/smoltcp)) and `rustls` for TLS. The exit is an IPR, not a Network Requester. + +Consequences: + +- **One WASM module, smaller bundle.** v1's Go runtime accounted for ~6 MB of the full-fat bundle; v2 drops it. +- **Shared infrastructure with `mix-dns` and `mix-websocket`.** The same tunnel handles all three. +- **IPR exit policies apply.** What was allowed by your previous Network Requester may not be allowed by your default IPR; pin one with `preferredIpr` if you need a specific exit policy. + +--- +title: mix-dns: Hostname Resolution Over the Nym Mixnet +description: TypeScript package that resolves hostnames through the Nym mixnet as UDP DNS via an IPR exit gateway. +url: https://nym.com/docs/developers/mix-dns +--- + +# mix-dns + +[`@nymproject/mix-dns`](https://www.npmjs.com/package/@nymproject/mix-dns) resolves hostnames to IPs through the Nym mixnet. The query travels as a UDP datagram to a public resolver via the [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) exit gateway, not through the browser or OS resolver. + +```text +┌──────────────────────────────────────────────────────────────┐ +│ Your browser app │ +│ └─ mixDNS('example.com') │ +│ └─ mix-tunnel (smolmix-wasm, Web Worker) │ +│ └─ UDP datagram via the IPR │ +│ └─ public resolver (default 8.8.8.8:53) │ +└──────────────────────────────────────────────────────────────┘ +``` + +The resolver sees a query from the IPR's IP, not yours, and the browser's own resolver path (the OS stub resolver, any local DoH) is bypassed entirely. + +## When to use it + +`mix-dns` is for cases where you need the resolved IP itself, not a connection that uses it. [`mix-fetch`](/developers/mix-fetch) and [`mix-websocket`](/developers/mix-websocket) already resolve via the mixnet internally; you don't need to call `mixDNS` before either. + +Direct uses: + +- Validate that a hostname resolves to an expected IP range before connecting through any path. +- Build IP-based allow / deny lists for an app that performs the connection itself. +- Probe whether a hostname is reachable from the IPR exit's perspective without opening a connection. + +## In this section + +- [Get started](/developers/mix-dns/get-started): install and resolve your first hostname. +- [Reference & security](/developers/mix-dns/guides): configure the resolver, and what it sees. +- [TypeDoc reference](/developers/mix-dns/api/globals): generated from the source. +- [Browser example](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-dns/browser): a runnable example app. + +--- +title: Get started with mix-dns +description: Install @nymproject/mix-dns and resolve a hostname through the Nym mixnet. +url: https://nym.com/docs/developers/mix-dns/get-started +--- + +# Get started + +## Installation + +```bash +npm install @nymproject/mix-dns +``` + +ESM only, with the worker and WASM inlined via [`mix-tunnel`](/developers/mix-tunnel/get-started#installation); no bundler config needed. + +## Quick start + +```ts + +await setupMixTunnel(); + +const ip = await mixDNS('example.com'); +console.log(ip); // e.g. an IPv4 address string + +await disconnectMixTunnel(); +``` + +`mixDNS` returns the first resolved address as a string: an IPv4 A record when available, otherwise IPv6. It rejects if the hostname cannot be resolved. To resolve and immediately use the result via `mixFetch`, the simpler path is to skip `mixDNS` entirely and call `mixFetch('https://example.com')`, which handles resolution itself. + +Resolve hostnames live in the [mixnet playground](/developers/playground), with a tunnel-vs-clearnet (DoH) comparison. + +--- +title: mix-dns reference & security +description: Configure the DNS resolver used by mix-dns, and what the resolver sees through the IPR exit. +url: https://nym.com/docs/developers/mix-dns/guides +--- + +# Reference & security + +## Configuration + +The DNS resolver is configured at tunnel setup, not per-call. Pass the resolver in `setupMixTunnel`: + +```ts +await setupMixTunnel({ + // Set the resolver explicitly. Defaults are 8.8.8.8:53 primary and + // 1.1.1.1:53 fallback. Both fields take a `host:port` socket address; + // fallbackDns is used if the primary fails to respond. + primaryDns: '8.8.8.8:53', + fallbackDns: '1.1.1.1:53', +}); +``` + +The full options surface is documented under [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts). + +## Security model + +`mix-dns` follows the shared [mixnet exit security model](/developers/concepts/exit-security). The transport-specific exposure: at the IPR exit the query leaves as a plain UDP DNS request to the resolver, so the resolver sees the queried hostname and the IPR's IP, never yours. There is no TLS to terminate; the query and response are plaintext on the IPR-to-resolver leg. + +At the resolver the query is plaintext UDP. The resolver can read the hostname you are looking up, while the mixnet keeps it from learning who you are. Choosing `8.8.8.8` vs `1.1.1.1` only changes which third party sees the queries; both see them coming from the IPR. To remove the resolver from your trust set, pick one you already trust, or layer DNS-over-HTTPS via `mixFetch` to a DoH endpoint instead of `mixDNS`. + +--- +title: mix-websocket: WebSocket Over the Nym Mixnet +description: TypeScript package that exposes a WebSocket-like class for WS and WSS traffic routed through the Nym mixnet. +url: https://nym.com/docs/developers/mix-websocket +--- + +# mix-websocket + +[`@nymproject/mix-websocket`](https://www.npmjs.com/package/@nymproject/mix-websocket) exposes `MixWebSocket`, a class that mirrors the browser [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) API for traffic that needs to travel through the Nym mixnet. WS and WSS endpoints are reached via the [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) exit gateway; the destination server sees the connection coming from the IPR's IP, not yours. + +```text +┌──────────────────────────────────────────────────────────────┐ +│ Your browser app │ +│ └─ new MixWebSocket('wss://...') │ +│ └─ mix-tunnel (smolmix-wasm, Web Worker) │ +│ └─ smoltcp userspace TCP/IP + rustls TLS │ +│ └─ Nym mixnet → IPR exit gateway │ +│ └─ destination WS server │ +└──────────────────────────────────────────────────────────────┘ +``` + +The TLS handshake (for `wss://` targets) terminates inside the WASM bundle, end-to-end with the destination. The IPR sees TCP frames addressed to the destination's IP and port; for WSS, the payload is TLS ciphertext. + +## In this section + +- [Get started](/developers/mix-websocket/get-started): install and open your first mixnet WebSocket. +- [Reference](/developers/mix-websocket/guides): sending and receiving, error handling, configuration. +- [Concepts & security](/developers/mix-websocket/concepts): how it differs from the browser WebSocket, and the security model. +- [TypeDoc reference](/developers/mix-websocket/api/globals): generated from the source. +- [Browser example](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-websocket/browser): a runnable example app. + +--- +title: Get started with mix-websocket +description: Install @nymproject/mix-websocket and open a WebSocket through the Nym mixnet. +url: https://nym.com/docs/developers/mix-websocket/get-started +--- + +# Get started + +## Installation + +```bash +npm install @nymproject/mix-websocket +``` + +ESM only, with the worker and WASM inlined via [`mix-tunnel`](/developers/mix-tunnel/get-started#installation); no bundler config needed. + +## Quick start + +Echo against `wss://echo.websocket.org`, the same endpoint the smolmix dev tool uses: + +```ts + +await setupMixTunnel(); + +const ws = new MixWebSocket('wss://echo.websocket.org'); + +ws.addEventListener('open', () => { + console.log('connected'); + ws.send('hello mixnet'); +}); + +ws.addEventListener('message', (e) => { + console.log('received:', e.data); + ws.close(); +}); + +ws.addEventListener('close', () => console.log('closed')); +ws.addEventListener('error', (e) => console.error('error:', e)); +``` + +Or `await` on the upgrade instead of subscribing to the `open` event: + +```ts +const ws = new MixWebSocket('wss://echo.websocket.org'); +await ws.opened(); +ws.send('hello mixnet'); +``` + +Open a `MixWebSocket` live in the [mixnet playground](/developers/playground), which echoes messages and runs an echo burst over the live mixnet. + +--- +title: mix-websocket guides +description: Send and receive frames, handle errors, and configure the tunnel for mix-websocket. +url: https://nym.com/docs/developers/mix-websocket/guides +--- + +# Reference + +## Sending and receiving + +```ts +ws.addEventListener('message', (e) => { + if (typeof e.data === 'string') { + console.log('text frame:', e.data); + } else { + // ArrayBuffer for binary frames + console.log('binary frame:', e.data.byteLength, 'bytes'); + } +}); + +// Send a text frame +await ws.send('hello'); + +// Send a binary frame +await ws.send(new Uint8Array([0x01, 0x02, 0x03])); +``` + +`send()` rejects if called when `readyState` is not `OPEN`. Use `await ws.opened()` to gate the first send, and check `ws.readyState` if you keep references around for later use. + +## Error handling + +The `error` event carries a non-standard `.message` field with the underlying cause (the standard `Event` carries no payload). Use it for diagnostics: + +```ts +ws.addEventListener('error', (e) => { + const msg = (e as Event & { message?: string }).message; + console.error('mix-websocket failure:', msg); +}); +``` + +If the upgrade fails before `open` fires, `MixWebSocket` transitions to `CLOSED` and dispatches `error`. `opened()` resolves either on `open` or on the failure event, so callers don't hang. + +## Configuration + +`MixWebSocket` has no per-instance configuration beyond `url` and `protocols`. Tunnel-level options live on `setupMixTunnel`, the same shared call that mix-fetch and mix-dns use: + +```ts +await setupMixTunnel({ + preferredIpr: '...', + disableCoverTraffic: true, +}); +``` + +See [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts) for the full surface. + +--- +title: mix-websocket concepts & security +description: How MixWebSocket differs from the browser WebSocket, and what the IPR exit sees. +url: https://nym.com/docs/developers/mix-websocket/concepts +--- + +# Concepts & security + +## Differences from the browser WebSocket + +`MixWebSocket` extends `EventTarget` and mirrors the standard WebSocket surface where it makes sense. Differences are intentional and follow from the underlying WASM transport: + +| Browser `WebSocket` | `MixWebSocket` | Why | +|---|---|---| +| Synchronous constructor; readiness via the `open` event | Asynchronous constructor; readiness via `await ws.opened()` or the `open` event | The mixnet connect, smoltcp socket open, and TLS handshake all happen in the worker. | +| `binaryType: 'blob' \| 'arraybuffer'` (default Blob) | `binaryType` is fixed to `'arraybuffer'` | Blob construction in a Web Worker requires a transferable; ArrayBuffer is the lowest common denominator. | +| `bufferedAmount` | Not exposed | Writes queue inside the worker; no main-thread byte counter. | +| `send()` returns `void` synchronously | `send()` returns `Promise` | The send hops the worker boundary. | +| `close(code, reason)` returns `void` | `close(code, reason)` returns `Promise` | Same reason. | +| `protocols` argument supports string or string[] | Same | (no difference) | + +The standard `open`, `message`, `close`, and `error` events fire as you would expect. `MessageEvent.data` is `string` for text frames and `ArrayBuffer` for binary frames. + +## Security model + +`mix-websocket` follows the shared [mixnet exit security model](/developers/concepts/exit-security). What that means specifically for WS/WSS: + +| At the IPR exit | What's visible | +|---|---| +| Secure (`wss://`) | Destination IP and port. Frames are TLS ciphertext, terminating at the destination. | +| Plain (`ws://`) | Destination IP and port, plus every frame in plaintext. | + +TLS terminates inside the WASM bundle (via [`rustls`](https://docs.rs/rustls) in smolmix-wasm), not in the browser. Mozilla's CA bundle is compiled into the WASM. Use `wss://` for any non-public traffic; `ws://` is visible to the IPR in full. + +--- +title: mix-* Architecture: How the Browser Mixnet Packages Are Wired +description: The shared browser architecture behind mix-tunnel, mix-fetch, mix-dns, and mix-websocket: one Web Worker, one WASM instance, a Comlink boundary, and a smoltcp + rustls stack reaching the mixnet via an IPR. +url: https://nym.com/docs/developers/mix-architecture +--- + +# Architecture + +The four mix-* packages ([`mix-tunnel`](/developers/mix-tunnel), [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), and [`mix-websocket`](/developers/mix-websocket)) are not four independent clients. They are thin facades over a single shared tunnel, which runs in a Web Worker. This page explains that shared machinery once, so the per-package pages can stay focused on their own API. + +```text +Main thread (your app) + mix-fetch, mix-dns, mix-websocket + │ each re-exports mix-tunnel's controls and calls into it + ▼ + mix-tunnel → Comlink proxy + │ + ▼ postMessage (structured clone), into the worker +Web Worker (one per page) + smolmix-wasm + ├─ IPR client → WebSocket (WSS) to entry gateway + ├─ smoltcp (userspace TCP/IP stack) + └─ rustls (TLS, Mozilla CA bundle compiled in) + │ + ▼ + Nym mixnet: entry → 3 mix layers → IPR exit → internet +``` + +## The package family + +Only `mix-tunnel` owns the tunnel. The three feature packages each depend on it and **re-export** its controls (`setupMixTunnel`, `disconnectMixTunnel`, and `getTunnelState`) alongside their own operation: + +| Package | Adds | Re-exports from mix-tunnel | +|---|---|---| +| `mix-tunnel` | the tunnel itself | (owns them) | +| `mix-fetch` | `mixFetch`, `createMixFetch` | setup / disconnect / state | +| `mix-dns` | `mixDNS` | setup / disconnect / state | +| `mix-websocket` | `MixWebSocket` | setup / disconnect / state | + +So `import { setupMixTunnel, mixFetch } from '@nymproject/mix-fetch'` and `import { setupMixTunnel } from '@nymproject/mix-tunnel'` reach the **same** `setupMixTunnel`. You rarely import `mix-tunnel` directly; you get it transitively through whichever feature package you use. + +## One tunnel, one WASM instance + +The bundler deduplicates [`@nymproject/mix-tunnel`](https://www.npmjs.com/package/@nymproject/mix-tunnel) to a single module, so no matter how many feature packages a page imports, there is exactly one Web Worker and one `smolmix-wasm` instance. Any feature package reaches that same instance through `getMixTunnel`. Bringing the tunnel up with `setupMixTunnel`, though, is a one-time operation: the first call succeeds and a second rejects with `tunnel already initialised`, so call it once. + +This is why the tunnel is configured once, at the first `setupMixTunnel`, and why options passed to a later call have no effect until teardown. It is also why a single connection to the entry gateway, a single IPR exit, and a single DNS cache are shared across all your `mixFetch` / `mixDNS` / `MixWebSocket` traffic. + +## The worker boundary + +The mixnet work (Sphinx packet construction, cover traffic, Poisson send timing, the smoltcp poll loop) is CPU-bound and must not block the UI thread. So `mix-tunnel` runs all of it in a Web Worker and talks to it over [Comlink](https://github.com/GoogleChromeLabs/comlink), which wraps `postMessage` in an async RPC. The main thread holds a `Comlink.Remote` proxy; every call (`mixFetch`, `mixDNS`, `ws.send`) is an `await` that hops the worker boundary. That boundary is the reason `MixWebSocket.send()` and `.close()` return promises where the browser `WebSocket` returns `void`. + +**The `proxy` re-export.** `mix-websocket` needs to pass a message callback *into* the worker. Comlink marks a value as "transfer this by proxy, not by clone" using a `Symbol` that is created per module instance. If `mix-websocket` bundled its own copy of Comlink, that symbol would not match the one the worker-owning module's serialiser checks for, and the callback would fall through to structured clone, which cannot clone functions, so it throws. To avoid that, `mix-tunnel` re-exports `proxy`, and `mix-websocket` imports it from there, so both sides share one Comlink instance and the marker symbol matches. + +## Inside the worker + +The worker hosts `smolmix-wasm`, the WebAssembly build of the Rust [`smolmix`](/developers/smolmix) crate. Three pieces do the work: + +- **IPR client**: opens a WebSocket (WSS by default, via `forceTls`) to a Nym entry gateway and speaks the mixnet protocol, exiting at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router). +- **`smoltcp`**: a userspace TCP/IP stack. Because the browser exposes no raw sockets, smolmix runs its own TCP and UDP over the mixnet's IP transport. A reactor polls smoltcp and wakes the relevant tasks when data arrives. +- **`rustls`**: terminates TLS for `https://` and `wss://` end-to-end with the destination, with the Mozilla CA bundle compiled into the WASM. The IPR sees only ciphertext for encrypted targets. + +The tunnel is **one-shot per WASM instance**: `setupMixTunnel` can initialise it once. After `disconnectMixTunnel`, the instance is spent and the page must reload to build a new tunnel. + +## Tunnel lifecycle + +```text +(no tunnel) ──setupMixTunnel()──▶ connecting ──▶ ready ──┐ + │ mixFetch / mixDNS / MixWebSocket + ▼ + shutdown ◀──disconnectMixTunnel()── (still ready) +``` + +`getTunnelState()` reflects this as `connecting | ready | shutting_down | shutdown | failed` (see [tunnel state](/developers/mix-tunnel/guides#tunnel-state); the diagram shows the happy path, `shutting_down` is the transient during teardown and `failed` carries a `reason`). The transitions are coarse; the fine-grained gateway, IPR-discovery, and smoltcp events are logged to the browser console when the tunnel is brought up with `debug: true`. + +## Going deeper + +- The native crate and its design: [`smolmix`](/developers/smolmix). +- The package sources, including bundler/WASM-inlining specifics and the worker plumbing: [`sdk/typescript/packages`](https://github.com/nymtech/nym/tree/develop/sdk/typescript/packages) and the WASM crate under [`wasm/smolmix`](https://github.com/nymtech/nym/tree/develop/wasm/smolmix). +- See it run: the [mixnet playground](/developers/playground). + +--- +title: TypeScript SDK: Mixnet Messaging for the Browser +description: TypeScript SDK for end-to-end mixnet messaging from a browser app. For HTTP, DNS, and WebSocket over the mixnet, see the mix-fetch / mix-dns / mix-websocket packages. url: https://nym.com/docs/developers/typescript --- # 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. +`@nymproject/sdk` is the browser-side raw messaging SDK. It opens a Nym mixnet client in a Web Worker and exposes a message-passing API: send text or binary payloads to another Nym address, receive payloads via event subscriptions, optionally reply anonymously with SURBs. -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. +```text +┌──────────────────────────────────────────────────────────────┐ +│ Your browser app (alice) │ +│ └─ Nym Mixnet Client (WASM, Web Worker) │ +│ └─ WebSocket to entry gateway │ +│ └─ Nym mixnet (entry → 3 mix layers → exit) │ +│ └─ Peer MixnetClient (bob) │ +│ └─ Your peer's app │ +└──────────────────────────────────────────────────────────────┘ +``` + +Both ends run a Nym client. Sphinx encryption protects every hop end-to-end; neither gateway nor any mix node can link sender to receiver. This is the **end-to-end messaging** path, where you control both sides. + +If you need HTTP, DNS, or WebSocket connections through the mixnet to third-party services, this isn't the right SDK. Use [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), or [`mix-websocket`](/developers/mix-websocket), all built on [`mix-tunnel`](/developers/mix-tunnel) for IPR exit routing. + +For background on Sphinx, the mixnet exit model, and what the exit gateway can see, see [Exit security](/developers/concepts/exit-security). ## Packages - **mixFetch** +| Package | Variant | When to use | +|---|---|---| +| `@nymproject/sdk` | ESM | Modern project, configurable bundler. See [Bundling](/developers/typescript/bundling/bundling). | +| `@nymproject/sdk-full-fat` | ESM, inlined | Modern project, no bundler config available. Larger bundle. | +| `@nymproject/sdk-commonjs` | CJS | Legacy project, configurable bundler. | +| `@nymproject/sdk-full-fat-commonjs` | CJS, inlined | Legacy project, no bundler config available. Larger bundle. | +| `@nymproject/contract-clients` | ESM | Query and execute Nym smart contracts on the Nyx chain. Separate transport (CosmWasm RPC, not the mixnet). See [Smart Contracts](/developers/typescript/smart-contracts). | - 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 - -
-
-
- - **Mixnet Client** - - Send and receive text and binary messages over the Nym mixnet - -
-
-
- - **Nym Smart Contracts** - - Query and execute methods on the smart contracts that run the Nym mixnet - -### 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. - -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. - -## 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 -``` +The `*-full-fat` variants embed the WASM and Web Worker as Base64 in the JS bundle. Bundle size is large (tens of MB). Prefer a standard variant where bundler configuration is possible. ## 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 -// 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 - 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); + console.log('Received:', 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 - -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 - ---- -title: mixFetch Example: Private HTTP Requests -description: Replace browser fetch with mixFetch to route HTTP requests through the Nym mixnet. Covers setup, CA certificates, TLS configuration, and usage examples. -url: https://nym.com/docs/developers/typescript/examples/mix-fetch ---- - -# mixFetch - -An easy way to secure parts or all of your web app is to replace calls to [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) with `mixFetch`. It works the same as vanilla `fetch`: it's a proxied wrapper around the original function. - -Things to be aware of: - -- **CA certificates** are bundled into the WASM binary at build time. They're updated with each SDK release, so if you hit a certificate error, update to the latest `@nymproject/mix-fetch-full-fat` version. -- **HTTPS and WSS.** When serving your app over HTTPS, the mixnet connection must also use Secure WebSockets to avoid a [mixed content](https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content) error. Set `forceTls: true` in your `SetupMixFetchOps` config (see below) and the SDK will automatically select a WSS-capable gateway. -- `mixFetch` supports **concurrent requests** (up to 10) to the same or different URLs. - -## Environment Setup - -Create a new project with Vite: - -```bash -npm create vite@latest -``` - -Choose React + TypeScript, then: - -```bash -cd -npm i -npm run dev -``` - -## Installation - -```bash -npm install @nymproject/mix-fetch-full-fat @mui/material @emotion/react @emotion/styled -``` - -The MUI packages are used by the example UI below. If you only need `mixFetch` itself, install just `@nymproject/mix-fetch-full-fat`. - -## Configuration - -```ts - -const mixFetchOptions: SetupMixFetchOps = { - clientId: "my-app", - preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1", - mixFetchOverride: { - requestTimeoutMs: 60_000, - }, - forceTls: true, // use Secure WebSockets (required when serving over HTTPS) -}; -``` - -`preferredGateway` is optional. If omitted, the SDK auto-selects a gateway. You can pin a specific one via [Harbourmaster](https://harbourmaster.nymtech.net/). - -## Full Example - -This example shows explicit initialization via `createMixFetch`, single URL fetch, and concurrent requests. Results appear both in the UI and in a visible log panel. - -For this example we use the `full-fat` version of the ESM SDK. If you use the unbundled ESM variant, make sure your [bundler configuration](/developers/typescript/bundling/bundling) copies the WASM and web worker files to the output bundle. - -```tsx - -const defaultUrl = - "https://nymtech.net/.wellknown/network-requester/exit-policy.txt"; -const args = { mode: "unsafe-ignore-cors" }; -const mixFetchOptions: SetupMixFetchOps = { - clientId: "docs-mixfetch-demo", - preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1", - mixFetchOverride: { - requestTimeoutMs: 60_000, - }, +await nym.client.start({ + clientId: crypto.randomUUID(), + nymApiUrl: 'https://validator.nymtech.net/api', forceTls: true, -}; +}); -// Log entry type for the visible log panel -type LogLevel = "info" | "error" | "send" | "receive"; -type LogEntry = { timestamp: string; message: string; level: LogLevel }; - -const logColors: Record = { - info: "gray", - error: "red", - send: "blue", - receive: "green", -}; - -const logLabels: Record = { - info: "INFO", - error: "ERROR", - send: "SEND", - receive: "RECV", -}; - -export const MixFetch = () => { - // MixFetch initialization state - const [status, setStatus] = useState<"idle" | "starting" | "ready" | "error">("idle"); - const [errorMsg, setErrorMsg] = useState(null); - - // Log panel state - const [logs, setLogs] = useState([]); - const logEndRef = useRef(null); - - // Single fetch state - const [url, setUrl] = useState(defaultUrl); - const [html, setHtml] = useState(); - const [busy, setBusy] = useState(false); - - // Concurrent fetch state - const [concurrentResults, setConcurrentResults] = useState([]); - const [concurrentBusy, setConcurrentBusy] = useState(false); - - // Auto-scroll log panel to bottom - useEffect(() => { - logEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [logs]); - - // Helper to add a timestamped log entry - const addLog = (message: string, level: LogLevel) => { - const timestamp = new Date().toISOString().substring(11, 23); - setLogs((prev) => [...prev, { timestamp, message, level }]); - }; - - // Initialize MixFetch explicitly via createMixFetch - const handleStart = async () => { - try { - setStatus("starting"); - setErrorMsg(null); - addLog("Starting MixFetch...", "info"); - await createMixFetch(mixFetchOptions); - setStatus("ready"); - addLog("MixFetch is ready!", "info"); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - setStatus("error"); - setErrorMsg(msg); - addLog(`Error: ${msg}`, "error"); - } - }; - - // Single URL fetch: reuses the existing MixFetch singleton - const handleFetch = async () => { - try { - setBusy(true); - setHtml(undefined); - addLog(`Sending request to ${url}...`, "send"); - const response = await mixFetch(url, args, mixFetchOptions); - const resHtml = await response.text(); - setHtml(resHtml); - addLog(`Response received (${resHtml.length} bytes)`, "receive"); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - addLog(`Fetch error: ${msg}`, "error"); - } finally { - setBusy(false); - } - }; - - // Send 5 concurrent requests to different URLs on the same domain - const handleConcurrentFetch = async () => { - const baseUrl = "https://jsonplaceholder.typicode.com/posts/"; - const count = 5; - try { - setConcurrentBusy(true); - setConcurrentResults([]); - addLog( - `Starting ${count} concurrent requests to ${baseUrl}1-${count}...`, - "send", - ); - const requests = Array.from({ length: count }, (_, i) => { - const targetUrl = `${baseUrl}${i + 1}`; - return mixFetch(targetUrl, args, mixFetchOptions) - .then((res) => res.json()) - .then((json: { id: number; title: string }) => { - const entry = `[${json.id}] ${json.title}`; - addLog(entry, "receive"); - return entry; - }); - }); - const results = await Promise.all(requests); - setConcurrentResults(results); - addLog(`All ${count} concurrent requests completed!`, "info"); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - addLog(`Concurrent fetch error: ${msg}`, "error"); - } finally { - setConcurrentBusy(false); - } - }; - - const isReady = status === "ready"; - - return ( - - {/* Start MixFetch */} - - Start MixFetch - - {status === "starting" && } - - {status === "idle" ? "Not started" : - status === "starting" ? "Starting..." : - status === "ready" ? "Ready" : - `Error: ${errorMsg}`} - - {/* Fetch controls: disabled until MixFetch is ready */} - - {/* Single fetch */} - - setUrl(e.target.value)} - /> - - Fetch - - {busy && } - {html && ( - <> - Response - - {html} - - - )} - - {/* Concurrent fetch */} - - Concurrent Requests - - Send 5 Concurrent Requests (posts/1-5) - - {concurrentBusy && } - {concurrentResults.length > 0 && ( - - {concurrentResults.map((result, i) => ( - {result} - ))} - - )} - - {/* Log Panel */} - {logs.length > 0 && ( - - Log - {logs.map((entry, i) => ( - - {entry.timestamp} [{logLabels[entry.level]}] {entry.message} - - ))} - - )} - - ); -}; +await nym.client.send({ + payload: { message: 'hello mixnet', mimeType: 'text/plain' }, + recipient: nym.client.selfAddress(), +}); ``` +For the full walkthrough including SURB replies and disconnect, see [Quick Start](/developers/typescript/quick-start). + +## Smart contracts + +[`@nymproject/contract-clients`](https://www.npmjs.com/package/@nymproject/contract-clients) provides query and signing clients for every Nym smart contract on the Nyx chain (Mixnet, Coconut DKG, Vesting, Service Provider Directory, and more). It uses [CosmJS](https://github.com/cosmos/cosmjs) and the Nyx RPC endpoint, not the mixnet, so payloads are not routed through Sphinx. + +See [Smart Contracts](/developers/typescript/smart-contracts) for query and execute examples, and [Cosmos Kit](/developers/typescript/cosmos-kit) for wallet integration. + +## Where to go next + +| | | +|---|---| +| [Quick Start](/developers/typescript/quick-start) | Vanilla TypeScript walkthrough of the messaging API. | +| [Smart Contracts](/developers/typescript/smart-contracts) | Query and execute Nym contracts via `@nymproject/contract-clients`. | +| [Cosmos Kit](/developers/typescript/cosmos-kit) | Wallet connection (Keplr, Ledger, Wallet Connect) for Nyx. | +| [Bundling](/developers/typescript/bundling/bundling) | Webpack and esbuild configurations for WASM + Web Workers. | +| [TypeDoc Reference](/developers/typescript/api/sdk/globals) | Generated API reference for `@nymproject/sdk`. | + --- -title: TypeScript Mixnet Client Example -description: Send and receive private messages in the browser using the Nym TypeScript SDK. Includes setup, SURB anonymous replies, and environment configuration. -url: https://nym.com/docs/developers/typescript/examples/mixnet +title: TypeScript SDK Quick Start +description: Send and receive messages over the Nym mixnet using @nymproject/sdk. Vanilla TypeScript, no framework. +url: https://nym.com/docs/developers/typescript/quick-start --- -# Mixnet Client +# Quick start -The [`SDK Client`](https://www.npmjs.com/package/@nymproject/sdk) lets you send and receive messages over the Nym mixnet. - -The client is message-based: it sends one-way messages to another client's address. Replying can be achieved in two ways: -- Reveal the sender's address to the recipient (as part of the payload) -- Use a SURB (single use reply block) that lets the recipient reply without compromising the identity of either party - -## Environment Setup - -Create a new project with Vite: - -```bash -npm create vite@latest -``` - -Choose React + TypeScript, then: - -```bash -cd -npm i -npm run dev -``` +A minimal, framework-free example. Connect to the mixnet, subscribe to incoming messages, send a message to yourself, disconnect. ## Installation @@ -4216,127 +4537,72 @@ npm run dev npm install @nymproject/sdk-full-fat ``` -## Full Example +`sdk-full-fat` inlines the WASM and Web Worker as Base64, so no bundler configuration is needed. For smaller bundles, install `@nymproject/sdk` instead and follow the [Bundling guide](/developers/typescript/bundling/bundling). -This example creates a Mixnet client, connects to a gateway, and provides a UI for sending and receiving messages through the mixnet. +## Send and receive -For this example we use the `full-fat` version of the ESM SDK. If you use the unbundled ESM variant, make sure your [bundler configuration](/developers/typescript/bundling/bundling) copies the WASM and web worker files to the output bundle. +```ts -```ts copy filename="App.tsx" +const nymApiUrl = 'https://validator.nymtech.net/api'; - createNymMixnetClient, - NymMixnetClient, - Payload, -} from "@nymproject/sdk-full-fat"; +const nym = await createNymMixnetClient(); -const nymApiUrl = "https://validator.nymtech.net/api"; +nym.events.subscribeToConnected((e) => { + console.log('Connected:', e.args.address); +}); -export const Traffic = () => { - const [nym, setNym] = useState(); - const [selfAddress, setSelfAddress] = useState(); - const [recipient, setRecipient] = useState(); - const [payload, setPayload] = useState(); - const [receivedMessage, setReceivedMessage] = useState(); - const [buttonEnabled, setButtonEnabled] = useState(false); +nym.events.subscribeToTextMessageReceivedEvent((e) => { + console.log('Received:', e.args.payload); +}); - const init = async () => { - const client = await createNymMixnetClient(); - setNym(client); +await nym.client.start({ + clientId: crypto.randomUUID(), + nymApiUrl, + forceTls: true, // WSS to entry gateway, required on HTTPS pages +}); - // start the client and connect to a gateway - await client?.client.start({ - clientId: crypto.randomUUID(), - nymApiUrl, - forceTls: true, // force WSS - }); +const selfAddress = nym.client.selfAddress(); +await nym.client.send({ + payload: { message: 'hello mixnet', mimeType: 'text/plain' }, + recipient: selfAddress, +}); - // check when is connected and set the self address - client?.events.subscribeToConnected((e) => { - const { address } = e.args; - setSelfAddress(address); - }); +// ... receive event fires, message logged ... - // show whether the client is ready or not - client?.events.subscribeToLoaded((e) => { - console.log("Client ready: ", e.args); - }); - - // show message payload content when received - client?.events.subscribeToTextMessageReceivedEvent((e) => { - console.log(e.args.payload); - setReceivedMessage(e.args.payload); - }); - }; - - const stop = async () => { - await nym?.client.stop(); - }; - - const send = () => - payload && recipient && nym?.client.send({ payload, recipient }); - - useEffect(() => { - init(); - return () => { - stop(); - }; - }, []); - - useEffect(() => { - if (recipient && payload) { - setButtonEnabled(true); - } else { - setButtonEnabled(false); - } - }, [recipient, payload]); - - if (!nym || !selfAddress) { - return ( - - ); - } - - return ( - - My self address is: - {selfAddress || "loading"} - Communication through the Mixnet - setRecipient(e.target.value)} - size="small" - /> - - setPayload({ message: e.target.value, mimeType: "text/plain" }) - } - size="small" - /> -