Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 520b880b1b | |||
| b96dc11558 | |||
| 7b076ba212 | |||
| 39504b1d5f | |||
| d30bbb3f2a | |||
| 9d4690d5ad | |||
| 6cfa88c0b9 | |||
| 181e1f7526 | |||
| 69c54674cf | |||
| d15c47dbde | |||
| 7529cde148 | |||
| 9ca0f32c47 |
@@ -0,0 +1,244 @@
|
||||
// ENS-over-the-mixnet demo, ported from wasm/ens-demo. Resolve <name>.eth to an
|
||||
// address + contenthash, then fetch the IPFS site, every byte through mixFetch.
|
||||
// The tunnel lifecycle + options live in <MixTunnelSetup>; this component owns
|
||||
// the ENS flow and receives a `mixFetch` when the tunnel is ready.
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
import type { JsonRpcProvider } from 'ethers';
|
||||
import { MixTunnelSetup, type MixFetchFn } from '../shared/mixTunnel';
|
||||
import { Button, LogPanel, useLogs, box, row, input, sub, legend } from '../shared/ui';
|
||||
import { buildProvider, callMixFetch, decompressBody, expandGatewayUrl, formatSize, htmlFingerprint, renderFingerprint } from './lib';
|
||||
|
||||
const NAME_PRESETS = ['vitalik.eth', 'ens.eth', 'gregskril.eth', 'raffy.eth', 'luc.eth'];
|
||||
const RPC_PRESETS = ['https://ethereum-rpc.publicnode.com', 'https://rpc.ankr.com/eth', 'https://eth.public-rpc.com'];
|
||||
const GATEWAY_PRESETS = ['https://{cid}.ipfs.dweb.link/', 'https://dweb.link/ipfs/{cid}/'];
|
||||
|
||||
const IP_ECHO_URL = 'https://ipinfo.io/ip';
|
||||
const IP_SHAPE_RE = /^[\d.:a-f]{3,45}$/i;
|
||||
|
||||
const preStyle: React.CSSProperties = {
|
||||
maxHeight: 240,
|
||||
overflowY: 'auto',
|
||||
fontSize: 12.5,
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowWrap: 'anywhere',
|
||||
background: 'rgba(127,127,127,0.06)',
|
||||
border: '1px solid rgba(127,127,127,0.2)',
|
||||
borderRadius: 6,
|
||||
padding: '0.5rem',
|
||||
margin: '0.5rem 0 0',
|
||||
};
|
||||
|
||||
export function EnsDemo() {
|
||||
const { log, lines } = useLogs();
|
||||
const [mixFetch, setMixFetch] = useState<MixFetchFn | null>(null);
|
||||
const providerRef = useRef<JsonRpcProvider | null>(null);
|
||||
|
||||
const [ensName, setEnsName] = useState('vitalik.eth');
|
||||
const [ensRpc, setEnsRpc] = useState(RPC_PRESETS[0]);
|
||||
const [gateway, setGateway] = useState(GATEWAY_PRESETS[0]);
|
||||
const [customCid, setCustomCid] = useState('');
|
||||
const [lastCid, setLastCid] = useState<string | null>(null);
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [verifyLink, setVerifyLink] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const connected = mixFetch != null;
|
||||
const ensLog = (msg: string, colour?: 'green' | 'red' | 'orange' | 'gray') => log('ens', msg, colour);
|
||||
|
||||
function ensureProvider(): JsonRpcProvider {
|
||||
if (providerRef.current) return providerRef.current;
|
||||
const rpc = ensRpc.trim();
|
||||
if (!rpc) throw new Error('RPC URL is required');
|
||||
ensLog(`building JsonRpcProvider({ rpc: ${rpc}, transport: mixFetch })`);
|
||||
providerRef.current = buildProvider(rpc, mixFetch!, ensLog);
|
||||
return providerRef.current;
|
||||
}
|
||||
|
||||
function onReady(fn: MixFetchFn) {
|
||||
setMixFetch(() => fn);
|
||||
}
|
||||
function onDisconnect() {
|
||||
setMixFetch(null);
|
||||
providerRef.current = null;
|
||||
setLastCid(null);
|
||||
}
|
||||
|
||||
async function resolveAddress() {
|
||||
const name = ensName.trim();
|
||||
if (!name) return ensLog('Name is required', 'red');
|
||||
let provider: JsonRpcProvider;
|
||||
try {
|
||||
provider = ensureProvider();
|
||||
} catch (e) {
|
||||
return ensLog(`${e}`, 'red');
|
||||
}
|
||||
ensLog(`step 1/3: resolving ${name} via ENS Registry + Resolver`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const addr = await provider.resolveName(name);
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
if (addr) ensLog(`${name} -> ${addr} (${ms} ms total)`, 'green');
|
||||
else ensLog(`${name} has no addr record (${ms} ms)`, 'orange');
|
||||
} catch (e: any) {
|
||||
ensLog(`resolveName failed: ${e.shortMessage || e.message || e}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
async function getContenthash() {
|
||||
const name = ensName.trim();
|
||||
if (!name) return ensLog('Name is required', 'red');
|
||||
let provider: JsonRpcProvider;
|
||||
try {
|
||||
provider = ensureProvider();
|
||||
} catch (e) {
|
||||
return ensLog(`${e}`, 'red');
|
||||
}
|
||||
ensLog(`step 2/3: reading contenthash record from ${name}'s resolver`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const resolver = await provider.getResolver(name);
|
||||
if (!resolver) return ensLog(`${name} has no resolver`, 'orange');
|
||||
const content = await resolver.getContentHash();
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
if (!content) return ensLog(`${name} has no contenthash (${ms} ms)`, 'orange');
|
||||
ensLog(`contenthash: ${content} (${ms} ms total)`, 'green');
|
||||
const ipfsMatch = content.match(/^ipfs:\/\/(.+)$/);
|
||||
if (ipfsMatch) {
|
||||
setLastCid(ipfsMatch[1]);
|
||||
ensLog(`decoded CID: ${ipfsMatch[1]}`);
|
||||
} else {
|
||||
ensLog('non-IPFS scheme in contenthash; nothing to fetch', 'orange');
|
||||
}
|
||||
} catch (e: any) {
|
||||
ensLog(`contenthash lookup failed: ${e.shortMessage || e.message || e}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchIpfsCid(cid: string, label: string) {
|
||||
if (!cid) return ensLog(`${label}: CID is required`, 'red');
|
||||
const gw = gateway.trim();
|
||||
if (!gw) return ensLog('IPFS gateway is required', 'red');
|
||||
const url = expandGatewayUrl(gw, cid);
|
||||
ensLog(`${label} GET ${url}`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const raw = await callMixFetch(mixFetch!, url, {});
|
||||
const buf = await decompressBody(raw.body, raw.headers);
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
const ctype = raw.headers['content-type'] || '';
|
||||
const wireSize = raw.body ? raw.body.byteLength : 0;
|
||||
const wireNote = wireSize !== buf.byteLength ? ` (${formatSize(wireSize)} wire, decompressed)` : '';
|
||||
ensLog(`${raw.status} ${raw.statusText}: ${formatSize(buf.byteLength)} ${ctype}${wireNote} in ${ms} ms`, 'green');
|
||||
|
||||
const text = new TextDecoder('utf-8', { fatal: false }).decode(buf);
|
||||
const looksLikeHtml = ctype.includes('html') || /<html\b|<!doctype html/i.test(text.slice(0, 1000));
|
||||
if (looksLikeHtml) {
|
||||
const fp = htmlFingerprint(text);
|
||||
if (fp.title) ensLog(`page title: "${fp.title}"`, 'green');
|
||||
setPreview(renderFingerprint(fp, buf.byteLength));
|
||||
} else if (ctype.includes('json')) {
|
||||
try {
|
||||
setPreview(JSON.stringify(JSON.parse(text), null, 2));
|
||||
} catch {
|
||||
setPreview(text);
|
||||
}
|
||||
} else if (ctype.includes('text/')) {
|
||||
setPreview(text);
|
||||
} else {
|
||||
setPreview(`[binary content, ${formatSize(buf.byteLength)}, ${ctype || 'unknown type'}]`);
|
||||
}
|
||||
setVerifyLink(`https://ipfs.io/ipfs/${cid}/`);
|
||||
ensLog('Visual content check (open the link below in another tab), not CID-hash verification.', 'gray');
|
||||
} catch (e: any) {
|
||||
ensLog(`${label} fetch failed: ${e.message || e}`, 'red');
|
||||
ensLog("If this is a 403/429 or connection error, the exit IP may be rate-limited. Tick 'Use random IPR' and reload.", 'orange');
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyIp() {
|
||||
if (!mixFetch) return;
|
||||
setBusy(true);
|
||||
ensLog('comparing direct-clearnet IP vs Nym-exit IP...');
|
||||
let directIp: string;
|
||||
try {
|
||||
const text = (await (await fetch(IP_ECHO_URL)).text()).trim();
|
||||
directIp = IP_SHAPE_RE.test(text) ? text : `(unexpected: ${text})`;
|
||||
} catch (e: any) {
|
||||
directIp = `error: ${e.message || e}`;
|
||||
}
|
||||
ensLog(` your real IP (direct fetch, no Nym): ${directIp}`, 'orange');
|
||||
let nymIp: string;
|
||||
try {
|
||||
const raw = await callMixFetch(mixFetch, IP_ECHO_URL, {});
|
||||
const text = new TextDecoder().decode(raw.body).trim();
|
||||
nymIp = IP_SHAPE_RE.test(text) ? text : `(unexpected: ${text})`;
|
||||
} catch (e: any) {
|
||||
nymIp = `error: ${e.message || e}`;
|
||||
}
|
||||
ensLog(` what the upstream sees via mixFetch -> Nym: ${nymIp}`, 'green');
|
||||
if (!nymIp.startsWith('error') && !directIp.startsWith('error') && nymIp !== directIp) {
|
||||
ensLog('IPs differ. The RPC and gateway see the Nym exit, not you. Every ENS step uses the same path.', 'green');
|
||||
} else if (nymIp.startsWith('error') || directIp.startsWith('error')) {
|
||||
ensLog('Could not complete the comparison. Try again, or reconnect with a different IPR.', 'red');
|
||||
} else {
|
||||
ensLog('IPs match. The mixnet route may not be active, or the IP service is behind a shared CDN. Try again.', 'red');
|
||||
}
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ margin: '1.5rem 0' }}>
|
||||
<MixTunnelSetup onReady={onReady} onDisconnect={onDisconnect} clientIdPrefix="ens-demo" />
|
||||
|
||||
<div style={box}>
|
||||
<div style={legend}>ENS lookup</div>
|
||||
<div style={row}>
|
||||
<Button onClick={verifyIp} disabled={!connected || busy}>Verify IP routing</Button>
|
||||
<span style={sub}>Confirms traffic exits through Nym. The comparison makes one direct (clearnet) call to ipinfo.io, so you will see a single ipinfo.io row in the Network tab.</span>
|
||||
</div>
|
||||
|
||||
<div style={row}>
|
||||
<label style={sub}>Name</label>
|
||||
<select style={input} value={ensName} onChange={(e) => setEnsName(e.target.value)} disabled={!connected}>
|
||||
{NAME_PRESETS.map((n) => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
<input style={input} value={ensName} onChange={(e) => setEnsName(e.target.value)} disabled={!connected} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>RPC</label>
|
||||
<select style={input} value={RPC_PRESETS.includes(ensRpc) ? ensRpc : ''} onChange={(e) => { setEnsRpc(e.target.value); providerRef.current = null; }} disabled={!connected}>
|
||||
{RPC_PRESETS.map((u) => <option key={u} value={u}>{u}</option>)}
|
||||
</select>
|
||||
<input style={input} value={ensRpc} onChange={(e) => { setEnsRpc(e.target.value); providerRef.current = null; }} disabled={!connected} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>IPFS gateway</label>
|
||||
<select style={input} value={GATEWAY_PRESETS.includes(gateway) ? gateway : ''} onChange={(e) => setGateway(e.target.value)} disabled={!connected}>
|
||||
{GATEWAY_PRESETS.map((g) => <option key={g} value={g}>{g}</option>)}
|
||||
</select>
|
||||
<input style={input} value={gateway} onChange={(e) => setGateway(e.target.value)} disabled={!connected} />
|
||||
</div>
|
||||
|
||||
<div style={row}>
|
||||
<Button onClick={resolveAddress} disabled={!connected}>1. Resolve address</Button>
|
||||
<Button onClick={getContenthash} disabled={!connected}>2. Get contenthash</Button>
|
||||
<Button onClick={() => lastCid && fetchIpfsCid(lastCid, 'step 3/3:')} disabled={!connected || !lastCid}>3. Fetch from IPFS</Button>
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>Or fetch any CID</label>
|
||||
<input style={input} value={customCid} onChange={(e) => setCustomCid(e.target.value)} placeholder="bafybe... or Qm..." disabled={!connected} />
|
||||
<Button onClick={() => fetchIpfsCid(customCid.trim(), 'custom')} disabled={!connected || !customCid.trim()}>Fetch</Button>
|
||||
</div>
|
||||
|
||||
<LogPanel lines={lines('ens')} placeholder="Connect the tunnel, then run a lookup." />
|
||||
{verifyLink && (
|
||||
<div style={{ ...sub, marginTop: '0.4rem' }}>
|
||||
verify visually in another tab: <a href={verifyLink} target="_blank" rel="noopener noreferrer">{verifyLink}</a>
|
||||
</div>
|
||||
)}
|
||||
{preview != null && <pre style={preStyle}>{preview}</pre>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// ENS demo logic, ported from wasm/ens-demo/index.js and decoupled from the DOM.
|
||||
// The interesting bit is buildProvider(): ethers v6 lets us swap its HTTP
|
||||
// transport by assigning FetchRequest.getUrlFunc, so every JSON-RPC call routes
|
||||
// through mixFetch. To ethers, mixFetch looks like any fetch; to the mixnet,
|
||||
// ethers looks like any caller.
|
||||
|
||||
import { JsonRpcProvider, FetchRequest } from 'ethers';
|
||||
import type { MixFetchFn } from '../shared/mixTunnel';
|
||||
import type { Colour } from '../shared/ui';
|
||||
|
||||
export type LogFn = (msg: string, colour?: Colour) => void;
|
||||
export type LogLinkFn = (prefix: string, url: string) => void;
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
// base58btc decoder (Bitcoin/IPFS variant; alphabet excludes 0/O/I/l), used to
|
||||
// take CIDv0 strings apart into their raw multihash bytes.
|
||||
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
const BASE58_MAP: Record<string, number> = (() => {
|
||||
const m: Record<string, number> = Object.create(null);
|
||||
for (let i = 0; i < BASE58_ALPHABET.length; i++) m[BASE58_ALPHABET[i]] = i;
|
||||
return m;
|
||||
})();
|
||||
|
||||
function base58Decode(str: string): Uint8Array {
|
||||
let zeros = 0;
|
||||
while (zeros < str.length && str[zeros] === '1') zeros++;
|
||||
let value = 0n;
|
||||
for (const c of str) {
|
||||
if (!(c in BASE58_MAP)) throw new Error(`invalid base58 char: ${c}`);
|
||||
value = value * 58n + BigInt(BASE58_MAP[c]);
|
||||
}
|
||||
const bytes: number[] = [];
|
||||
while (value > 0n) {
|
||||
bytes.unshift(Number(value & 0xffn));
|
||||
value >>= 8n;
|
||||
}
|
||||
for (let i = 0; i < zeros; i++) bytes.unshift(0);
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
// RFC 4648 base32, lowercase, no padding (the multibase 'b' alphabet CIDv1 uses).
|
||||
const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
|
||||
|
||||
function base32Encode(bytes: Uint8Array): string {
|
||||
let bits = 0;
|
||||
let value = 0;
|
||||
let output = '';
|
||||
for (const byte of bytes) {
|
||||
value = (value << 8) | byte;
|
||||
bits += 8;
|
||||
while (bits >= 5) {
|
||||
output += BASE32_ALPHABET[(value >>> (bits - 5)) & 0x1f];
|
||||
bits -= 5;
|
||||
}
|
||||
}
|
||||
if (bits > 0) output += BASE32_ALPHABET[(value << (5 - bits)) & 0x1f];
|
||||
return output;
|
||||
}
|
||||
|
||||
// Convert CIDv0 ("Qm...") to CIDv1 ("bafybe..."). Pure re-encoding: same content,
|
||||
// same sha2-256 digest, different envelope. Needed for IPFS subdomain gateways,
|
||||
// whose DNS labels are case-insensitive (base58btc is case-sensitive; base32
|
||||
// lowercase survives the round trip). Pass through unchanged if not a canonical
|
||||
// CIDv0.
|
||||
export function cidV0ToV1(cid: string): string {
|
||||
if (!cid.startsWith('Qm') || cid.length !== 46) return cid;
|
||||
let decoded: Uint8Array;
|
||||
try {
|
||||
decoded = base58Decode(cid);
|
||||
} catch {
|
||||
return cid;
|
||||
}
|
||||
if (decoded.length !== 34 || decoded[0] !== 0x12 || decoded[1] !== 0x20) return cid;
|
||||
const v1 = new Uint8Array(36);
|
||||
v1[0] = 0x01; // CID version 1
|
||||
v1[1] = 0x70; // codec: dag-pb (preserves CIDv0's implicit codec)
|
||||
v1.set(decoded, 2); // copy multihash verbatim
|
||||
return 'b' + base32Encode(v1);
|
||||
}
|
||||
|
||||
// Expand a gateway template against a CID. {cid} placeholder supports path form
|
||||
// (https://gw/ipfs/{cid}/) and subdomain form (https://{cid}.ipfs.gw/); the
|
||||
// latter forces CIDv0 -> CIDv1. Legacy no-placeholder form is path-only.
|
||||
export function expandGatewayUrl(gateway: string, cid: string): string {
|
||||
if (gateway.includes('{cid}')) {
|
||||
const isSubdomain = /\{cid\}\.ipfs\./.test(gateway) || /\{cid\}\.ipns\./.test(gateway);
|
||||
const finalCid = isSubdomain ? cidV0ToV1(cid) : cid;
|
||||
return gateway.replace('{cid}', finalCid);
|
||||
}
|
||||
return `${gateway}${cid}/`;
|
||||
}
|
||||
|
||||
export interface FlatResponse {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: Record<string, string>;
|
||||
body: Uint8Array;
|
||||
}
|
||||
|
||||
// Normalise headers to a lowercase-keyed plain object so downstream code only
|
||||
// ever sees one shape (mix-fetch v2 returns a real Response; smolmix's older
|
||||
// shape returned [k,v] tuples).
|
||||
export function headersToObj(headers: unknown): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
if (!headers) return out;
|
||||
if (headers instanceof Headers || headers instanceof Map) {
|
||||
for (const [k, v] of (headers as Headers).entries()) out[k.toLowerCase()] = v;
|
||||
return out;
|
||||
}
|
||||
if (Array.isArray(headers)) {
|
||||
for (const [k, v] of headers) out[String(k).toLowerCase()] = v;
|
||||
return out;
|
||||
}
|
||||
if (typeof headers === 'object') {
|
||||
for (const [k, v] of Object.entries(headers as Record<string, string>)) out[k.toLowerCase()] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Single boundary into the mixnet: flatten the Response to {status, headers
|
||||
// (lowercased), body (bytes)} once, because downstream does its own
|
||||
// decompression / TextDecoder work on the raw wire payload.
|
||||
export async function callMixFetch(mixFetch: MixFetchFn, url: string, init?: RequestInit): Promise<FlatResponse> {
|
||||
const res = await mixFetch(url, init || {});
|
||||
const body = new Uint8Array(await res.arrayBuffer());
|
||||
return { status: res.status, statusText: res.statusText, headers: headersToObj(res.headers), body };
|
||||
}
|
||||
|
||||
// Decompress a body if the server set Content-Encoding. Native fetch does this
|
||||
// transparently; smolmix returns raw wire bytes. Brotli (br) is not in
|
||||
// DecompressionStream; pass it through.
|
||||
export async function decompressBody(body: Uint8Array, headers: Record<string, string>): Promise<Uint8Array> {
|
||||
if (!body || body.byteLength === 0) return body;
|
||||
const enc = (headers['content-encoding'] || '').toLowerCase().trim();
|
||||
if (!enc || enc === 'identity') return body;
|
||||
|
||||
let format: 'gzip' | 'deflate' | 'deflate-raw' | null = null;
|
||||
if (enc === 'gzip' || enc === 'x-gzip') format = 'gzip';
|
||||
else if (enc === 'deflate') format = 'deflate';
|
||||
else if (enc === 'deflate-raw') format = 'deflate-raw';
|
||||
if (!format) return body;
|
||||
|
||||
// body is always a plain ArrayBuffer-backed Uint8Array at runtime; the cast
|
||||
// sidesteps the TS 5.7 generic-typed-array vs BlobPart (ArrayBuffer) mismatch.
|
||||
const stream = new Blob([body as BlobPart]).stream().pipeThrough(new DecompressionStream(format));
|
||||
const buf = await new Response(stream).arrayBuffer();
|
||||
return new Uint8Array(buf);
|
||||
}
|
||||
|
||||
export function stripContentEncoding(headers: Record<string, string>): Record<string, string> {
|
||||
const out = { ...headers };
|
||||
delete out['content-encoding'];
|
||||
return out;
|
||||
}
|
||||
|
||||
export interface Fingerprint {
|
||||
title: string | null;
|
||||
h1: string | null;
|
||||
description: string | null;
|
||||
ogTitle: string | null;
|
||||
counts: { links: number; images: number; scripts: number; stylesheets: number; headings: number };
|
||||
bodyTextLen: number;
|
||||
}
|
||||
|
||||
// Extract a human-meaningful fingerprint from HTML for visual comparison against
|
||||
// the same page in another tab. DOMParser runs no scripts and fetches nothing.
|
||||
export function htmlFingerprint(text: string): Fingerprint {
|
||||
const doc = new DOMParser().parseFromString(text, 'text/html');
|
||||
const get = (sel: string, attr?: string): string | null => {
|
||||
const el = doc.querySelector(sel);
|
||||
if (!el) return null;
|
||||
return attr ? el.getAttribute(attr) : el.textContent?.trim() ?? null;
|
||||
};
|
||||
return {
|
||||
title: get('title'),
|
||||
h1: get('h1'),
|
||||
description: get('meta[name="description"]', 'content') || get('meta[property="og:description"]', 'content'),
|
||||
ogTitle: get('meta[property="og:title"]', 'content'),
|
||||
counts: {
|
||||
links: doc.querySelectorAll('a').length,
|
||||
images: doc.querySelectorAll('img').length,
|
||||
scripts: doc.querySelectorAll('script').length,
|
||||
stylesheets: doc.querySelectorAll('link[rel="stylesheet"], style').length,
|
||||
headings: doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length,
|
||||
},
|
||||
bodyTextLen: doc.body ? doc.body.textContent!.replace(/\s+/g, ' ').trim().length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderFingerprint(fp: Fingerprint, totalBytes: number): string {
|
||||
const orNone = (s: string | null) => s || '(none)';
|
||||
return [
|
||||
'page fingerprint:',
|
||||
'',
|
||||
` title: ${orNone(fp.title)}`,
|
||||
` H1: ${orNone(fp.h1)}`,
|
||||
` description: ${orNone(fp.description)}`,
|
||||
` og:title: ${orNone(fp.ogTitle)}`,
|
||||
'',
|
||||
` ${fp.counts.links} links, ${fp.counts.images} images, ${fp.counts.scripts} scripts, ${fp.counts.stylesheets} stylesheets, ${fp.counts.headings} headings`,
|
||||
` body text: ${fp.bodyTextLen.toLocaleString()} chars after stripping tags`,
|
||||
` HTML size: ${formatSize(totalBytes)} raw response body`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
// Bridge ethers' FetchRequest to mixFetch. Every JSON-RPC call routes through
|
||||
// the mixnet; we decompress (smolmix doesn't) and log per-call timing/selector.
|
||||
export function buildProvider(rpcUrl: string, mixFetch: MixFetchFn, ensLog: LogFn): JsonRpcProvider {
|
||||
const base = new FetchRequest(rpcUrl);
|
||||
|
||||
base.getUrlFunc = async (req: FetchRequest) => {
|
||||
const t0 = performance.now();
|
||||
const raw = await callMixFetch(mixFetch, req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: (req.body ?? undefined) as BodyInit | undefined,
|
||||
});
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
|
||||
const body = await decompressBody(raw.body, raw.headers);
|
||||
const headers = stripContentEncoding(raw.headers);
|
||||
|
||||
ensLog(` RPC ${req.method} ${req.url} -> ${raw.status} ${raw.statusText}`);
|
||||
ensLog(` ${raw.body.byteLength}B wire, ${body.byteLength}B decoded, ${ms} ms`);
|
||||
|
||||
if (req.body) {
|
||||
try {
|
||||
const parsed = JSON.parse(new TextDecoder().decode(req.body));
|
||||
const data: string = parsed.params?.[0]?.data || '';
|
||||
const selector = data.slice(0, 10);
|
||||
ensLog(` -> method=${parsed.method} selector=${selector} args=0x${data.slice(10)}`);
|
||||
} catch {
|
||||
/* not all RPC bodies are eth_call with calldata */
|
||||
}
|
||||
}
|
||||
if (body.byteLength) {
|
||||
try {
|
||||
ensLog(` <- ${new TextDecoder().decode(body)}`);
|
||||
} catch {
|
||||
/* binary body */
|
||||
}
|
||||
}
|
||||
|
||||
return { statusCode: raw.status, statusMessage: raw.statusText, headers, body };
|
||||
};
|
||||
|
||||
// staticNetwork skips the eth_chainId discovery probe: one round trip saved.
|
||||
return new JsonRpcProvider(base, 'mainnet', { staticNetwork: true });
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
// Railgun-over-the-mixnet demo, ported from wasm/railgun-demo. Two privacy
|
||||
// layers: Nym hides the network (RPC via mixFetch), Railgun hides the
|
||||
// application layer (shielded notes). Sepolia testnet only.
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { HDNodeWallet, JsonRpcProvider, formatEther } from 'ethers';
|
||||
import { MixTunnelSetup, type MixFetchFn } from '../shared/mixTunnel';
|
||||
import { Button, LogPanel, useLogs, box, row, input, sub, legend } from '../shared/ui';
|
||||
import { buildProvider, callMixFetch, installGlobalMixFetchRouting, withRetry } from '../shared/mixfetch';
|
||||
import {
|
||||
DEFAULT_MNEMONIC,
|
||||
SEPOLIA_CHAIN_ID,
|
||||
STORAGE_KEY,
|
||||
createRailgunWalletFromMnemonic,
|
||||
derivePublicAddress,
|
||||
ensureRailgunEngine,
|
||||
shieldEth,
|
||||
type RailgunWalletInfo,
|
||||
} from './lib';
|
||||
|
||||
const RPC_PRESETS = ['https://ethereum-sepolia-rpc.publicnode.com', 'https://rpc.sepolia.org'];
|
||||
// Fixed shield amount: a single small value so the shared, faucet-funded testnet
|
||||
// wallet can't be drained by an arbitrary amount.
|
||||
const SHIELD_AMOUNT = '0.01';
|
||||
const IP_ECHO_URL = 'https://ipinfo.io/ip';
|
||||
const IP_SHAPE_RE = /^[\d.:a-f]{3,45}$/i;
|
||||
|
||||
export function RailgunDemo() {
|
||||
const { log, lines } = useLogs();
|
||||
const dlog = (msg: string, colour?: 'green' | 'red' | 'orange' | 'gray') => log('railgun', msg, colour);
|
||||
|
||||
const [mixFetch, setMixFetch] = useState<MixFetchFn | null>(null);
|
||||
const [mnemonic, setMnemonic] = useState('');
|
||||
const [publicAddr, setPublicAddr] = useState('(not generated)');
|
||||
const [railgunWallet, setRailgunWallet] = useState<RailgunWalletInfo | null>(null);
|
||||
const [rpc, setRpc] = useState(RPC_PRESETS[0]);
|
||||
const [balance, setBalance] = useState('');
|
||||
const [txHash, setTxHash] = useState<string | null>(null);
|
||||
const [storageStatus, setStorageStatus] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [importPhrase, setImportPhrase] = useState('');
|
||||
|
||||
const publicWalletRef = useRef<HDNodeWallet | null>(null);
|
||||
const providerRef = useRef<JsonRpcProvider | null>(null);
|
||||
|
||||
const connected = mixFetch != null;
|
||||
const hasWallet = publicAddr !== '(not generated)';
|
||||
|
||||
function updateStorageStatus() {
|
||||
let stored: string | null = null;
|
||||
try {
|
||||
stored = localStorage.getItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* localStorage disabled */
|
||||
}
|
||||
setStorageStatus(stored ? 'wallet saved in browser storage (auto-loaded on reload)' : 'no wallet saved; generate or import to persist one');
|
||||
}
|
||||
|
||||
function ensureProvider(): JsonRpcProvider {
|
||||
if (providerRef.current) return providerRef.current;
|
||||
if (!mixFetch) throw new Error('connect the mixnet tunnel first');
|
||||
const url = rpc.trim();
|
||||
if (!url) throw new Error('Sepolia RPC URL is required');
|
||||
dlog(`building JsonRpcProvider({ rpc: ${url}, transport: mixFetch })`);
|
||||
providerRef.current = buildProvider(url, mixFetch, SEPOLIA_CHAIN_ID);
|
||||
return providerRef.current;
|
||||
}
|
||||
|
||||
async function deriveRailgun(phrase: string) {
|
||||
dlog('initialising Railgun engine + deriving shielded address...');
|
||||
try {
|
||||
await ensureRailgunEngine(rpc.trim(), dlog);
|
||||
const result = await createRailgunWalletFromMnemonic(phrase.trim());
|
||||
setRailgunWallet(result);
|
||||
dlog(`Railgun address derived: ${result.railgunAddress}`, 'green');
|
||||
} catch (e: any) {
|
||||
dlog(`Railgun derivation failed: ${e.message || e}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
function loadWallet(phrase: string) {
|
||||
let wallet: HDNodeWallet;
|
||||
try {
|
||||
wallet = derivePublicAddress(phrase);
|
||||
} catch (e: any) {
|
||||
dlog(`invalid mnemonic: ${e.message || e}`, 'red');
|
||||
return;
|
||||
}
|
||||
publicWalletRef.current = wallet;
|
||||
setPublicAddr(wallet.address);
|
||||
setRailgunWallet(null);
|
||||
setMnemonic(phrase.trim());
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, phrase.trim());
|
||||
} catch {
|
||||
/* localStorage disabled */
|
||||
}
|
||||
updateStorageStatus();
|
||||
dlog(`public address derived: ${wallet.address}`, 'green');
|
||||
if (mixFetch) void deriveRailgun(phrase);
|
||||
else dlog('connect the mixnet tunnel to derive the Railgun address', 'orange');
|
||||
}
|
||||
|
||||
// Auto-load on mount: stored mnemonic, else the funded testnet fallback.
|
||||
useEffect(() => {
|
||||
let stored: string | null = null;
|
||||
try {
|
||||
stored = localStorage.getItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* localStorage disabled */
|
||||
}
|
||||
const phrase = stored || DEFAULT_MNEMONIC;
|
||||
setMnemonic(phrase);
|
||||
updateStorageStatus();
|
||||
try {
|
||||
const wallet = derivePublicAddress(phrase);
|
||||
publicWalletRef.current = wallet;
|
||||
setPublicAddr(wallet.address);
|
||||
dlog(`auto-loaded wallet: ${wallet.address}`, 'green');
|
||||
dlog('public side ready. The Railgun address derives once the tunnel is up.');
|
||||
} catch (e: any) {
|
||||
dlog(`auto-load failed: ${e.message || e}`, 'red');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
function onReady(fn: MixFetchFn) {
|
||||
setMixFetch(() => fn);
|
||||
// Route every ethers HTTP call (incl. Railgun's internal providers) through Nym.
|
||||
installGlobalMixFetchRouting(fn);
|
||||
if (publicWalletRef.current && !railgunWallet) void deriveRailgun(mnemonic);
|
||||
}
|
||||
function onDisconnect() {
|
||||
setMixFetch(null);
|
||||
providerRef.current = null;
|
||||
}
|
||||
|
||||
function generateWallet() {
|
||||
dlog('generating fresh BIP-39 mnemonic...');
|
||||
const wallet = HDNodeWallet.createRandom();
|
||||
loadWallet(wallet.mnemonic!.phrase);
|
||||
}
|
||||
function importWallet() {
|
||||
loadWallet(importPhrase);
|
||||
setImportPhrase('');
|
||||
}
|
||||
function clearWallet() {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* localStorage disabled */
|
||||
}
|
||||
publicWalletRef.current = null;
|
||||
setPublicAddr('(not generated)');
|
||||
setRailgunWallet(null);
|
||||
setMnemonic('');
|
||||
updateStorageStatus();
|
||||
dlog('cleared stored wallet; reload to load the funded fallback');
|
||||
}
|
||||
|
||||
async function checkBalance() {
|
||||
if (!publicWalletRef.current) return dlog('generate or import a wallet first', 'red');
|
||||
let provider: JsonRpcProvider;
|
||||
try {
|
||||
provider = ensureProvider();
|
||||
} catch (e: any) {
|
||||
return dlog(`${e.message || e}`, 'red');
|
||||
}
|
||||
setBusy(true);
|
||||
dlog(`eth_getBalance(${publicWalletRef.current.address}) via mixFetch...`);
|
||||
try {
|
||||
const wei = await withRetry(() => provider.getBalance(publicWalletRef.current!.address), 'eth_getBalance', { log: dlog });
|
||||
const eth = formatEther(wei);
|
||||
setBalance(`${eth} ETH (Sepolia)`);
|
||||
dlog(`balance: ${eth} ETH`, 'green');
|
||||
} catch (e: any) {
|
||||
dlog(`balance lookup failed: ${e.shortMessage || e.message || e}`, 'red');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function shield() {
|
||||
if (!railgunWallet) return dlog('Railgun wallet not derived; connect tunnel + generate wallet first', 'red');
|
||||
if (!publicWalletRef.current) return dlog('public wallet missing', 'red');
|
||||
let provider: JsonRpcProvider;
|
||||
try {
|
||||
provider = ensureProvider();
|
||||
} catch (e: any) {
|
||||
return dlog(`${e.message || e}`, 'red');
|
||||
}
|
||||
setBusy(true);
|
||||
setTxHash(null);
|
||||
try {
|
||||
await shieldEth({
|
||||
publicWallet: publicWalletRef.current,
|
||||
railgunWallet,
|
||||
provider,
|
||||
amountStr: SHIELD_AMOUNT,
|
||||
log: dlog,
|
||||
onTxHash: setTxHash,
|
||||
});
|
||||
} catch (e: any) {
|
||||
dlog(`shield failed: ${e.shortMessage || e.message || e}`, 'red');
|
||||
// Tear down the provider so its background pollers stop after a failure.
|
||||
try {
|
||||
providerRef.current?.destroy();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
providerRef.current = null;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyIp() {
|
||||
if (!mixFetch) return dlog('connect the mixnet tunnel first', 'red');
|
||||
setBusy(true);
|
||||
dlog('comparing direct-clearnet IP vs Nym-exit IP...');
|
||||
let directIp: string;
|
||||
try {
|
||||
directIp = (await (await fetch(IP_ECHO_URL)).text()).trim();
|
||||
if (!IP_SHAPE_RE.test(directIp)) directIp = `(unexpected: ${directIp})`;
|
||||
} catch (e: any) {
|
||||
directIp = `error: ${e.message || e}`;
|
||||
}
|
||||
dlog(` your real IP (direct fetch, no Nym): ${directIp}`, 'orange');
|
||||
let nymIp: string;
|
||||
try {
|
||||
const raw = await callMixFetch(mixFetch, IP_ECHO_URL, {});
|
||||
nymIp = new TextDecoder().decode(raw.body).trim();
|
||||
if (!IP_SHAPE_RE.test(nymIp)) nymIp = `(unexpected: ${nymIp})`;
|
||||
} catch (e: any) {
|
||||
nymIp = `error: ${e.message || e}`;
|
||||
}
|
||||
dlog(` what the upstream sees via mixFetch -> Nym: ${nymIp}`, 'green');
|
||||
if (!nymIp.startsWith('error') && !directIp.startsWith('error') && nymIp !== directIp) {
|
||||
dlog('IPs differ. Every Shield broadcast uses this same Nym-exit path.', 'green');
|
||||
} else {
|
||||
dlog('Could not confirm a different exit IP. Try again, or reconnect with a different IPR.', 'red');
|
||||
}
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ margin: '1.5rem 0' }}>
|
||||
<div style={{ ...box, borderColor: 'var(--colorWarn, #d97706)' }}>
|
||||
<strong>Sepolia testnet only.</strong>{' '}
|
||||
<span style={sub}>
|
||||
The wallet holds only test ETH from public faucets and the mnemonic is stored in plain
|
||||
browser storage. Never paste a mainnet mnemonic into this demo.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<MixTunnelSetup onReady={onReady} onDisconnect={onDisconnect} clientIdPrefix="railgun-demo" />
|
||||
|
||||
<div style={box}>
|
||||
<div style={legend}>Wallet</div>
|
||||
<div style={sub}>
|
||||
A testnet wallet is auto-loaded from browser storage. Its mnemonic is not shown here:
|
||||
it holds only Sepolia test ETH, so please don't be cheeky and try to pull the funded
|
||||
testnet key out. Import your own below if you'd rather.
|
||||
</div>
|
||||
<div style={row}>
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
style={input}
|
||||
value={importPhrase}
|
||||
onChange={(e) => setImportPhrase(e.target.value)}
|
||||
placeholder="import your own 12-word mnemonic (optional)"
|
||||
/>
|
||||
<Button onClick={importWallet} disabled={!importPhrase.trim()}>Import</Button>
|
||||
<Button onClick={generateWallet}>Generate</Button>
|
||||
<Button onClick={clearWallet}>Clear</Button>
|
||||
</div>
|
||||
<div style={sub}>public address: <code>{publicAddr}</code></div>
|
||||
<div style={sub}>Railgun address: <code>{railgunWallet ? railgunWallet.railgunAddress : connected ? '(deriving...)' : '(connect tunnel to derive)'}</code></div>
|
||||
<div style={sub}>{storageStatus}</div>
|
||||
</div>
|
||||
|
||||
<div style={box}>
|
||||
<div style={legend}>Public Sepolia state</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>RPC</label>
|
||||
<select style={input} value={RPC_PRESETS.includes(rpc) ? rpc : ''} disabled={connected || busy} onChange={(e) => { setRpc(e.target.value); providerRef.current = null; }}>
|
||||
{RPC_PRESETS.map((u) => <option key={u} value={u}>{u}</option>)}
|
||||
</select>
|
||||
<input style={input} value={rpc} disabled={connected || busy} onChange={(e) => { setRpc(e.target.value); providerRef.current = null; }} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<Button onClick={checkBalance} disabled={!connected || !hasWallet || busy}>Check balance</Button>
|
||||
<Button onClick={verifyIp} disabled={!connected || busy}>Verify IP routing</Button>
|
||||
<span style={sub}>{balance}</span>
|
||||
</div>
|
||||
<div style={sub}>Verify IP makes one direct (clearnet) call to ipinfo.io for the comparison, so you will see a single ipinfo.io row in the Network tab.</div>
|
||||
</div>
|
||||
|
||||
<div style={box}>
|
||||
<div style={legend}>Shield ETH into a private note</div>
|
||||
<div style={row}>
|
||||
<Button onClick={shield} disabled={!connected || !railgunWallet || busy}>Shield {SHIELD_AMOUNT} ETH</Button>
|
||||
<span style={sub}>Fixed at {SHIELD_AMOUNT} ETH so the shared testnet wallet isn't drained.</span>
|
||||
</div>
|
||||
{txHash && (
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<a
|
||||
href={`https://sepolia.etherscan.io/tx/${txHash}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#3b82f6', textDecoration: 'underline', fontWeight: 600 }}
|
||||
>
|
||||
View transaction on Etherscan
|
||||
</a>{' '}
|
||||
<code style={{ ...sub, opacity: 0.7 }}>{txHash.slice(0, 10)}...{txHash.slice(-8)}</code>
|
||||
</div>
|
||||
)}
|
||||
<LogPanel lines={lines('railgun')} placeholder="Connect the tunnel, then derive the wallet and shield." />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
// Railgun-over-the-mixnet logic, ported from wasm/railgun-demo/index.js.
|
||||
// Two privacy layers: Nym hides the network (every RPC via mixFetch), Railgun
|
||||
// hides the application layer (shielded notes break the on-chain graph).
|
||||
//
|
||||
// The @railgun-community SDK is imported dynamically (no bundled types here, so
|
||||
// it is typed `any`) and the engine is a process-global singleton, so the
|
||||
// started/loaded flags live at module scope, which is the faithful model.
|
||||
|
||||
import { HDNodeWallet, Mnemonic, JsonRpcProvider, keccak256, parseEther } from 'ethers';
|
||||
import { withRetry } from '../shared/mixfetch';
|
||||
|
||||
export const SEPOLIA_CHAIN_ID = 11155111;
|
||||
export const SEPOLIA_NETWORK_NAME = 'Ethereum_Sepolia';
|
||||
export const SEPOLIA_WETH = '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14';
|
||||
export const TXID_VERSION_V2 = 'V2_PoseidonMerkle';
|
||||
export const STORAGE_KEY = 'railgun-demo-mnemonic';
|
||||
export const DEFAULT_MNEMONIC = 'inherit joy bubble reveal fit skin repair involve spoil cube robot angry';
|
||||
const ENCRYPTION_KEY = '0101010101010101010101010101010101010101010101010101010101010101';
|
||||
|
||||
export type RailgunLog = (msg: string, colour?: 'green' | 'red' | 'orange' | 'gray') => void;
|
||||
export interface RailgunWalletInfo { id: string; railgunAddress: string; }
|
||||
|
||||
let engineStarted = false;
|
||||
let providerLoaded = false;
|
||||
|
||||
// Pure-client public address derivation. No network, no engine; works before
|
||||
// the tunnel is up so the page can show the funding target immediately.
|
||||
export function derivePublicAddress(phrase: string): HDNodeWallet {
|
||||
const mnemonic = Mnemonic.fromPhrase(phrase.trim());
|
||||
return HDNodeWallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/0");
|
||||
}
|
||||
|
||||
async function ensureEngineStarted(): Promise<void> {
|
||||
if (engineStarted) return;
|
||||
const railgun: any = await import('@railgun-community/wallet');
|
||||
const { MemoryLevel }: any = await import('memory-level');
|
||||
const db = new MemoryLevel();
|
||||
// Read-only artifact store: Shield does not need proving artifacts.
|
||||
const artifactStore = new railgun.ArtifactStore(
|
||||
async () => null,
|
||||
async () => {},
|
||||
async () => false,
|
||||
);
|
||||
await railgun.startRailgunEngine(
|
||||
'railgundemo', // wallet source id; alphanumeric only
|
||||
db,
|
||||
false, // shouldDebug
|
||||
artifactStore,
|
||||
false, // useNativeArtifacts (Node-only)
|
||||
false, // skipMerkletreeScans
|
||||
undefined, // poiNodeURLs (undefined keeps POI uninstantiated)
|
||||
undefined, // customPOILists
|
||||
false, // verboseScanLogging
|
||||
);
|
||||
// Sepolia is POI-gated but the public aggregator is dead and POI is not what
|
||||
// this demo proves: clear the network's poi field so the engine treats it as
|
||||
// a pre-POI deployment. Production would point at a real POI URL instead.
|
||||
const { NETWORK_CONFIG }: any = await import('@railgun-community/shared-models');
|
||||
NETWORK_CONFIG[SEPOLIA_NETWORK_NAME].poi = undefined;
|
||||
// Disable GraphQL quick-sync (its subgraph map lacks Sepolia, so it would
|
||||
// spam-XHR /undefined). Falls back to direct eth_getLogs scanning.
|
||||
const engine = railgun.getEngine();
|
||||
engine.quickSyncEvents = async () => ({ commitmentEvents: [], unshieldEvents: [], nullifierEvents: [] });
|
||||
engine.quickSyncRailgunTransactionsV2 = async () => [];
|
||||
engineStarted = true;
|
||||
}
|
||||
|
||||
async function loadProviderOnce(rpc: string): Promise<void> {
|
||||
if (providerLoaded) return;
|
||||
const railgun: any = await import('@railgun-community/wallet');
|
||||
// One provider, weight 2 (the validator requires totalWeight >= 2; a single
|
||||
// HTTPS endpoint avoids competing TCP handshakes during cold start).
|
||||
const fallbackConfig = { chainId: SEPOLIA_CHAIN_ID, providers: [{ provider: rpc, priority: 1, weight: 2 }] };
|
||||
await railgun.loadProvider(fallbackConfig, SEPOLIA_NETWORK_NAME, 10000);
|
||||
providerLoaded = true;
|
||||
}
|
||||
|
||||
export async function ensureRailgunEngine(rpc: string, log: RailgunLog): Promise<void> {
|
||||
if (engineStarted && providerLoaded) return;
|
||||
log('initialising Railgun engine (one-time)...');
|
||||
await ensureEngineStarted();
|
||||
// loadProvider hits the network via mixFetch; cold-start can exceed Railgun's
|
||||
// 60s timeout, so retry (the second attempt finds the pool warm).
|
||||
await withRetry(() => loadProviderOnce(rpc), 'loadProvider', { log });
|
||||
log('Railgun engine ready', 'green');
|
||||
}
|
||||
|
||||
export async function createRailgunWalletFromMnemonic(phrase: string): Promise<RailgunWalletInfo> {
|
||||
const railgun: any = await import('@railgun-community/wallet');
|
||||
const creationBlockNumbers = { [SEPOLIA_NETWORK_NAME]: 10_900_000 };
|
||||
return await railgun.createRailgunWallet(ENCRYPTION_KEY, phrase, creationBlockNumbers);
|
||||
}
|
||||
|
||||
// Shield ETH into a shielded note. The headline action: a 4-step flow that
|
||||
// signs a shield key, estimates gas, populates the tx, then signs + broadcasts
|
||||
// (idempotently) through the mixFetch-routed provider.
|
||||
export async function shieldEth(opts: {
|
||||
publicWallet: HDNodeWallet;
|
||||
railgunWallet: RailgunWalletInfo;
|
||||
provider: JsonRpcProvider;
|
||||
amountStr: string;
|
||||
log: RailgunLog;
|
||||
onTxHash: (hash: string) => void;
|
||||
}): Promise<void> {
|
||||
const { publicWallet, railgunWallet, provider, amountStr, log, onTxHash } = opts;
|
||||
const railgun: any = await import('@railgun-community/wallet');
|
||||
|
||||
let amountWei: bigint;
|
||||
try {
|
||||
amountWei = parseEther(amountStr);
|
||||
} catch {
|
||||
log(`invalid amount: "${amountStr}"`, 'red');
|
||||
return;
|
||||
}
|
||||
if (amountWei <= 0n) {
|
||||
log('amount must be > 0', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`shielding ${amountStr} ETH -> ${railgunWallet.railgunAddress}`);
|
||||
|
||||
// Step 1: shieldPrivateKey = keccak256 of a signature over a deterministic
|
||||
// message. Signing proves consent and binds the key to the public wallet.
|
||||
log('step 1/4: signing shield-key derivation message...');
|
||||
const msg = railgun.getShieldPrivateKeySignatureMessage();
|
||||
const sigHex = await publicWallet.signMessage(msg);
|
||||
const shieldPrivateKey = keccak256(sigHex);
|
||||
const wrappedERC20Amount = { tokenAddress: SEPOLIA_WETH, amount: amountWei };
|
||||
|
||||
// Step 2: gas estimate (needs the funder's address to simulate the call).
|
||||
log('step 2/4: estimating gas via mixFetch...');
|
||||
// withRetry can't infer T from the `any`-typed SDK call, so it falls back to
|
||||
// unknown; annotate the result to read its fields.
|
||||
const gasEstResp: any = await withRetry(
|
||||
() =>
|
||||
railgun.gasEstimateForShieldBaseToken(
|
||||
TXID_VERSION_V2,
|
||||
SEPOLIA_NETWORK_NAME,
|
||||
railgunWallet.railgunAddress,
|
||||
shieldPrivateKey,
|
||||
wrappedERC20Amount,
|
||||
publicWallet.address,
|
||||
),
|
||||
'gasEstimateForShieldBaseToken',
|
||||
{ log },
|
||||
);
|
||||
log(` gas estimate: ${gasEstResp.gasEstimate.toString()} units`);
|
||||
|
||||
// EIP-1559 gas details, padded 50% so a transient spike during the mixnet
|
||||
// round trip does not strand the tx.
|
||||
const feeData = await provider.getFeeData();
|
||||
const maxFeePerGas = ((feeData.maxFeePerGas ?? feeData.gasPrice ?? 30_000_000_000n) * 3n) / 2n;
|
||||
const maxPriorityFeePerGas = ((feeData.maxPriorityFeePerGas ?? 2_000_000_000n) * 3n) / 2n;
|
||||
const gasDetails = { evmGasType: 2, gasEstimate: gasEstResp.gasEstimate, maxFeePerGas, maxPriorityFeePerGas };
|
||||
|
||||
// Step 3: populate the actual transaction.
|
||||
log('step 3/4: populating shield transaction...');
|
||||
const populateResp = await railgun.populateShieldBaseToken(
|
||||
TXID_VERSION_V2,
|
||||
SEPOLIA_NETWORK_NAME,
|
||||
railgunWallet.railgunAddress,
|
||||
shieldPrivateKey,
|
||||
wrappedERC20Amount,
|
||||
gasDetails,
|
||||
);
|
||||
const tx = populateResp.transaction;
|
||||
|
||||
// Step 4: sign then broadcast separately, so the tx hash is fixed before any
|
||||
// broadcast attempt and a dropped response can be retried idempotently.
|
||||
log('step 4/4: signing + broadcasting via mixFetch -> Nym...');
|
||||
const signer = publicWallet.connect(provider);
|
||||
const populated = await signer.populateTransaction(tx);
|
||||
const signedHex = await signer.signTransaction(populated);
|
||||
const txHash = keccak256(signedHex);
|
||||
log(` signed tx hash: ${txHash}`);
|
||||
log(` -> To: ${populated.to} (Railgun Sepolia proxy contract)`);
|
||||
log(` -> calldata selector: ${(populated.data || '').slice(0, 10)} (Railgun shield function; Etherscan decodes this)`);
|
||||
log(` -> full calldata: ${populated.data || ''}`);
|
||||
onTxHash(txHash);
|
||||
|
||||
let sentTx: any;
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
sentTx = await provider.broadcastTransaction(signedHex);
|
||||
log(` broadcast OK (attempt ${attempt})`, 'green');
|
||||
break;
|
||||
} catch (e: any) {
|
||||
const m = e.shortMessage || e.message || String(e);
|
||||
try {
|
||||
const existing = await provider.getTransaction(txHash);
|
||||
if (existing) {
|
||||
log(' broadcast response failed but tx is on chain, partial success', 'green');
|
||||
sentTx = existing;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
/* getTransaction failed too; treat as not on chain, retry */
|
||||
}
|
||||
if (attempt < 3) {
|
||||
log(` broadcast attempt ${attempt}/3 failed (${m}), retrying in 10s...`, 'orange');
|
||||
await new Promise((r) => setTimeout(r, 10_000));
|
||||
} else {
|
||||
log(' broadcast failed after 3 attempts. The mixnet route to the RPC is degraded. Disconnect, tick "Use random IPR", reconnect, then Shield again.', 'red');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log('waiting for receipt (1 confirmation)...');
|
||||
const receipt = await sentTx.wait(1);
|
||||
if (receipt && receipt.status === 1) {
|
||||
log(`shielded. Block ${receipt.blockNumber}, gas used ${receipt.gasUsed}`, 'green');
|
||||
log("verify on Etherscan: the To field is Railgun's Sepolia proxy, the method decodes to a shield, and the logs hold an encrypted Shield commitment.", 'green');
|
||||
} else {
|
||||
log('tx mined but reverted', 'red');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Shared mixnet glossary for the demo pages, with links to the relevant docs.
|
||||
// Raw <a> inside a React component does not pick up Nextra's MDX link styling,
|
||||
// so the links are styled explicitly via the L helper.
|
||||
import React from 'react';
|
||||
|
||||
function L({ href, children }: { href: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<a href={href} style={{ color: '#3b82f6', textDecoration: 'underline' }}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function MixnetGlossary() {
|
||||
return (
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Mixnet.</strong> An overlay network that routes your traffic through several relays,
|
||||
mixed in with everyone else's, so no single point can link sender to receiver. See{' '}
|
||||
<L href="/network/mixnet-mode">mixnet mode</L>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Entry gateway.</strong> Your first hop into the mixnet. The browser holds one
|
||||
WebSocket to it, and all tunnelled traffic travels over that single connection as opaque
|
||||
frames. See <L href="/network/infrastructure/nym-nodes">Nym nodes</L>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>IPR (IP Packet Router), the exit.</strong> Where traffic leaves the mixnet for the
|
||||
public internet. The destination sees the IPR's IP, not yours. See{' '}
|
||||
<L href="/network/infrastructure/exit-services#ip-packet-router">exit services</L>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>SURB (single-use reply block).</strong> A prepaid, single-use return envelope. The
|
||||
exit replies through it without ever learning your address. See{' '}
|
||||
<L href="/network/mixnet-mode/anonymous-replies">anonymous replies</L>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Cover traffic / Poisson timing.</strong> Decoy packets sent on randomised timing, so
|
||||
your real traffic blends into a steady stream. See{' '}
|
||||
<L href="/network/mixnet-mode/cover-traffic">cover traffic</L>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>mixFetch.</strong> A <code>fetch()</code>-shaped function from{' '}
|
||||
<L href="/developers/mix-fetch"><code>@nymproject/mix-fetch</code></L>. It runs the mixnet
|
||||
client (smolmix) in a Web Worker, so each request goes through the mixnet rather than the
|
||||
browser's network stack.
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Shared "Watch the Network tab" callout, used on the playground and the demo
|
||||
// pages. Generic wording so it reads correctly wherever a single mixnet tunnel
|
||||
// carries the page's traffic.
|
||||
import React from 'react';
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
export function NetworkTabCallout() {
|
||||
return (
|
||||
<Callout type="info">
|
||||
<strong>Watch the Network tab.</strong> Open DevTools → Network before you connect. Once the
|
||||
tunnel reports ready, every operation you run here adds <strong>no new request</strong> 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.
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Shared mixnet-tunnel setup panel for the in-docs demos.
|
||||
//
|
||||
// Owns the connection lifecycle (setup / disconnect / state) and the options
|
||||
// surface (IPR pin, SURBs, DNS, timeouts, ...), and hands the parent demo a
|
||||
// `mixFetch` function once the tunnel is `ready`. Modelled on the playground's
|
||||
// inline setup section; the demos differ only in what they do with `mixFetch`.
|
||||
//
|
||||
// The package import is dynamic so the multi-MB wasm chunk loads only when the
|
||||
// visitor clicks Connect, not on page render. Everything here is client-only;
|
||||
// render the demo page with `next/dynamic` + `ssr: false`.
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, LogPanel, StatusText, useLogs, box, row, input, num, sub, legend, type Status } from './ui';
|
||||
|
||||
export type MixFetchFn = (url: string, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
interface MixFetchModule {
|
||||
setupMixTunnel: (opts?: Record<string, unknown>) => Promise<void>;
|
||||
disconnectMixTunnel: () => Promise<void>;
|
||||
getTunnelState: () => Promise<{ state: string; reason?: string }>;
|
||||
mixFetch: MixFetchFn;
|
||||
}
|
||||
|
||||
// Lazy-load the published mix-fetch facade. The literal specifier keeps webpack
|
||||
// code-splitting the wasm into an async chunk.
|
||||
async function loadMixFetch(): Promise<MixFetchModule> {
|
||||
// @ts-ignore -- @nymproject/mix-fetch resolves at runtime; lazy wasm chunk
|
||||
const m = await import('@nymproject/mix-fetch');
|
||||
return m as unknown as MixFetchModule;
|
||||
}
|
||||
|
||||
const clampSurbs = (n: number, min: number) => Math.min(50, Math.max(min, n));
|
||||
|
||||
// Default IPR exit for the docs demos. Pinned so a demo connects to a known
|
||||
// exit by default; users can switch to auto-discovery with 'Use random IPR'.
|
||||
const DEFAULT_IPR =
|
||||
'6B6iuWX4bQP4GVA4Yq7XmZencaaGw6BaPY6xJWYSwsbF.6g6LRx1fgU2Q2A4ZPKonYHtfBARh1GPMe1LtXk6vpRR8@q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1';
|
||||
|
||||
export function MixTunnelSetup({
|
||||
onReady,
|
||||
onDisconnect,
|
||||
clientIdPrefix = 'docs-demo',
|
||||
}: {
|
||||
onReady: (mixFetch: MixFetchFn) => void;
|
||||
onDisconnect?: () => void;
|
||||
clientIdPrefix?: string;
|
||||
}) {
|
||||
const { log, lines } = useLogs();
|
||||
const [mods, setMods] = useState<MixFetchModule | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
// The tunnel is one-shot per page (smolmix OnceLock + single worker), so once
|
||||
// it has been torn down, Connect stays disabled until a reload.
|
||||
const [terminated, setTerminated] = useState(false);
|
||||
const [status, setStatus] = useState<Status>({ text: 'Not started', colour: 'gray' });
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
// Connection options.
|
||||
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 [debug, setDebug] = useState(true);
|
||||
const [openSurbs, setOpenSurbs] = useState(10);
|
||||
const [dataSurbs, setDataSurbs] = useState(2);
|
||||
const [primaryDns, setPrimaryDns] = useState('');
|
||||
const [fallbackDns, setFallbackDns] = useState('');
|
||||
const [dnsTimeout, setDnsTimeout] = useState('');
|
||||
const [connectTimeout, setConnectTimeout] = useState('');
|
||||
const [maxRedirects, setMaxRedirects] = useState('');
|
||||
const [storagePassphrase, setStoragePassphrase] = useState('');
|
||||
|
||||
// Generate the client id after mount (not at render) so SSG and client
|
||||
// hydration agree: Math.random at render would differ between the two.
|
||||
useEffect(() => {
|
||||
setClientId((c) => c || `${clientIdPrefix}-${Math.random().toString(36).slice(2, 8)}`);
|
||||
}, [clientIdPrefix]);
|
||||
|
||||
const optInt = (v: string): number | undefined => {
|
||||
const n = parseInt(v, 10);
|
||||
return Number.isNaN(n) ? undefined : n;
|
||||
};
|
||||
const optStr = (v: string): string | undefined => v.trim() || undefined;
|
||||
|
||||
async function connect() {
|
||||
if (!useRandomIpr && !iprAddress.trim()) {
|
||||
setStatus({ text: "IPR address required (or tick 'Use random IPR')", colour: 'red' });
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setStatus({ text: 'Connecting (building the client, connecting to the IPR exit)...', colour: 'orange' });
|
||||
log('tunnel', `Connecting (clientId=${clientId}, IPR: ${useRandomIpr ? 'auto-discover' : iprAddress.trim().slice(0, 28) + '...'}, SURBs open=${openSurbs} data=${dataSurbs})`, 'orange');
|
||||
try {
|
||||
const m = mods ?? (await loadMixFetch());
|
||||
if (!mods) setMods(m);
|
||||
// One WASM instance per browser tab, shared across demo pages by the
|
||||
// bundler. If another page already brought the tunnel up, reuse it rather
|
||||
// than calling setupMixTunnel again (which throws "already initialised").
|
||||
const existing = await m.getTunnelState().catch(() => null);
|
||||
if (existing && existing.state === 'ready') {
|
||||
log('tunnel', 'Tunnel already up from another page; reusing it (its original options apply).', 'green');
|
||||
} else {
|
||||
await m.setupMixTunnel({
|
||||
...(useRandomIpr ? {} : { preferredIpr: iprAddress.trim() }),
|
||||
clientId,
|
||||
forceTls,
|
||||
disablePoissonTraffic: disablePoisson,
|
||||
disableCoverTraffic: disableCover,
|
||||
openReplySurbs: clampSurbs(openSurbs, 1),
|
||||
dataReplySurbs: clampSurbs(dataSurbs, 0),
|
||||
primaryDns: optStr(primaryDns),
|
||||
fallbackDns: optStr(fallbackDns),
|
||||
dnsTimeoutMs: optInt(dnsTimeout),
|
||||
connectTimeoutMs: optInt(connectTimeout),
|
||||
maxRedirects: optInt(maxRedirects),
|
||||
storagePassphrase: storagePassphrase || undefined,
|
||||
debug,
|
||||
});
|
||||
log('tunnel', 'Tunnel ready', 'green');
|
||||
}
|
||||
setConnected(true);
|
||||
setStatus({ text: 'Connected', colour: 'green' });
|
||||
onReady(m.mixFetch);
|
||||
} catch (e) {
|
||||
const msg = String((e as any)?.message ?? e);
|
||||
if (/already initialised/i.test(msg)) {
|
||||
log('tunnel', 'Tunnel already initialised in this tab; reload the page if it does not connect.', 'orange');
|
||||
setStatus({ text: 'Failed (already initialised, reload)', colour: 'red' });
|
||||
} else {
|
||||
setStatus({ text: 'Failed', colour: 'red' });
|
||||
log('tunnel', `Connection failed: ${msg}`, 'red');
|
||||
log('tunnel', "Timeouts and IPR rate-limits are common. Try again, or tick 'Use random IPR' and reload.", 'orange');
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
if (!mods) return;
|
||||
setBusy(true);
|
||||
log('tunnel', 'Disconnecting...');
|
||||
try {
|
||||
await mods.disconnectMixTunnel();
|
||||
log('tunnel', 'Disconnected. Reload the page to reconnect.', 'green');
|
||||
setStatus({ text: 'Disconnected (reload to reconnect)', colour: 'gray' });
|
||||
} catch (e) {
|
||||
log('tunnel', `Disconnect failed: ${e}`, 'red');
|
||||
setStatus({ text: 'Disconnected after error (reload to reconnect)', colour: 'red' });
|
||||
} finally {
|
||||
// The tunnel is one-shot per page: smolmix uses a OnceLock and the package
|
||||
// owns one worker, so there is no fresh-client path without a reload. Keep
|
||||
// Connect disabled and say so rather than failing on a second connect.
|
||||
setConnected(false);
|
||||
setTerminated(true);
|
||||
setBusy(false);
|
||||
onDisconnect?.();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={box}>
|
||||
<div style={legend}>Mixnet tunnel</div>
|
||||
<div style={row}>
|
||||
<label style={{ ...sub, display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={useRandomIpr} onChange={(e) => setUseRandomIpr(e.target.checked)} disabled={connected || busy} />
|
||||
Use random IPR
|
||||
</label>
|
||||
<input
|
||||
style={input}
|
||||
value={iprAddress}
|
||||
onChange={(e) => setIprAddress(e.target.value)}
|
||||
placeholder="<nym-address of IPR exit node>"
|
||||
disabled={useRandomIpr || connected || busy}
|
||||
/>
|
||||
</div>
|
||||
<div style={row}>
|
||||
<Button onClick={connect} disabled={connected || busy || terminated}>{busy && !connected ? 'Connecting...' : 'Connect to mixnet'}</Button>
|
||||
<Button onClick={disconnect} disabled={!connected || busy}>Disconnect</Button>
|
||||
<StatusText status={status} />
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={showAdvanced}
|
||||
style={{ ...sub, marginLeft: 'auto', cursor: 'pointer', background: 'none', border: 'none', padding: 0, fontFamily: 'inherit', fontWeight: 'inherit', color: 'inherit' }}
|
||||
onClick={() => setShowAdvanced((v) => !v)}
|
||||
>
|
||||
{showAdvanced ? '▾ advanced' : '▸ advanced'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAdvanced && (
|
||||
<div style={{ ...row, flexDirection: 'column', alignItems: 'stretch', gap: 6 }}>
|
||||
<div style={row}>
|
||||
<label style={sub}>client id</label>
|
||||
<input style={input} value={clientId} onChange={(e) => setClientId(e.target.value)} disabled={connected || busy} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={{ ...sub, display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={forceTls} onChange={(e) => setForceTls(e.target.checked)} disabled={connected || busy} /> forceTls (WSS to gateway)
|
||||
</label>
|
||||
<label style={{ ...sub, display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={disablePoisson} onChange={(e) => setDisablePoisson(e.target.checked)} disabled={connected || busy} /> disable Poisson
|
||||
</label>
|
||||
<label style={{ ...sub, display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={disableCover} onChange={(e) => setDisableCover(e.target.checked)} disabled={connected || busy} /> disable cover traffic
|
||||
</label>
|
||||
<label style={{ ...sub, display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={debug} onChange={(e) => setDebug(e.target.checked)} disabled={connected || busy} /> verbose console logs
|
||||
</label>
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>open SURBs</label>
|
||||
<input style={num} type="number" min={1} value={openSurbs} onChange={(e) => setOpenSurbs(+e.target.value)} disabled={connected || busy} />
|
||||
<label style={sub}>data SURBs</label>
|
||||
<input style={num} type="number" min={0} value={dataSurbs} onChange={(e) => setDataSurbs(+e.target.value)} disabled={connected || busy} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>primary DNS</label>
|
||||
<input style={input} value={primaryDns} onChange={(e) => setPrimaryDns(e.target.value)} placeholder="8.8.8.8:53" disabled={connected || busy} />
|
||||
<label style={sub}>fallback DNS</label>
|
||||
<input style={input} value={fallbackDns} onChange={(e) => setFallbackDns(e.target.value)} placeholder="1.1.1.1:53" disabled={connected || busy} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>dns timeout ms</label>
|
||||
<input style={num} value={dnsTimeout} onChange={(e) => setDnsTimeout(e.target.value)} placeholder="30000" disabled={connected || busy} />
|
||||
<label style={sub}>connect timeout ms</label>
|
||||
<input style={num} value={connectTimeout} onChange={(e) => setConnectTimeout(e.target.value)} placeholder="60000" disabled={connected || busy} />
|
||||
<label style={sub}>max redirects</label>
|
||||
<input style={num} value={maxRedirects} onChange={(e) => setMaxRedirects(e.target.value)} placeholder="5" disabled={connected || busy} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>storage passphrase</label>
|
||||
<input style={input} type="password" value={storagePassphrase} onChange={(e) => setStoragePassphrase(e.target.value)} placeholder="(plaintext if empty)" disabled={connected || busy} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LogPanel lines={lines('tunnel')} placeholder="Press Connect to bring up the tunnel." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Shared mixnet + ethers helpers for demos that route HTTP through mixFetch.
|
||||
// ens has its own copies of the small helpers (it predates this file); railgun
|
||||
// uses these. Consolidate ens onto this later.
|
||||
|
||||
import { FetchRequest, JsonRpcProvider, type Networkish } from 'ethers';
|
||||
import type { MixFetchFn } from './mixTunnel';
|
||||
|
||||
export interface FlatResponse {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: Record<string, string>;
|
||||
body: Uint8Array;
|
||||
}
|
||||
|
||||
export function headersToObj(headers: unknown): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
if (!headers) return out;
|
||||
if (headers instanceof Headers || headers instanceof Map) {
|
||||
for (const [k, v] of (headers as Headers).entries()) out[k.toLowerCase()] = v;
|
||||
return out;
|
||||
}
|
||||
if (Array.isArray(headers)) {
|
||||
for (const [k, v] of headers) out[String(k).toLowerCase()] = v;
|
||||
return out;
|
||||
}
|
||||
if (typeof headers === 'object') {
|
||||
for (const [k, v] of Object.entries(headers as Record<string, string>)) out[k.toLowerCase()] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function callMixFetch(mixFetch: MixFetchFn, url: string, init?: RequestInit): Promise<FlatResponse> {
|
||||
const res = await mixFetch(url, init || {});
|
||||
const body = new Uint8Array(await res.arrayBuffer());
|
||||
return { status: res.status, statusText: res.statusText, headers: headersToObj(res.headers), body };
|
||||
}
|
||||
|
||||
export async function decompressBody(body: Uint8Array, headers: Record<string, string>): Promise<Uint8Array> {
|
||||
if (!body || body.byteLength === 0) return body;
|
||||
const enc = (headers['content-encoding'] || '').toLowerCase().trim();
|
||||
if (!enc || enc === 'identity') return body;
|
||||
let format: 'gzip' | 'deflate' | 'deflate-raw' | null = null;
|
||||
if (enc === 'gzip' || enc === 'x-gzip') format = 'gzip';
|
||||
else if (enc === 'deflate') format = 'deflate';
|
||||
else if (enc === 'deflate-raw') format = 'deflate-raw';
|
||||
if (!format) return body;
|
||||
// body is always a plain ArrayBuffer-backed Uint8Array at runtime; the cast
|
||||
// sidesteps the TS 5.7 generic-typed-array vs BlobPart (ArrayBuffer) mismatch.
|
||||
const stream = new Blob([body as BlobPart]).stream().pipeThrough(new DecompressionStream(format));
|
||||
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||
}
|
||||
|
||||
export function stripContentEncoding(headers: Record<string, string>): Record<string, string> {
|
||||
const out = { ...headers };
|
||||
delete out['content-encoding'];
|
||||
return out;
|
||||
}
|
||||
|
||||
// The getUrl adapter ethers uses: route the request through mixFetch, decompress,
|
||||
// rename the response fields ethers expects.
|
||||
function makeGetUrl(mixFetch: MixFetchFn) {
|
||||
return async (req: FetchRequest) => {
|
||||
const raw = await callMixFetch(mixFetch, req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: (req.body ?? undefined) as BodyInit | undefined,
|
||||
});
|
||||
const body = await decompressBody(raw.body, raw.headers);
|
||||
return {
|
||||
statusCode: raw.status,
|
||||
statusMessage: raw.statusText,
|
||||
headers: stripContentEncoding(raw.headers),
|
||||
body,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// A JsonRpcProvider whose transport is mixFetch (per-instance override).
|
||||
export function buildProvider(rpcUrl: string, mixFetch: MixFetchFn, network: Networkish): JsonRpcProvider {
|
||||
const base = new FetchRequest(rpcUrl);
|
||||
base.getUrlFunc = makeGetUrl(mixFetch);
|
||||
return new JsonRpcProvider(base, network, { staticNetwork: true });
|
||||
}
|
||||
|
||||
// Install a single global FetchRequest URL handler so EVERY ethers HTTP request
|
||||
// (including providers a library constructs internally from URL strings) routes
|
||||
// through mixFetch. Requires a single ethers instance across the bundle: the
|
||||
// `ethers$` alias in next.config.js enforces that.
|
||||
let globalRoutingInstalled = false;
|
||||
export function installGlobalMixFetchRouting(mixFetch: MixFetchFn): void {
|
||||
if (globalRoutingInstalled) return;
|
||||
FetchRequest.registerGetUrl(makeGetUrl(mixFetch));
|
||||
globalRoutingInstalled = true;
|
||||
}
|
||||
|
||||
// Retry wrapper for mixnet-routed RPC calls: the first request on a cold mixnet
|
||||
// path pays TCP-connect + TLS-handshake time, and Railgun's hardcoded timeouts
|
||||
// can fire on it; the retry finds the pool warm.
|
||||
export async function withRetry<T>(
|
||||
fn: () => Promise<T>,
|
||||
label: string,
|
||||
opts: { attempts?: number; delayMs?: number; log?: (msg: string, colour?: string) => void } = {},
|
||||
): Promise<T> {
|
||||
const { attempts = 3, delayMs = 3000, log } = opts;
|
||||
let lastErr: unknown;
|
||||
for (let i = 1; i <= attempts; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e: any) {
|
||||
lastErr = e;
|
||||
const msg = e.shortMessage || e.message || String(e);
|
||||
if (i < attempts) {
|
||||
log?.(`${label} attempt ${i}/${attempts} failed (${msg}), retrying in ${delayMs / 1000}s...`, 'orange');
|
||||
await new Promise((r) => setTimeout(r, delayMs));
|
||||
} else {
|
||||
log?.(`${label} failed after ${attempts} attempts: ${msg}`, 'red');
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Shared UI primitives for the in-docs demos (ens, railgun). These live with
|
||||
// the playground today; re-exported here so the demo components import from one
|
||||
// stable place rather than reaching into ../playground. If the primitives ever
|
||||
// move to a neutral home, only this file changes.
|
||||
export * from '../../playground/ui';
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"nodes": 683,
|
||||
"nodes": 685,
|
||||
"locations": 75,
|
||||
"mixnodes": 240,
|
||||
"exit_gateways": 435
|
||||
"exit_gateways": 437
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
Tuesday, June 9th 2026, 13:20:08 UTC
|
||||
Tuesday, June 9th 2026, 16:06:04 UTC
|
||||
|
||||
@@ -4,117 +4,95 @@ Start this nym-node
|
||||
Usage: nym-node run [OPTIONS]
|
||||
|
||||
Options:
|
||||
--id <ID>
|
||||
Id of the nym-node to use [env: NYMNODE_ID=] [default: default-nym-node]
|
||||
--config-file <CONFIG_FILE>
|
||||
Path to a configuration file of this node [env: NYMNODE_CONFIG=]
|
||||
--accept-operator-terms-and-conditions
|
||||
Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env:
|
||||
NYMNODE_ACCEPT_OPERATOR_TERMS=]
|
||||
--deny-init
|
||||
Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist [env: NYMNODE_DENY_INIT=]
|
||||
--init-only
|
||||
If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env: NYMNODE_INIT_ONLY=]
|
||||
--local
|
||||
Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
|
||||
--mode [<MODE>...]
|
||||
Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
--modes <MODES>
|
||||
Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
-w, --write-changes
|
||||
If this node has been initialised before, specify whether to write any new changes to the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=]
|
||||
--bonding-information-output <BONDING_INFORMATION_OUTPUT>
|
||||
Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding information is still a subject to change and this argument should be
|
||||
treated only as a preview of future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=]
|
||||
-o, --output <OUTPUT>
|
||||
Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible values: text, json]
|
||||
--public-ips <PUBLIC_IPS>
|
||||
Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In nearly all circumstances, it's going to be identical to the
|
||||
address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=]
|
||||
--hostname <HOSTNAME>
|
||||
Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients [env: NYMNODE_HOSTNAME=]
|
||||
--location <LOCATION>
|
||||
Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit
|
||||
numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
|
||||
--http-bind-address <HTTP_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
|
||||
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH>
|
||||
Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
|
||||
--http-access-token <HTTP_ACCESS_TOKEN>
|
||||
An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=]
|
||||
--expose-system-info <EXPOSE_SYSTEM_INFO>
|
||||
Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values: true, false]
|
||||
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE>
|
||||
Specify whether basic system hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
|
||||
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE>
|
||||
Specify whether detailed system crypto hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
|
||||
--nyxd-urls <NYXD_URLS>
|
||||
Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
|
||||
--nyxd-websocket-url <NYXD_WEBSOCKET_URL>
|
||||
Url to the websocket endpoint of a nyx validator, for example `wss://rpc.nymtech.net/websocket`. It is used for subscribing to new block events [env: NYMNODE_NYXD_WEBSOCKET=]
|
||||
--mixnet-bind-address <MIXNET_BIND_ADDRESS>
|
||||
Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
|
||||
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT>
|
||||
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
|
||||
--nym-api-urls <NYM_API_URLS>
|
||||
Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
|
||||
--enable-console-logging <ENABLE_CONSOLE_LOGGING>
|
||||
Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false]
|
||||
--wireguard-enabled <WIREGUARD_ENABLED>
|
||||
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
|
||||
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
|
||||
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT>
|
||||
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
|
||||
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env:
|
||||
NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
|
||||
--wireguard-userspace <WIREGUARD_USERSPACE>
|
||||
Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=]
|
||||
[possible values: true, false]
|
||||
--verloc-bind-address <VERLOC_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
|
||||
--verloc-announce-port <VERLOC_ANNOUNCE_PORT>
|
||||
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
|
||||
--entry-bind-address <ENTRY_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=]
|
||||
--announce-ws-port <ANNOUNCE_WS_PORT>
|
||||
Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
|
||||
--announce-wss-port <ANNOUNCE_WSS_PORT>
|
||||
If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
|
||||
--enforce-zk-nyms <ENFORCE_ZK_NYMS>
|
||||
Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible
|
||||
values: true, false]
|
||||
--mnemonic <MNEMONIC>
|
||||
Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=]
|
||||
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL>
|
||||
Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
|
||||
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY>
|
||||
Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env:
|
||||
NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
|
||||
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL>
|
||||
Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
|
||||
--open-proxy <OPEN_PROXY>
|
||||
Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible values: true,
|
||||
false]
|
||||
--nr-allow-local-ips <NR_ALLOW_LOCAL_IPS>
|
||||
Allow the network requester to forward traffic to non-globally-routable addresses. Intended for local development, private-network deployments, and testnet scenarios. Not
|
||||
recommended on production exit gateway unless you know what you're doing [env: NYMNODE_NR_ALLOW_LOCAL_IPS=] [possible values: true, false]
|
||||
--ipr-allow-local-ips <IPR_ALLOW_LOCAL_IPS>
|
||||
Allow the IP packet router to forward traffic to non-globally-routable addresses. Intended for local development, private-network deployments, and testnet scenarios. Not recommended
|
||||
on production exit gateway unless you know what you're doing [env: NYMNODE_IPR_ALLOW_LOCAL_IPS=] [possible values: true, false]
|
||||
--lp-control-bind-address <LP_CONTROL_BIND_ADDRESS>
|
||||
Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=]
|
||||
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT>
|
||||
Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the `lp_control_bind_address` will be used instead [env:
|
||||
NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
|
||||
--lp-data-bind-address <LP_DATA_BIND_ADDRESS>
|
||||
Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=]
|
||||
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT>
|
||||
Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the `lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
|
||||
--lp-use-mock-ecash <LP_USE_MOCK_ECASH>
|
||||
Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP listener will accept any credential without
|
||||
blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
|
||||
-h, --help
|
||||
Print help
|
||||
--id <ID> Id of the nym-node to use [env: NYMNODE_ID=] [default: default-nym-node]
|
||||
--config-file <CONFIG_FILE> Path to a configuration file of this node [env: NYMNODE_CONFIG=]
|
||||
--accept-operator-terms-and-conditions Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at
|
||||
<https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env: NYMNODE_ACCEPT_OPERATOR_TERMS=]
|
||||
--deny-init Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist
|
||||
[env: NYMNODE_DENY_INIT=]
|
||||
--init-only If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses
|
||||
[env: NYMNODE_INIT_ONLY=]
|
||||
--local Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
|
||||
--mode [<MODE>...] Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway,
|
||||
exit-gateway, exit-providers-only]
|
||||
--modes <MODES> Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode,
|
||||
entry-gateway, exit-gateway, exit-providers-only]
|
||||
-w, --write-changes If this node has been initialised before, specify whether to write any new changes to the config file [env:
|
||||
NYMNODE_WRITE_CONFIG_CHANGES=]
|
||||
--bonding-information-output <BONDING_INFORMATION_OUTPUT> Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding
|
||||
information is still a subject to change and this argument should be treated only as a preview of future features [env:
|
||||
NYMNODE_BONDING_INFORMATION_OUTPUT=]
|
||||
-o, --output <OUTPUT> Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible
|
||||
values: text, json]
|
||||
--public-ips <PUBLIC_IPS> Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In
|
||||
nearly all circumstances, it's going to be identical to the address you're going to use for bonding [env:
|
||||
NYMNODE_PUBLIC_IPS=]
|
||||
--hostname <HOSTNAME> Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients
|
||||
[env: NYMNODE_HOSTNAME=]
|
||||
--location <LOCATION> Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g.
|
||||
'PL'), three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
|
||||
--http-bind-address <HTTP_BIND_ADDRESS> Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
|
||||
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH> Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
|
||||
--http-access-token <HTTP_ACCESS_TOKEN> An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env:
|
||||
NYMNODE_HTTP_ACCESS_TOKEN=]
|
||||
--expose-system-info <EXPOSE_SYSTEM_INFO> Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=]
|
||||
[possible values: true, false]
|
||||
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE> Specify whether basic system hardware information should be exposed. default: true [env:
|
||||
NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
|
||||
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE> Specify whether detailed system crypto hardware information should be exposed. default: true [env:
|
||||
NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
|
||||
--nyxd-urls <NYXD_URLS> Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
|
||||
--nyxd-websocket-url <NYXD_WEBSOCKET_URL> Url to the websocket endpoint of a nyx validator, for example `wss://rpc.nymtech.net/websocket`. It is used for
|
||||
subscribing to new block events [env: NYMNODE_NYXD_WEBSOCKET=]
|
||||
--mixnet-bind-address <MIXNET_BIND_ADDRESS> Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
|
||||
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the
|
||||
node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
|
||||
--nym-api-urls <NYM_API_URLS> Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
|
||||
--enable-console-logging <ENABLE_CONSOLE_LOGGING> Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=]
|
||||
[possible values: true, false]
|
||||
--wireguard-enabled <WIREGUARD_ENABLED> Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true,
|
||||
false]
|
||||
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS> Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env:
|
||||
NYMNODE_WG_BIND_ADDRESS=]
|
||||
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT> Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where
|
||||
the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX> The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is
|
||||
32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
|
||||
--wireguard-userspace <WIREGUARD_USERSPACE> Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments
|
||||
without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] [possible values: true, false]
|
||||
--verloc-bind-address <VERLOC_BIND_ADDRESS> Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
|
||||
--verloc-announce-port <VERLOC_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the
|
||||
node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
|
||||
--entry-bind-address <ENTRY_BIND_ADDRESS> Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env:
|
||||
NYMNODE_ENTRY_BIND_ADDRESS=]
|
||||
--announce-ws-port <ANNOUNCE_WS_PORT> Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will
|
||||
be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
|
||||
--announce-wss-port <ANNOUNCE_WSS_PORT> If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
|
||||
--enforce-zk-nyms <ENFORCE_ZK_NYMS> Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts
|
||||
non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, false]
|
||||
--mnemonic <MNEMONIC> Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be
|
||||
generated [env: NYMNODE_MNEMONIC=]
|
||||
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL> Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and
|
||||
local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
|
||||
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY> Expected public key of the entity signing the published attestation. This argument should never be set outside testnets
|
||||
and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
|
||||
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL> Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
|
||||
--open-proxy <OPEN_PROXY> Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it
|
||||
receives [env: NYMNODE_OPEN_PROXY=] [possible values: true, false]
|
||||
--nr-allow-local-ips <NR_ALLOW_LOCAL_IPS> Allow the network requester to forward traffic to non-globally-routable addresses. Intended for local development,
|
||||
private-network deployments, and testnet scenarios. Not recommended on production exit gateway unless you know what
|
||||
you're doing [env: NYMNODE_NR_ALLOW_LOCAL_IPS=] [possible values: true, false]
|
||||
--ipr-allow-local-ips <IPR_ALLOW_LOCAL_IPS> Allow the IP packet router to forward traffic to non-globally-routable addresses. Intended for local development,
|
||||
private-network deployments, and testnet scenarios. Not recommended on production exit gateway unless you know what
|
||||
you're doing [env: NYMNODE_IPR_ALLOW_LOCAL_IPS=] [possible values: true, false]
|
||||
--lp-control-bind-address <LP_CONTROL_BIND_ADDRESS> Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=]
|
||||
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT> Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the
|
||||
`lp_control_bind_address` will be used instead [env: NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
|
||||
--lp-data-bind-address <LP_DATA_BIND_ADDRESS> Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=]
|
||||
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT> Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the
|
||||
`lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
|
||||
--lp-use-mock-ecash <LP_USE_MOCK_ECASH> Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When
|
||||
enabled, the LP listener will accept any credential without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=]
|
||||
[possible values: true, false]
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
@@ -34,6 +34,48 @@ nextra.webpack = (config, options) => {
|
||||
// }),
|
||||
// );
|
||||
|
||||
// --- Railgun demo: browser polyfills for the @railgun-community SDK ---
|
||||
// Railgun pulls libp2p / pouchdb / crypto transitively and expects Node-stdlib
|
||||
// globals that webpack 5 no longer auto-polyfills. Client build only; the SSR
|
||||
// build resolves these natively. Mirrors wasm/railgun-demo/webpack.config.js.
|
||||
if (!options.isServer) {
|
||||
newConfig.resolve.fallback = {
|
||||
...newConfig.resolve.fallback,
|
||||
buffer: require.resolve("buffer/"),
|
||||
crypto: require.resolve("crypto-browserify"),
|
||||
http: require.resolve("stream-http"),
|
||||
https: require.resolve("https-browserify"),
|
||||
stream: require.resolve("stream-browserify"),
|
||||
url: require.resolve("url/"),
|
||||
vm: require.resolve("vm-browserify"),
|
||||
zlib: require.resolve("browserify-zlib"),
|
||||
};
|
||||
// Force single instances of ethers and shared-models. ethers: so Railgun's
|
||||
// global FetchRequest.registerGetUrl shares static state with our import.
|
||||
// shared-models: so our `NETWORK_CONFIG[...].poi = undefined` POI sidestep
|
||||
// mutates the SAME object the engine's loadProvider reads (otherwise an
|
||||
// ESM/CJS split gives two copies and the POI gate still fires).
|
||||
newConfig.resolve.alias = {
|
||||
...newConfig.resolve.alias,
|
||||
ethers$: require.resolve("ethers"),
|
||||
"@railgun-community/shared-models$": require.resolve("@railgun-community/shared-models"),
|
||||
};
|
||||
newConfig.plugins.push(
|
||||
new options.webpack.ProvidePlugin({
|
||||
Buffer: ["buffer", "Buffer"],
|
||||
process: "process/browser",
|
||||
}),
|
||||
);
|
||||
// Railgun ships its zk-SNARK circuits as async WASM.
|
||||
newConfig.experiments = { ...newConfig.experiments, asyncWebAssembly: true };
|
||||
}
|
||||
// Silence "Critical dependency" warnings from Railgun's GraphQL subgraph plumbing.
|
||||
newConfig.ignoreWarnings = [
|
||||
...(newConfig.ignoreWarnings || []),
|
||||
{ module: /@graphql-mesh/, message: /Critical dependency/ },
|
||||
{ module: /@graphql-tools\/url-loader/, message: /Critical dependency/ },
|
||||
];
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
@@ -1484,7 +1526,7 @@ const config = {
|
||||
form-action 'self';
|
||||
frame-ancestors 'none';
|
||||
upgrade-insecure-requests;
|
||||
connect-src 'self' wss://nym-node-cli.devrel.nymte.ch:9001 https://github.com *.vercel.app *.nymtech.net *.nymvpn.com *.nymte.ch *.nyx.network *.nym.com https://nym.com nymvpn.com https://nymvpn.com *.nymtech.cc;
|
||||
connect-src 'self' wss://* wss://nym-node-cli.devrel.nymte.ch:9001 https://github.com *.vercel.app *.nymtech.net *.nymvpn.com *.nymte.ch *.nyx.network *.nym.com https://nym.com nymvpn.com https://nymvpn.com *.nymtech.cc https://ipinfo.io;
|
||||
frame-src 'self' https://vercel.live *.vercel.app *.nym.com https://nym.com;
|
||||
worker-src 'self' blob: https://vercel.live *.vercel.app *.nym.com https://nym.com;
|
||||
`;
|
||||
|
||||
@@ -44,12 +44,16 @@
|
||||
"@nymproject/mix-tunnel": "^0.1.0",
|
||||
"@nymproject/mix-websocket": "^0.1.0",
|
||||
"@nymproject/sdk-full-fat": ">=1.5.1-rc.0 || ^1.4.1",
|
||||
"@railgun-community/shared-models": "7.5.0",
|
||||
"@railgun-community/wallet": "10.4.0",
|
||||
"@redocly/cli": "^1.25.15",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"chain-registry": "^1.19.0",
|
||||
"cosmjs-types": "^0.9.0",
|
||||
"ethers": "^6.13.1",
|
||||
"framer-motion": "^12.34.5",
|
||||
"lucide-react": "^0.438.0",
|
||||
"memory-level": "^1.0.0",
|
||||
"next": "15.5.10",
|
||||
"nextra": "2",
|
||||
"nextra-theme-docs": "2",
|
||||
@@ -65,6 +69,15 @@
|
||||
"@types/react": "^18.3.26",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"process": "^0.11.10",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.2.0",
|
||||
"url": "^0.11.4",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"eslint": "8.46.0",
|
||||
"eslint-config-next": "13.4.13",
|
||||
"next-sitemap": "4.2.3",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"title": "TypeScript"
|
||||
},
|
||||
"playground": "Playground (embedded clients)",
|
||||
"demos": "Demos",
|
||||
"mix-tunnel": "mix-tunnel (shared tunnel)",
|
||||
"mix-fetch": "mix-fetch (HTTPS requests)",
|
||||
"mix-dns": "mix-dns (DNS resolution)",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"ens": "ENS over the mixnet",
|
||||
"railgun": "Shielding ETH with Railgun"
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: "Demo: ENS resolution over the Nym mixnet"
|
||||
description: "Resolve an ENS name to its address and IPFS contenthash, then fetch the site, with every JSON-RPC and gateway request routed through the mixnet via mix-fetch. Shows the ethers-to-mixFetch adapter."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-09"
|
||||
---
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
export const EnsDemo = dynamic(
|
||||
() => import('../../../components/demos/ens/EnsDemo').then((m) => m.EnsDemo),
|
||||
{ ssr: false },
|
||||
)
|
||||
|
||||
import { NetworkTabCallout } from '../../../components/demos/shared/NetworkTabCallout'
|
||||
import { MixnetGlossary } from '../../../components/demos/shared/MixnetGlossary'
|
||||
|
||||
# ENS over the mixnet
|
||||
|
||||
A normal ENS lookup (name to address to IPFS website) built with
|
||||
[ethers.js](https://docs.ethers.org/v6/), except every network request goes
|
||||
through the Nym mixnet instead of leaving over your normal connection. The
|
||||
Ethereum RPC node and the IPFS gateway see the [IPR](/network/infrastructure/exit-services#ip-packet-router)
|
||||
exit's IP, not yours, and your ISP cannot see which names or sites you reach.
|
||||
The trade-off is latency: every packet takes a multi-relay path, so requests are
|
||||
slower than a direct route.
|
||||
|
||||
## How it works
|
||||
|
||||
The whole integration is one adapter. ethers v6 exposes
|
||||
`FetchRequest.getUrlFunc` as a settable property, so you replace its HTTP
|
||||
transport with a function that calls [`mixFetch`](/developers/mix-fetch). To
|
||||
ethers it looks like an ordinary fetch; to the mixnet, ethers looks like any
|
||||
other caller.
|
||||
|
||||
```ts
|
||||
import { JsonRpcProvider, FetchRequest } from 'ethers';
|
||||
import { mixFetch } from '@nymproject/mix-fetch';
|
||||
|
||||
function buildProvider(rpcUrl: string): JsonRpcProvider {
|
||||
const base = new FetchRequest(rpcUrl);
|
||||
|
||||
// Route every JSON-RPC call through mixFetch, renaming the response
|
||||
// fields ethers expects (status -> statusCode, statusText -> statusMessage).
|
||||
base.getUrlFunc = async (req) => {
|
||||
const res = await mixFetch(req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body ?? undefined,
|
||||
});
|
||||
return {
|
||||
statusCode: res.status,
|
||||
statusMessage: res.statusText,
|
||||
headers: Object.fromEntries(res.headers),
|
||||
body: new Uint8Array(await res.arrayBuffer()),
|
||||
};
|
||||
};
|
||||
|
||||
// staticNetwork skips the eth_chainId probe: one mixnet round trip saved.
|
||||
return new JsonRpcProvider(base, 'mainnet', { staticNetwork: true });
|
||||
}
|
||||
```
|
||||
|
||||
One caveat: browser `fetch` decompresses gzip transparently and `mixFetch` does
|
||||
not, so the demo adds a `DecompressionStream` step after each response (Cloudflare
|
||||
gzips RPC replies). The full version with decompression and per-call logging is in
|
||||
[`components/demos/ens/lib.ts`](https://github.com/nymtech/nym/tree/develop/documentation/docs/components/demos/ens).
|
||||
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch) and [`ethers`](https://www.npmjs.com/package/ethers).
|
||||
|
||||
The lookup itself is three steps, each an Ethereum call or HTTPS GET over the same
|
||||
tunnel:
|
||||
|
||||
1. **Resolve address.** Two `eth_call`s: the ENS Registry's `resolver(node)`
|
||||
returns the resolver contract for the name, then `resolver.addr(node)` returns
|
||||
the Ethereum address. `node` is the namehash (recursive keccak256 over the
|
||||
labels).
|
||||
2. **Get contenthash.** One more `eth_call`: `resolver.contenthash(node)`. ethers
|
||||
decodes the EIP-1577 multicodec bytes to a URI; this demo handles `ipfs://`.
|
||||
3. **Fetch from IPFS.** A plain HTTPS GET to a gateway with the CID as a subdomain
|
||||
or path label. CIDv0 (`Qm...`) is re-encoded as CIDv1 (`bafy...`) for subdomain
|
||||
gateways, since DNS is case-insensitive.
|
||||
|
||||
## Try it
|
||||
|
||||
Connect to bring the tunnel up (a default IPR exit is pinned; tick **Use random
|
||||
IPR** for auto-discovery), click **Verify IP routing** to confirm traffic exits
|
||||
through Nym, then run the three steps.
|
||||
|
||||
<NetworkTabCallout />
|
||||
|
||||
<EnsDemo />
|
||||
|
||||
## What to expect
|
||||
|
||||
- **The first request is the slow one.** Connecting builds the mixnet client and
|
||||
handshakes with the IPR; no TCP or TLS yet. The first request to a host then
|
||||
runs a TCP and TLS handshake carried as IP packets over the mixnet (several
|
||||
sequential round trips). smolmix keeps that connection warm and reuses it, so
|
||||
later requests to the same host are much quicker. A long pause is handshakes in
|
||||
flight, not a hang.
|
||||
- **You will not see the tunnelled requests in DevTools.** The RPC and IPFS
|
||||
requests never touch the browser's `fetch`. They leave the worker as encrypted
|
||||
packets over a single WebSocket to the entry gateway, which is the one
|
||||
connection the Network tab shows. The exception is **Verify IP routing**, which
|
||||
deliberately makes one direct clearnet call to ipinfo.io for comparison.
|
||||
- **Rate limiting.** Public IPFS gateways and Ethereum RPCs rate-limit shared IP
|
||||
addresses. If requests start failing with 403, 429, or connection errors, the
|
||||
exit IP is likely flagged: tick **Use random IPR** and reload for a fresh exit.
|
||||
|
||||
## Glossary
|
||||
|
||||
<MixnetGlossary />
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: "Demo: Shielding testnet ETH into Railgun over the Nym mixnet"
|
||||
description: "Shield testnet ETH into a Railgun private note with every Ethereum RPC call routed through the Nym mixnet. Shows the global ethers-to-mixFetch routing that covers a whole SDK."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-09"
|
||||
---
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
export const RailgunDemo = dynamic(
|
||||
() => import('../../../components/demos/railgun/RailgunDemo').then((m) => m.RailgunDemo),
|
||||
{ ssr: false },
|
||||
)
|
||||
|
||||
import { NetworkTabCallout } from '../../../components/demos/shared/NetworkTabCallout'
|
||||
import { MixnetGlossary } from '../../../components/demos/shared/MixnetGlossary'
|
||||
|
||||
# Shielding testnet ETH into Railgun over the mixnet
|
||||
|
||||
**Nym** hides the network layer: every Ethereum RPC
|
||||
call goes through the mixnet via [`mixFetch`](/developers/mix-fetch), so the RPC
|
||||
node and your ISP cannot link you to the query, and **Railgun** hides the
|
||||
application layer, as shielded notes break the on-chain link between sender,
|
||||
receiver, and amount. This demo covers just the **shield** step on Sepolia:
|
||||
depositing testnet ETH into a private note. It does not do private transfers or
|
||||
unshielding.
|
||||
|
||||
## What you can do here
|
||||
|
||||
This page is interactive. You bring up a mixnet tunnel, derive a Railgun wallet,
|
||||
and broadcast a **real shield transaction** on the Sepolia testnet, with every
|
||||
Ethereum RPC call routed through the mixnet. The shield lands on chain (you can
|
||||
open it on Etherscan), but the IP that submitted it is the Nym exit's, not yours.
|
||||
|
||||
The entire integration is a single ethers shim (shown below). Because the
|
||||
Railgun engine talks to the chain through the `ethers` library, routing through
|
||||
`mixFetch` is enough to put a whole privacy SDK behind the mixnet. The same
|
||||
pattern drops into any `ethers`-based app or library.
|
||||
|
||||
## How it works
|
||||
|
||||
The [ENS demo](/developers/demos/ens) swapped one provider's transport, but Railgun
|
||||
constructs its own providers internally, so routing only our provider would leak
|
||||
the engine's RPC to clearnet. Instead this demo installs a **global** `ethers`
|
||||
transport: `FetchRequest.registerGetUrl` routes every ethers HTTP call in the
|
||||
page through `mixFetch`, including the ones the Railgun engine makes.
|
||||
|
||||
```ts
|
||||
import { FetchRequest } from 'ethers';
|
||||
import { mixFetch } from '@nymproject/mix-fetch';
|
||||
|
||||
// Every ethers HTTP request in the process now goes through the mixnet.
|
||||
FetchRequest.registerGetUrl(async (req) => {
|
||||
const res = await mixFetch(req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body ?? undefined,
|
||||
});
|
||||
return {
|
||||
statusCode: res.status,
|
||||
statusMessage: res.statusText,
|
||||
headers: Object.fromEntries(res.headers),
|
||||
body: new Uint8Array(await res.arrayBuffer()),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
`registerGetUrl` is global static state on the `FetchRequest` class, so this only
|
||||
works if `ethers` is a **single instance** across your bundle. If your app and
|
||||
Railgun resolve to different ethers copies, the handler installs on one and the
|
||||
engine uses the other. Pin the exact version Railgun peer-depends on.
|
||||
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch), [`@railgun-community/wallet`](https://www.npmjs.com/package/@railgun-community/wallet), and [`ethers`](https://www.npmjs.com/package/ethers).
|
||||
|
||||
Shielding is a four-step flow, all over the mixnet: sign a shield key, estimate
|
||||
gas, populate the transaction, then sign and broadcast. The broadcast that lands
|
||||
on Sepolia is observable on Etherscan, but the IP that submitted it stays hidden.
|
||||
|
||||
## Try it
|
||||
|
||||
<div className="nx-mt-4" />
|
||||
|
||||
The demo auto-loads a funded Sepolia testnet wallet. Connect the tunnel (the
|
||||
Railgun address derives once the engine is up), check the balance, then shield a
|
||||
small amount. If the wallet is low, top it up at a
|
||||
[Sepolia faucet](https://sepoliafaucet.com/) using the public address shown.
|
||||
|
||||
**Sepolia testnet only.** The wallet holds test ETH and the mnemonic is
|
||||
stored in plain browser storage. Never paste a mainnet mnemonic.
|
||||
|
||||
<NetworkTabCallout />
|
||||
|
||||
<RailgunDemo />
|
||||
|
||||
## What to expect
|
||||
|
||||
- **Engine init is the slow part.** `loadProvider` hits Sepolia over a cold
|
||||
mixnet route, which can exceed Railgun's internal timeout on the first try; the
|
||||
demo retries and the second attempt finds the connection pool warm.
|
||||
- **Shielding makes several RPC calls** (gas estimate, fee data, broadcast,
|
||||
receipt), each a mixnet round trip. The broadcast step retries idempotently:
|
||||
the tx hash is fixed before broadcasting, so a dropped response can be re-sent
|
||||
or detected as already-on-chain.
|
||||
- **Rate limiting.** If RPC calls start failing with 403/429 or connection
|
||||
errors, the exit IP is flagged: disconnect, tick **Use random IPR**, reload,
|
||||
and reconnect for a fresh exit.
|
||||
|
||||
## Glossary
|
||||
|
||||
<MixnetGlossary />
|
||||
@@ -9,6 +9,7 @@ lastUpdated: "2026-06-09"
|
||||
import { Callout } from 'nextra/components'
|
||||
import { MixPlayground } from '../../components/playground/MixPlayground'
|
||||
import { MessagingDemo } from '../../components/playground/messaging-section'
|
||||
import { NetworkTabCallout } from '../../components/demos/shared/NetworkTabCallout'
|
||||
|
||||
# Mixnet playground
|
||||
|
||||
@@ -19,18 +20,11 @@ This playground runs Nym's browser TypeScript packages against the live mixnet.
|
||||
|
||||
Some sections send the same request over the tunnel and over the clearnet, so you can compare the two.
|
||||
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch), [`@nymproject/mix-dns`](https://www.npmjs.com/package/@nymproject/mix-dns), [`@nymproject/mix-tunnel`](https://www.npmjs.com/package/@nymproject/mix-tunnel), [`@nymproject/mix-websocket`](https://www.npmjs.com/package/@nymproject/mix-websocket), and [`@nymproject/sdk`](https://www.npmjs.com/package/@nymproject/sdk).
|
||||
|
||||
## HTTPS / DNS / WebSockets
|
||||
|
||||
<Callout type="info">
|
||||
**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.
|
||||
</Callout>
|
||||
<NetworkTabCallout />
|
||||
|
||||
<Callout type="info">
|
||||
Everything here runs client-side over the live Nym mixnet. The first
|
||||
|
||||
Generated
+5499
-98
File diff suppressed because it is too large
Load Diff
@@ -7,3 +7,16 @@ allowBuilds:
|
||||
sharp: false
|
||||
tiny-secp256k1: false
|
||||
unrs-resolver: false
|
||||
# Pulled in by the railgun demo deps. Native crypto / ws speedups (the browser
|
||||
# bundle uses the JS implementations) and postinstall scripts the webpack-built
|
||||
# demo does not need; skip their builds (and avoid native-compile failures in CI).
|
||||
"@railgun-community/wallet": false
|
||||
blake-hash: false
|
||||
bufferutil: false
|
||||
es5-ext: false
|
||||
keccak: false
|
||||
secp256k1: false
|
||||
utf-8-validate: false
|
||||
web3: false
|
||||
web3-bzz: false
|
||||
web3-shh: false
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
@version: 1.20.4
|
||||
@generated: 2026-06-09
|
||||
@pages: 163
|
||||
@pages: 165
|
||||
@source: https://github.com/nymtech/nym/tree/develop/documentation/docs
|
||||
|
||||
---
|
||||
@@ -3671,16 +3671,9 @@ This playground runs Nym's browser TypeScript packages against the live mixnet.
|
||||
|
||||
Some sections send the same request over the tunnel and over the clearnet, so you can compare the two.
|
||||
|
||||
## HTTPS / DNS / WebSockets
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch), [`@nymproject/mix-dns`](https://www.npmjs.com/package/@nymproject/mix-dns), [`@nymproject/mix-tunnel`](https://www.npmjs.com/package/@nymproject/mix-tunnel), [`@nymproject/mix-websocket`](https://www.npmjs.com/package/@nymproject/mix-websocket), and [`@nymproject/sdk`](https://www.npmjs.com/package/@nymproject/sdk).
|
||||
|
||||
**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.
|
||||
## HTTPS / DNS / WebSockets
|
||||
|
||||
Everything here runs client-side over the live Nym mixnet. The first
|
||||
`setupMixTunnel` is slow (a few seconds): it loads the WebAssembly client,
|
||||
@@ -3702,6 +3695,200 @@ 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: Demo: ENS resolution over the Nym mixnet
|
||||
description: Resolve an ENS name to its address and IPFS contenthash, then fetch the site, with every JSON-RPC and gateway request routed through the mixnet via mix-fetch. Shows the ethers-to-mixFetch adapter.
|
||||
url: https://nym.com/docs/developers/demos/ens
|
||||
---
|
||||
|
||||
export const EnsDemo = dynamic(
|
||||
() => import('../../../components/demos/ens/EnsDemo').then((m) => m.EnsDemo),
|
||||
{ ssr: false },
|
||||
)
|
||||
|
||||
# ENS over the mixnet
|
||||
|
||||
A normal ENS lookup (name to address to IPFS website) built with
|
||||
[ethers.js](https://docs.ethers.org/v6/), except every network request goes
|
||||
through the Nym mixnet instead of leaving over your normal connection. The
|
||||
Ethereum RPC node and the IPFS gateway see the [IPR](/network/infrastructure/exit-services#ip-packet-router)
|
||||
exit's IP, not yours, and your ISP cannot see which names or sites you reach.
|
||||
The trade-off is latency: every packet takes a multi-relay path, so requests are
|
||||
slower than a direct route.
|
||||
|
||||
## How it works
|
||||
|
||||
The whole integration is one adapter. ethers v6 exposes
|
||||
`FetchRequest.getUrlFunc` as a settable property, so you replace its HTTP
|
||||
transport with a function that calls [`mixFetch`](/developers/mix-fetch). To
|
||||
ethers it looks like an ordinary fetch; to the mixnet, ethers looks like any
|
||||
other caller.
|
||||
|
||||
```ts
|
||||
|
||||
function buildProvider(rpcUrl: string): JsonRpcProvider {
|
||||
const base = new FetchRequest(rpcUrl);
|
||||
|
||||
// Route every JSON-RPC call through mixFetch, renaming the response
|
||||
// fields ethers expects (status -> statusCode, statusText -> statusMessage).
|
||||
base.getUrlFunc = async (req) => {
|
||||
const res = await mixFetch(req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body ?? undefined,
|
||||
});
|
||||
return {
|
||||
statusCode: res.status,
|
||||
statusMessage: res.statusText,
|
||||
headers: Object.fromEntries(res.headers),
|
||||
body: new Uint8Array(await res.arrayBuffer()),
|
||||
};
|
||||
};
|
||||
|
||||
// staticNetwork skips the eth_chainId probe: one mixnet round trip saved.
|
||||
return new JsonRpcProvider(base, 'mainnet', { staticNetwork: true });
|
||||
}
|
||||
```
|
||||
|
||||
One caveat: browser `fetch` decompresses gzip transparently and `mixFetch` does
|
||||
not, so the demo adds a `DecompressionStream` step after each response (Cloudflare
|
||||
gzips RPC replies). The full version with decompression and per-call logging is in
|
||||
[`components/demos/ens/lib.ts`](https://github.com/nymtech/nym/tree/develop/documentation/docs/components/demos/ens).
|
||||
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch) and [`ethers`](https://www.npmjs.com/package/ethers).
|
||||
|
||||
The lookup itself is three steps, each an Ethereum call or HTTPS GET over the same
|
||||
tunnel:
|
||||
|
||||
1. **Resolve address.** Two `eth_call`s: the ENS Registry's `resolver(node)`
|
||||
returns the resolver contract for the name, then `resolver.addr(node)` returns
|
||||
the Ethereum address. `node` is the namehash (recursive keccak256 over the
|
||||
labels).
|
||||
2. **Get contenthash.** One more `eth_call`: `resolver.contenthash(node)`. ethers
|
||||
decodes the EIP-1577 multicodec bytes to a URI; this demo handles `ipfs://`.
|
||||
3. **Fetch from IPFS.** A plain HTTPS GET to a gateway with the CID as a subdomain
|
||||
or path label. CIDv0 (`Qm...`) is re-encoded as CIDv1 (`bafy...`) for subdomain
|
||||
gateways, since DNS is case-insensitive.
|
||||
|
||||
## Try it
|
||||
|
||||
Connect to bring the tunnel up (a default IPR exit is pinned; tick **Use random
|
||||
IPR** for auto-discovery), click **Verify IP routing** to confirm traffic exits
|
||||
through Nym, then run the three steps.
|
||||
|
||||
## What to expect
|
||||
|
||||
- **The first request is the slow one.** Connecting builds the mixnet client and
|
||||
handshakes with the IPR; no TCP or TLS yet. The first request to a host then
|
||||
runs a TCP and TLS handshake carried as IP packets over the mixnet (several
|
||||
sequential round trips). smolmix keeps that connection warm and reuses it, so
|
||||
later requests to the same host are much quicker. A long pause is handshakes in
|
||||
flight, not a hang.
|
||||
- **You will not see the tunnelled requests in DevTools.** The RPC and IPFS
|
||||
requests never touch the browser's `fetch`. They leave the worker as encrypted
|
||||
packets over a single WebSocket to the entry gateway, which is the one
|
||||
connection the Network tab shows. The exception is **Verify IP routing**, which
|
||||
deliberately makes one direct clearnet call to ipinfo.io for comparison.
|
||||
- **Rate limiting.** Public IPFS gateways and Ethereum RPCs rate-limit shared IP
|
||||
addresses. If requests start failing with 403, 429, or connection errors, the
|
||||
exit IP is likely flagged: tick **Use random IPR** and reload for a fresh exit.
|
||||
|
||||
## Glossary
|
||||
|
||||
---
|
||||
title: Demo: Railgun private payments over the Nym mixnet
|
||||
description: Shield testnet ETH into a Railgun private note with every Ethereum RPC call routed through the Nym mixnet. Shows the global ethers-to-mixFetch routing that covers a whole SDK.
|
||||
url: https://nym.com/docs/developers/demos/railgun
|
||||
---
|
||||
|
||||
export const RailgunDemo = dynamic(
|
||||
() => import('../../../components/demos/railgun/RailgunDemo').then((m) => m.RailgunDemo),
|
||||
{ ssr: false },
|
||||
)
|
||||
|
||||
# Railgun over the mixnet
|
||||
|
||||
Two privacy layers stacked. **Nym** hides the network layer: every Ethereum RPC
|
||||
call goes through the mixnet via [`mixFetch`](/developers/mix-fetch), so the RPC
|
||||
node and your ISP cannot link you to the query. **Railgun** hides the
|
||||
application layer: shielded notes break the on-chain link between sender,
|
||||
receiver, and amount. This demo shields testnet ETH on Sepolia.
|
||||
|
||||
## What you can do here
|
||||
|
||||
This page is interactive. You bring up a mixnet tunnel, derive a Railgun wallet,
|
||||
and broadcast a **real shield transaction** on the Sepolia testnet, with every
|
||||
Ethereum RPC call routed through the mixnet. The shield lands on chain (you can
|
||||
open it on Etherscan), but the IP that submitted it is the Nym exit's, not yours.
|
||||
|
||||
The entire integration is a single ethers shim (shown below). Because the
|
||||
Railgun engine talks to the chain through ethers, routing ethers through
|
||||
`mixFetch` is enough to put a whole privacy SDK behind the mixnet. The same
|
||||
pattern drops into any ethers-based app or library.
|
||||
|
||||
## How it works
|
||||
|
||||
The [ENS demo](/developers/demos/ens) swapped one provider's transport. Railgun
|
||||
constructs its own providers internally, so routing only our provider would leak
|
||||
the engine's RPC to clearnet. Instead this demo installs a **global** ethers
|
||||
transport: `FetchRequest.registerGetUrl` routes every ethers HTTP call in the
|
||||
page through `mixFetch`, including the ones the Railgun engine makes.
|
||||
|
||||
```ts
|
||||
|
||||
// Every ethers HTTP request in the process now goes through the mixnet.
|
||||
FetchRequest.registerGetUrl(async (req) => {
|
||||
const res = await mixFetch(req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body ?? undefined,
|
||||
});
|
||||
return {
|
||||
statusCode: res.status,
|
||||
statusMessage: res.statusText,
|
||||
headers: Object.fromEntries(res.headers),
|
||||
body: new Uint8Array(await res.arrayBuffer()),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
`registerGetUrl` is global static state on the `FetchRequest` class, so this only
|
||||
works if ethers is a **single instance** across your bundle. If your app and
|
||||
Railgun resolve to different ethers copies, the handler installs on one and the
|
||||
engine uses the other. Pin the exact ethers version Railgun peer-depends on (this
|
||||
demo aliases ethers to one instance in the bundler).
|
||||
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch), [`@railgun-community/wallet`](https://www.npmjs.com/package/@railgun-community/wallet), and [`ethers`](https://www.npmjs.com/package/ethers).
|
||||
|
||||
Shielding is a four-step flow, all over the mixnet: sign a shield key, estimate
|
||||
gas, populate the transaction, then sign and broadcast. The broadcast that lands
|
||||
on Sepolia is observable on Etherscan, but the IP that submitted it stays hidden.
|
||||
|
||||
## Try it
|
||||
|
||||
The demo auto-loads a funded Sepolia testnet wallet. Connect the tunnel (the
|
||||
Railgun address derives once the engine is up), check the balance, then shield a
|
||||
small amount. If the wallet is low, top it up at a
|
||||
[Sepolia faucet](https://sepoliafaucet.com/) using the public address shown.
|
||||
|
||||
**Sepolia testnet only.** The wallet holds only test ETH and the mnemonic is
|
||||
stored in plain browser storage. Never paste a mainnet mnemonic.
|
||||
|
||||
## What to expect
|
||||
|
||||
- **Engine init is the slow part.** `loadProvider` hits Sepolia over a cold
|
||||
mixnet route, which can exceed Railgun's internal timeout on the first try; the
|
||||
demo retries and the second attempt finds the connection pool warm.
|
||||
- **Shielding makes several RPC calls** (gas estimate, fee data, broadcast,
|
||||
receipt), each a mixnet round trip. The broadcast step retries idempotently:
|
||||
the tx hash is fixed before broadcasting, so a dropped response can be re-sent
|
||||
or detected as already-on-chain.
|
||||
- **Rate limiting.** If RPC calls start failing with 403/429 or connection
|
||||
errors, the exit IP is flagged: disconnect, tick **Use random IPR**, reload,
|
||||
and reconnect for a fresh exit.
|
||||
|
||||
## Glossary
|
||||
|
||||
---
|
||||
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.
|
||||
@@ -4089,7 +4276,7 @@ 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.
|
||||
- **IPR exit policies apply.** What was allowed by your previous Network Requester may not be allowed by your default IPR, which applies the current [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt).
|
||||
|
||||
---
|
||||
title: mix-dns: Hostname Resolution Over the Nym Mixnet
|
||||
@@ -10096,7 +10283,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
|
||||
@@ -10748,7 +10935,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
|
||||
|
||||
@@ -20358,9 +20545,109 @@ ansible-playbook deploy.yml
|
||||
|
||||
###### 2. Bond
|
||||
|
||||
Anyone having acces to your account mnemonic can take all your funds and manage manage your node, be careful where you store it!
|
||||
|
||||
Bonding can be managed via two playbooks:
|
||||
|
||||
1. `bond.yml`: an interactive way, requiring operator to use own wallet (desktop or CLI)
|
||||
2. `auto-bond.yml`: automatic bonding flow requiring operator to prepare `nodes.csv` and have `nym-cli` installed
|
||||
|
||||
<Tabs items={[
|
||||
<code>bond.yml</code>,
|
||||
<code>auto-bond.yml</code>,
|
||||
]} defaultIndex="1">
|
||||
|
||||
A playbook to *interactively* register your nodes to Nym network by bonding it to Nyx blockchain accounts.
|
||||
This playbook is intercative as it prompts user for data from Nym wallet to sign a message. It will run roles on one inventory entry at a time by default.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- Nym Wallet or `nym-cli` to be used as a CLI wallet
|
||||
- An account per each node
|
||||
- At least 101 NYM per account
|
||||
|
||||
**Usage**
|
||||
|
||||
1. Sign in to the wallet per node
|
||||
2. Follow steps in `Bond` section
|
||||
3. Run the playbook on a side and follow the prompts
|
||||
|
||||
```sh
|
||||
cd playbooks
|
||||
ansible-playbook bond.yml
|
||||
```
|
||||
|
||||
Your nodes are bonded and will show in the network in the next epoch (max 60min).
|
||||
|
||||
A playbook to *automatically* register your nodes to Nym Network by bonding it to Nyx blockchain accounts.
|
||||
This automatic flow is slightly harder to setup in the beginning and it's recommended for operators bonding many nodes, as the initial work is worth it by saving the time of bonding a node at a time.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- Installed [`nym-cli`](/developers/tools/nym-cli)
|
||||
- Python3
|
||||
- Nym repository with directory `scripts/nym-node-setup/auto-bond/`, containing:
|
||||
- Python program [`auto_bond_all.py`](https://github.com/nymtech/nym/tree/develop/scripts/nym-node-setup/auto-bond/auto_bond_all.py)
|
||||
- `nodes.csv.example` with correct data
|
||||
|
||||
**Usage**
|
||||
|
||||
1. Copy `nodes.csv.example` to your prefered location without `.example` suffix
|
||||
2. Fill correctly the csv columns for each node you want to bond:
|
||||
- `inventory_node_id`: Your Ansible inventory node ID (ie `node1`)
|
||||
- `hostname`: same like in your `playbooks/inventory/all` (without `https://` !)
|
||||
- `ip`: same like `ansible_host` value in your Ansible inventory
|
||||
- `account`: Nyx account to bond this node with
|
||||
- `mnemonic`: Your nyx acount mnemonic
|
||||
- `identity_key`: node identity key - the easiest way to get it is to navigate to `playbooks/` and run:
|
||||
|
||||
```sh
|
||||
ansible all -i inventory/all -a "/root/nym-binaries/nym-node bonding-information"
|
||||
```
|
||||
|
||||
- `amount`: Amount to bond in `uNYM` (1 NYM = 1 000 000 uNYM), Make sure to leave extra 1 NYM (1 000 000 uNYM) for fees
|
||||
- `operator_cost`: [Operator cost](/operators/tokenomics/mixnet-rewards#rewards-distribution) in `uNYM` (1 NYM = 1 000 000 uNYM)
|
||||
|
||||
3. Save the csv
|
||||
4. Run `auto_bond_all.py` with all needed arguments.
|
||||
|
||||
- To see help menu:
|
||||
|
||||
```sh
|
||||
python3 ./auto_bond_all.py --help
|
||||
```
|
||||
|
||||
- To test your paths run with `--dry-run`
|
||||
|
||||
- Argument usage:
|
||||
|
||||
```sh
|
||||
--ansible-repo ANSIBLE_REPO
|
||||
Path to ansible playbooks directory (contains auto-bond.yml and inventory/)
|
||||
--cli-dir CLI_DIR Directory containing the nym-cli binary
|
||||
--dry-run Print commands without executing
|
||||
```
|
||||
|
||||
- Example (note that the `nodes.csv` has no flag as it's a required argument):
|
||||
|
||||
```sh
|
||||
python ./auto_bond_all.py \
|
||||
--ansible-repo ~/admin/nym-nodes/nym-nodes-ansible/playbooks \
|
||||
--cli-dir ~/repos/nymtech/nym/target/release \
|
||||
~/admin/nym-nodes/nodes.csv
|
||||
```
|
||||
|
||||
5. Your nodes should be bonded and come up in the next epoch (max 60min)
|
||||
|
||||
**Additional scripts**
|
||||
|
||||
Your `nodes.csv` can be used for other operations:
|
||||
- [`show_balances.py`](https://github.com/nymtech/nym/tree/develop/scripts/nym-node-setup/auto-bond/auto_bond_all.py): Shows all accounts balances if provided with Nyx accounts (`account` column)
|
||||
- [`unbond_all.py`](https://github.com/nymtech/nym/tree/develop/scripts/nym-node-setup/auto-bond/unbond_all.py): Unbond all nodes in the csv if provided with mnemonics (`mnemonic` column)
|
||||
|
||||
A playbook to interactively register your node to Nym network by bonding it to Nyx blockchain account.
|
||||
|
||||
This playbook is intercative as it prompts user for data from Nym wallet to sign a message. It will run roles on one inventory entry at a time by default.
|
||||
This playbook is interactive as it prompts user for data from Nym wallet to sign a message. It will run roles on one inventory entry at a time by default.
|
||||
|
||||
```sh
|
||||
cd playbooks
|
||||
@@ -20463,6 +20750,20 @@ ansible-playbook <PLAYBOOK>.yml --list-tags
|
||||
ansible-playbook deploy.yml --list-tags
|
||||
```
|
||||
|
||||
###### Arbitrary command output
|
||||
|
||||
You can use ansible to read a `STDOUT` from any command, using this logic:
|
||||
```sh
|
||||
ansible all -i inventory/all -a "<COMMAND>>"
|
||||
|
||||
# for example to get all node ID keys
|
||||
ansible all -i inventory/all -a "/root/nym-binaries/nym-node bonding-information"
|
||||
```
|
||||
|
||||
- Note that the command gets also run, be mindful what you executing
|
||||
|
||||
- This logic can be combined with the arguments above, for example to limit the range of nodes
|
||||
|
||||
###### nocows
|
||||
|
||||
Yes, by default there is a cow printed under each task, you can turn it off by opening `playbooks/ansible.cfg` and un-commenting the `nocows` line:
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- **Developers can only access Mixnet mode** via the SDKs. dVPN mode is exclusively provided by the NymVPN product, which can be downloaded and purchased at [nym.com](https://nym.com). There is no SDK for dVPN mode.
|
||||
- The primary developer SDK is the **Rust SDK** (`nym-sdk` crate). The TypeScript SDK wraps the Rust SDK via WebAssembly.
|
||||
- For Rust integrations, prefer the **mixnet** module for simple send/receive, **stream** for TCP-like `AsyncRead`/`AsyncWrite` connections, and **client_pool** for bursty workloads. The **tcp_proxy** module is unmaintained; recommend stream instead.
|
||||
- For browser/TypeScript proxy integrations, the **mix-*** packages share one tunnel via **`@nymproject/mix-tunnel`**: **`mix-fetch`** (HTTP/S), **`mix-dns`** (DNS), **`mix-websocket`** (WS/WSS). For raw end-to-end messaging where you control both ends, use the **TypeScript SDK** (`@nymproject/sdk`).
|
||||
- Node operators should use the **`nym-node`** binary. Legacy `nym-mixnode` and `nym-gateway` binaries are deprecated and no longer supported.
|
||||
- When referring to token collateral for nodes, use **"bonding"** not "staking". Nodes operate in **modes** (mixnode, entry gateway, exit gateway), not "roles".
|
||||
- The Nym blockchain is called **Nyx** and runs on Cosmos SDK. The native token is **NYM**.
|
||||
@@ -23,7 +24,7 @@
|
||||
|
||||
## Developers
|
||||
|
||||
- [Integration Overview](https://nym.com/docs/developers/integrations): Start here for building on Nym: choosing an SDK, integration patterns
|
||||
- [Developer Overview](https://nym.com/docs/developers): Start here for building on Nym: runtime (native vs browser) and approach (end-to-end vs proxy) map onto the right crate or package
|
||||
- [Rust SDK Overview](https://nym.com/docs/developers/rust): Module overview and quick-start for the Rust SDK
|
||||
- [Rust SDK Installation](https://nym.com/docs/developers/rust/importing): Adding nym-sdk to your Cargo.toml
|
||||
- [Mixnet Module](https://nym.com/docs/developers/rust/mixnet): Send and receive Sphinx-encrypted messages
|
||||
@@ -35,8 +36,15 @@
|
||||
- [Client Pool Tutorial](https://nym.com/docs/developers/rust/client-pool/tutorial): Handle bursty traffic with pooled clients
|
||||
- [TcpProxy Module](https://nym.com/docs/developers/rust/tcpproxy): Tunnel existing TCP services through the mixnet (unmaintained; use stream instead)
|
||||
- [smolmix](https://nym.com/docs/developers/smolmix): TCP/UDP over the mixnet via a userspace IP stack, drop-in TcpStream and UdpSocket (see examples in source)
|
||||
- [TypeScript SDK](https://nym.com/docs/developers/typescript): Browser and Node.js SDK using WebAssembly
|
||||
- [mix-fetch](https://nym.com/docs/developers/typescript/examples/mix-fetch): Drop-in fetch() replacement that routes HTTP through the mixnet
|
||||
- [TypeScript SDK](https://nym.com/docs/developers/typescript): Browser-side raw end-to-end messaging (@nymproject/sdk) and Nyx smart-contract bindings
|
||||
- [mix-tunnel](https://nym.com/docs/developers/mix-tunnel): Shared browser mixnet tunnel; the base layer for mix-fetch, mix-dns, and mix-websocket
|
||||
- [mix-fetch](https://nym.com/docs/developers/mix-fetch): Drop-in fetch() replacement routing HTTP(S) through the mixnet via an IPR exit
|
||||
- [mix-dns](https://nym.com/docs/developers/mix-dns): Hostname-to-IP resolution through the mixnet (UDP DNS via an IPR exit)
|
||||
- [mix-websocket](https://nym.com/docs/developers/mix-websocket): WebSocket-like class for WS and WSS over the mixnet
|
||||
- [mix-* Architecture](https://nym.com/docs/developers/mix-architecture): How the shared browser tunnel, Web Worker, Comlink boundary, and smoltcp+rustls stack are wired
|
||||
- [Playground](https://nym.com/docs/developers/playground): Interactive browser playground driving the mix-* packages and the raw messaging SDK against the live mixnet
|
||||
- [ENS Demo](https://nym.com/docs/developers/demos/ens): Resolve ENS names and fetch IPFS sites over the mixnet via an ethers-to-mixFetch shim
|
||||
- [Railgun Demo](https://nym.com/docs/developers/demos/railgun): Shield testnet ETH into a Railgun private note with every Ethereum RPC routed through the mixnet
|
||||
|
||||
## Operators
|
||||
|
||||
|
||||
Reference in New Issue
Block a user