npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kusamashield/mixnet-rpc

v0.1.0

Published

Nym and Tor privacy clients that wrap EVM JSON-RPC (ethers) and Polkadot/Substrate API calls for Kusama Shield.

Readme

@kusamashield/mixnet-rpc

Nym and Tor privacy clients that wrap EVM JSON-RPC (ethers v6) and Polkadot/Substrate API calls. Framework-agnostic, browser-first, extracted from the Kusama Shield interface.

  • Nym — HTTP(S) through mixFetch and WebSockets through the smolmix mixnet tunnel. Hides your IP from RPC providers and can tunnel Polkadot WSS.
  • Tor — HTTP(S) through the webtor-rs WASM client (Snowflake bridge). Tor cannot tunnel WebSockets.
  • Wrappers — privacy-aware fetch, an ethers JsonRpcProvider, and a global WebSocket shim so @polkadot/api connects over Nym.

Browser only. Nym is WASM + a Web Worker and Tor is WASM; neither runs under Node. This package is ESM-only.

Install

npm install @kusamashield/mixnet-rpc

The Nym SDK (@nymproject/mix-fetch, @nymproject/mix-tunnel) is a regular dependency. The Tor WASM (webtor-rs) is vendored, so no extra download.

Integration wrappers need optional peer dependencies:

npm install ethers                 # for @kusamashield/mixnet-rpc/ethers
npm install @polkadot/api @polkadot/rpc-provider   # for .../polkadot

Quick start

import {
  enableNym,
  enableTor,
  disablePrivacy,
  privacyFetch,
  onPrivacyChange,
} from "@kusamashield/mixnet-rpc";

onPrivacyChange((s) => console.log(s.mode, s.status, s.message));

await enableNym();                 // or enableTor()
const res = await privacyFetch("https://eth-rpc.polkadot.io", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_chainId", params: [] }),
});
console.log(await res.json());

await disablePrivacy();

privacyFetch behaves exactly like fetch: it routes JSON-RPC payloads (and URLs matching the configured RPC host hints) through the active transport, and falls back to the native fetch when no privacy mode is active or the request is not proxyable.

Cookbook: read a balance and the latest block

The two recipes below are self-contained. They work with any transport (off, tor, nym); just call the enable* function you want, or skip it to go direct. The full, runnable versions live in test/browser-tests.ts.

With an EVM / ETH JSON-RPC endpoint (ethers v6)

import { enableNym, disablePrivacy, rpcTimeoutMs } from "@kusamashield/mixnet-rpc";
import { createPrivacyProvider } from "@kusamashield/mixnet-rpc/ethers";

// Optional: route the EVM RPC through Tor or the Nym mixnet.
await enableNym(); // or enableTor(), or omit to go direct

const provider = createPrivacyProvider("https://eth-rpc.polkadot.io");
// pass a network id/object to skip auto-detection: createPrivacyProvider(url, 420420419)

// Latest block number.
const latestBlock = await provider.getBlockNumber();

// Native balance (wei) for any address.
const balanceWei = await provider.getBalance("0x0D694Da746e73D1e255c1894F90e38170db45809");
console.log("latest block:", latestBlock, "balance:", balanceWei.toString());

await provider.destroy();
await disablePrivacy();

createPrivacyProvider(url, network?, options?) accepts the same arguments as ethers' JsonRpcProvider. Because it installs a privacy-aware getUrlFunc on the provider's FetchRequest, every JSON-RPC call (getBlockNumber, getBalance, call, estimateGas, ...) is routed through the active transport. Set createPrivacyProvider(url, undefined, { staticNetwork: true }) to avoid the initial network-detection round trips.

With Polkadot / Substrate (@polkadot/api)

Import the /polkadot entry before @polkadot/api so @polkadot/x-ws captures the WebSocket shim (see the section below for why):

import "@kusamashield/mixnet-rpc/polkadot";
import { ApiPromise } from "@polkadot/api";
import { createPolkadotProvider } from "@kusamashield/mixnet-rpc/polkadot";
import { enableNym, disablePrivacy, rpcTimeoutMs } from "@kusamashield/mixnet-rpc";

// Polkadot WSS can only be tunnelled by Nym (Tor has no WebSocket support).
await enableNym();

const provider = await createPolkadotProvider(
  "wss://polkadot-asset-hub-rpc.polkadot.io",
  { timeout: rpcTimeoutMs(30_000) }, // widened while Nym is active
);

const api = await ApiPromise.create({ provider, noInitWarn: true });

// Latest block.
const header = await api.rpc.chain.getHeader();
console.log("chain:", (await api.rpc.system.chain()).toString(), "block:", header.number.toString());

