Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- fixed: Revert `@nymproject/mix-fetch` to v1 (1.4.4), restoring the pinned gateway and network requester. The v2 stack shipped in 2.47.0 fails to complete small HTTPS JSON-RPC requests through most exit nodes and its exit-node auto-discovery rarely converges, which left wallets with NYM privacy enabled unable to sync or send.
- fixed: Bound the NYM mixFetch setup at 60 seconds, so a dead gateway surfaces an error instead of blocking the first mixnet request indefinitely.

## 2.47.0 (2026-07-11)

- changed: Upgrade `@nymproject/mix-fetch` to v2, which routes NYM mixnet traffic through the new smolmix-wasm tunnel (`@nymproject/mix-tunnel`). The v2 wasm + worker are inlined into the bundle, so the build no longer copies sibling `.wasm`/`web-worker-*.js` assets.
Expand Down
18 changes: 4 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"*.{js,jsx,ts,tsx}": "eslint"
},
"dependencies": {
"@nymproject/mix-fetch": "^2.0.0",
"@nymproject/mix-fetch": "^1.4.4",
"aes-js": "^3.1.0",
"base-x": "^4.0.1",
"biggystring": "^4.2.3",
Expand Down
11 changes: 9 additions & 2 deletions src/io/browser/browser-io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { makeLocalStorageDisklet } from 'disklet'
import { LogBackend, makeLog } from '../../core/log/log'
import { EdgeFetchOptions, EdgeFetchResponse, EdgeIo } from '../../types/types'
import { scrypt } from '../../util/crypto/scrypt'
import { initMixFetch } from '../../util/nym'
import { initMixFetch, mixFetchOptions } from '../../util/nym'
import { fetchCorsProxy } from './fetch-cors-proxy'

// Only try CORS proxy/bridge techniques up to 5 times
Expand Down Expand Up @@ -50,7 +50,14 @@ export function makeBrowserIo(logBackend: LogBackend): EdgeIo {

if (privacy === 'nym') {
const nymFetch = await initMixFetch(log)
return await nymFetch(uri, opts)
return await nymFetch(
uri,
{
...opts,
mode: 'unsafe-ignore-cors' as RequestMode
},
mixFetchOptions
)
}
if (corsBypass === 'always') {
return await fetchCorsProxy(uri, opts)
Expand Down
12 changes: 10 additions & 2 deletions src/io/react-native/react-native-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
EdgeFetchResponse,
EdgeIo
} from '../../types/types'
import { initMixFetch } from '../../util/nym'
import { initMixFetch, mixFetchOptions } from '../../util/nym'
import { hideProperties } from '../hidden-properties'
import { makeNativeBridge } from './native-bridge'
import { WorkerApi, YAOB_THROTTLE_MS } from './react-native-types'
Expand Down Expand Up @@ -177,7 +177,15 @@ async function makeIo(logBackend: LogBackend): Promise<EdgeIo> {

if (privacy === 'nym') {
const nymFetch = await initMixFetch(log)
return await nymFetch(uri, opts)
const response = await nymFetch(
uri,
{
...opts,
mode: 'unsafe-ignore-cors' as RequestMode
},
mixFetchOptions
)
return response
}
if (corsBypass === 'always') {
return await nativeFetch(uri, opts)
Expand Down
74 changes: 53 additions & 21 deletions src/util/nym.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,85 @@
import {
createMixFetch,
disconnectMixTunnel,
SetupMixTunnelOpts
disconnectMixFetch,
IMixFetch,
IMixFetchFn,
SetupMixFetchOps
} from '@nymproject/mix-fetch'

import { EdgeLog } from '../types/types'

/** The fetch-bound function `createMixFetch` resolves to. */
type MixFetchFn = (url: string, init?: RequestInit) => Promise<Response>

/**
* Configuration options for the NYM mixFetch tunnel.
* Configuration options for the NYM mixFetch client.
*/
export const mixFetchOptions: SetupMixTunnelOpts = {
export const mixFetchOptions: SetupMixFetchOps = {
clientId: 'edge-core-js-2026-03-10',
preferredGateway: '5rXcNe2a44vXisK3uqLHCzpzvEwcnsijDMU7hg4fcYk8', // with WSS
preferredNetworkRequester:
'5x6q9UfVHs5AohKMUqeivj7a556kVVy7QwoKige8xHxh.6CFoB3kJaDbYz6oafPJxNxNjzahpT2NtgtytcSyN9EvF@5rXcNe2a44vXisK3uqLHCzpzvEwcnsijDMU7hg4fcYk8',
forceTls: true, // force WSS
// Mixnet round trips are slow, so give the tunnel handshake plenty of time.
// v1 tuned a 5 min `requestTimeoutMs`; v2 exposes no per-request timeout, but
// the tunnel setup is where mixnet latency bites, so restore that 5 min
// budget here to avoid premature failures during the handshake.
connectTimeoutMs: 300000
mixFetchOverride: {
requestTimeoutMs: 300000
}
}

/**
* Budget for `createMixFetch` itself (client start + gateway handshake).
*
* A healthy setup with the pinned gateway completes in under 10s measured.
* Without a bound here the whole app blocks on the first mixnet request for
* as long as a dead gateway keeps us waiting, which reads to the user as a
* freeze.
*/
const SETUP_TIMEOUT_MS = 60000

// MixFetch initialization state
let mixFetchInitPromise: Promise<MixFetchFn> | null = null
let mixFetchInitPromise: Promise<IMixFetch> | null = null

/**
* Initialize the NYM mixFetch client. Must be called before using mixFetch.
* Safe to call multiple times - subsequent calls return the same promise.
*/
export async function initMixFetch(log: EdgeLog): Promise<MixFetchFn> {
export async function initMixFetch(log: EdgeLog): Promise<IMixFetchFn> {
if (mixFetchInitPromise == null) {
log('Initializing mixFetch...')
mixFetchInitPromise = createMixFetch(mixFetchOptions)
.then(mixFetch => {
const pending = createMixFetch(mixFetchOptions)
// The timeout below can abandon this setup while it is still in flight.
// Deliberately do NOT tear it down on late completion: `createMixFetch`
// resolves to a healthy global singleton, and disconnecting it (a
// process-wide operation) would race a newer init that has taken over.
// A late completion just repopulates `__mixFetchGlobal`, which the next
// init reuses. Swallow a late rejection so it is not unhandled.
pending.catch(() => {})
let timer: ReturnType<typeof setTimeout> | undefined
const timeout = new Promise<never>((resolve, reject) => {
timer = setTimeout(() => {
reject(
new Error(`mixFetch setup timed out after ${SETUP_TIMEOUT_MS}ms`)
)
}, SETUP_TIMEOUT_MS)
})
mixFetchInitPromise = Promise.race([pending, timeout])
.then(mixFetchModule => {
log('mixFetch initialized successfully')
return mixFetch
return mixFetchModule
})
.catch(async error => {
// Tear down any partially-established tunnel left by the failed init
// so the next createMixFetch call starts fresh instead of reusing a
// Clean up stale global state left by the failed init so the
// next createMixFetch call starts fresh instead of reusing a
// broken singleton.
try {
await disconnectMixTunnel()
await disconnectMixFetch()
} catch {}
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (window as any).__mixFetchGlobal
mixFetchInitPromise = null
log.error('mixFetch initialization failed:', error)
throw error
})
.finally(() => {
clearTimeout(timer)
})
}
return await mixFetchInitPromise
const mixFetchModule = await mixFetchInitPromise
return mixFetchModule.mixFetch
}
24 changes: 24 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ module.exports = {
static: bundlePath
},
entry: './src/io/react-native/react-native-worker.ts',
experiments: {
asyncWebAssembly: true
},
mode: debug ? 'development' : 'production',
module: {
rules: [
Expand All @@ -72,6 +75,10 @@ module.exports = {
loader: 'babel-loader',
options: { presets: ['@babel/preset-env'] }
}
},
{
test: /\.wasm$/,
type: 'webassembly/async'
}
]
},
Expand All @@ -85,12 +92,29 @@ module.exports = {
plugins: [
new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'] }),
new webpack.ProvidePlugin({ process: ['process'] }),
// Copy static files and mix-fetch WASM/worker files
new CopyPlugin({
patterns: [
// HTML entry point
{
from: path.resolve(__dirname, 'src/index.html'),
to: 'index.html'
},
// mix-fetch WASM files for NYM mixnet support
{
from: path.resolve(
__dirname,
'node_modules/@nymproject/mix-fetch/*.wasm'
),
to: '[name][ext]'
},
// mix-fetch web worker files
{
from: path.resolve(
__dirname,
'node_modules/@nymproject/mix-fetch/web-worker-*.js'
),
to: '[name][ext]'
}
]
})
Expand Down
Loading