// Native balance (planck) via system.account.
const account = await api.query.system.account(
  "5Dxr9EoL7ChvxPJAsQ8gZe1Dfbwh9AEeDmB3KpbCrihLU85u",
);
// At runtime `account.data.free` is a BN; cast for TypeScript.
const free = (account as any).data.free.toString();
console.log("free:", free);

await api.disconnect();
await disablePrivacy();

Or use the one-shot helper that builds both the provider and the API:

import { createPolkadotApi } from "@kusamashield/mixnet-rpc/polkadot";
const api = await createPolkadotApi("wss://polkadot-asset-hub-rpc.polkadot.io");

Running in a browser: @polkadot/api expects a global Buffer. Add a polyfill before importing it (the test page does this in test/polyfills.ts):

import { Buffer } from "buffer";
(globalThis as any).Buffer ??= Buffer;

ethers v6

import { enableNym } from "@kusamashield/mixnet-rpc";
import { createPrivacyProvider } from "@kusamashield/mixnet-rpc/ethers";

await enableNym();

const provider = createPrivacyProvider("https://eth-rpc.polkadot.io", 420420419);
const block = await provider.getBlockNumber();

The provider installs a custom getUrlFunc on its FetchRequest, so every JSON-RPC round trip goes through the active transport. You can also get the raw pieces:

import { createEthersFetchRequest, privacyGetUrl } from "@kusamashield/mixnet-rpc/ethers";

Polkadot / Substrate

Import the /polkadot entry before @polkadot/api, so that @polkadot/x-ws captures the shim (it reads globalThis.WebSocket at module-eval time):

import "@kusamashield/mixnet-rpc/polkadot";   // installs the WebSocket shim
import { ApiPromise, WsProvider } from "@polkadot/api";
import { enableNym } from "@kusamashield/mixnet-rpc";

await enableNym();

// WSS to known Polkadot RPC hosts is now tunnelled through Nym.
const api = await ApiPromise.create({
  provider: new WsProvider("wss://kusama-asset-hub-rpc.polkadot.io"),
});

Or use the helpers, which dynamic-import the provider/API:

import {
  createPolkadotProvider,
  createPolkadotApi,
  isPolkadotUsingShim,
} from "@kusamashield/mixnet-rpc/polkadot";

const provider = await createPolkadotProvider("wss://kusama-asset-hub-rpc.polkadot.io");
const api = await createPolkadotApi("wss://kusama-asset-hub-rpc.polkadot.io");

console.log(await isPolkadotUsingShim()); // true when import order is correct

While Nym is active, use rpcTimeoutMs(base) to widen timeouts: Substrate metadata is ~1.3 MB and can take 30-60s+ over the mixnet.

import { rpcTimeoutMs } from "@kusamashield/mixnet-rpc";
const timeout = rpcTimeoutMs(15_000); // 180_000 while Nym is active

Global fetch interceptor (optional)

To transparently proxy third-party libraries that call fetch directly:

import { enableNym, installFetchInterceptor } from "@kusamashield/mixnet-rpc";

installFetchInterceptor();
await enableNym();

Call uninstallFetchInterceptor() to restore the original fetch.

Configuration

import { configureMixnet, getMixnetConfig, resetMixnetConfig } from "@kusamashield/mixnet-rpc";

configureMixnet({
  rpcHostHints: ["my-rpc.example.com"],   // replaces defaults
  wsHostHints: ["my-ws.example.com"],
  excludeUrlPatterns: ["/api/"],          // never proxied
  nymRpcTimeoutMs: 180_000,
});

// Append instead of replacing:
configureMixnet({ rpcHostHints: ["another.example.com"] }, { append: true });

Tor options

import { enableTor } from "@kusamashield/mixnet-rpc";

await enableTor({
  snowflakeUrl: "wss://snowflake.torproject.net/",
  connectionTimeoutMs: 60_000,
  circuitTimeoutMs: 120_000,
  // Bundlers that mangle the vendored asset can point at their own copy:
  // wasmUrl: "/webtor-proxy/webtor_wasm.js",
});

The vendored WASM lives at @kusamashield/mixnet-rpc/wasm/webtor-proxy/webtor_wasm.js (and webtor_wasm_bg.wasm). It is loaded via new URL("../wasm/webtor-proxy/webtor_wasm.js", import.meta.url) by default; pass wasmUrl if your bundler does not preserve the package layout.

API

Mode control

| Export | Description | |--------|-------------| | enableNym(opts?) | Connect the Nym mixnet (disconnects Tor first). | | enableTor(opts?) | Connect Tor via Snowflake (disconnects Nym first). | | disablePrivacy(opts?) | Tear down transports and go direct. |

Clients

| Export | Description | |--------|-------------| | connectNym(opts?) / disconnectNym(opts?) / isNymConnected() | Nym lifecycle. | | nymFetch(url, init?) | Fetch through Nym. | | openNymWebSocket(url, protocols, handlers) | Raw Nym WebSocket handle. | | connectTor(opts?) / disconnectTor() / isTorConnected() | Tor lifecycle. | | torRequest(url, init?) | Fetch through Tor. |

State

| Export | Description | |--------|-------------| | getPrivacyState() | { mode, status, message } snapshot. | | onPrivacyChange(cb) | Subscribe to state changes. | | isPrivacyActive() / isNymActive() / isTorActive() | Status predicates. | | rpcTimeoutMs(base) | Nym-aware timeout. |

Wrappers

| Export | Entry | Description | |--------|-------|-------------| | privacyFetch | . | Privacy-aware fetch. | | installFetchInterceptor / uninstallFetchInterceptor | . | Global fetch override. | | NymAwareWebSocket | . / ./polkadot | WebSocket-compatible class. | | installWebSocketShim / uninstallWebSocketShim | . / ./polkadot | Global WebSocket override. | | createEthersFetchRequest / createPrivacyProvider / privacyGetUrl | ./ethers | ethers v6 integration. | | createPolkadotProvider / createPolkadotApi / isPolkadotUsingShim | ./polkadot | Polkadot integration. |

Testing

Two suites are included.

Unit tests (Node, fast)

Pure logic — config, state machine, request routing/helpers, WebSocket shim:

npm test          # or: npm run test:unit

Browser integration tests (real RPCs)

The browser test queries both an EVM JSON-RPC endpoint (balance + latest block via ethers) and a Polkadot endpoint (balance + latest block via @polkadot/api), and can route either through a privacy transport. Nym and Tor are WASM + Web Worker, so this runs in headless Chromium via a small CDP runner:

npm run test:browser              # mode=off (direct; reliable, default)
npm run test:browser:nym          # route through the Nym mixnet
npm run test:browser:tor          # route through Tor (HTTP JSON-RPC only)

Override endpoints/addresses with flags (or use --flag=value):

node scripts/run-browser-tests.mjs \
  --mode=off \
  --eth-rpc=https://eth-rpc-kusama.polkadot.io \
  --eth-address=0x... \
  --ws=wss://kusama-asset-hub-rpc.polkadot.io \
  --ss58=5...

| Flag | Default | |------|---------| | --mode=off\|nym\|tor | off | | --eth-rpc | https://eth-rpc.polkadot.io | | --eth-address | the Polkadot AssetHub pool | | --ws | wss://polkadot-asset-hub-rpc.polkadot.io | | --ss58 | a Kusama test account | | --timeout | 360000 ms |

It needs the chromium binary (CHROMIUM_BIN to override) and network access. The mode=off run is deterministic and is what you should use in CI; the nym/tor runs depend on external services and may fail intermittently (see the notes below). Example result:

{
  "mode": "off",
  "usingShim": true,
  "eth": { "blockNumber": 21029611, "balanceWei": "27670000003000000000" },
  "polkadot": { "chain": "Polkadot Asset Hub", "block": "21029613", "freePlanck": "53703807456" }
}

Notes and caveats

  • Nym tunnel is a singleton. After disconnectMixTunnel() the WASM is unusable until a full page reload. disconnectNym() only marks the transport inactive so it can be re-enabled; pass { teardown: true } to actually tear it down.
  • Import order matters for Polkadot. @polkadot/x-ws captures globalThis.WebSocket when it is first evaluated. Import @kusamashield/mixnet-rpc/polkadot before any @polkadot/api import. isPolkadotUsingShim() verifies this.
  • Tor cannot tunnel WebSockets — only HTTP(S) JSON-RPC.
  • Tor consensus (upstream): webtor-rs loads a cached Tor consensus from https://privacy-ethereum.github.io/webtor-rs/consensus.txt.br. As of 2026-09-24 those files return 404, so connectTor() fails with Failed to load cached consensus: HTTP error: 404. This is upstream in webtor-rs, not in this package; bump the vendored webtor-wasm (or self-host the consensus) when a fixed build is published.
  • Nym connectivity is flaky per run (gateway/IPR selection). HTTP mixFetch may time out or return an empty body, and Nym WSS may close with 1006. Retries, or a fresh browser profile / clientId, usually help.
  • First Polkadot connect over Nym downloads ~1.3 MB of runtime metadata and can take 30-60s+. Cache or prefetch it in your app and widen timeouts with rpcTimeoutMs.
  • WASM needs the right MIME type and, for some setups, cross-origin isolation. The interface disables COOP/COEP for wallet SDK compatibility; the Nym worker does not require SharedArrayBuffer.

License

MIT. Vendors webtor-rs (MIT) and depends on the Nym mixnet SDK (Apache-2.0).