Skip to content

Commit 560f7a8

Browse files
authored
Merge pull request #732 from EdgeApp/jon/revert-nym-mixfetch-v2
Revert NYM mix-fetch to v1 (1.4.4)
2 parents 1b2a25e + a323e15 commit 560f7a8

7 files changed

Lines changed: 104 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- 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.
6+
- fixed: Bound the NYM mixFetch setup at 60 seconds, so a dead gateway surfaces an error instead of blocking the first mixnet request indefinitely.
7+
58
## 2.47.0 (2026-07-11)
69

710
- 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.

package-lock.json

Lines changed: 4 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"*.{js,jsx,ts,tsx}": "eslint"
7979
},
8080
"dependencies": {
81-
"@nymproject/mix-fetch": "^2.0.0",
81+
"@nymproject/mix-fetch": "^1.4.4",
8282
"aes-js": "^3.1.0",
8383
"base-x": "^4.0.1",
8484
"biggystring": "^4.2.3",

src/io/browser/browser-io.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { makeLocalStorageDisklet } from 'disklet'
33
import { LogBackend, makeLog } from '../../core/log/log'
44
import { EdgeFetchOptions, EdgeFetchResponse, EdgeIo } from '../../types/types'
55
import { scrypt } from '../../util/crypto/scrypt'
6-
import { initMixFetch } from '../../util/nym'
6+
import { initMixFetch, mixFetchOptions } from '../../util/nym'
77
import { fetchCorsProxy } from './fetch-cors-proxy'
88

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

5151
if (privacy === 'nym') {
5252
const nymFetch = await initMixFetch(log)
53-
return await nymFetch(uri, opts)
53+
return await nymFetch(
54+
uri,
55+
{
56+
...opts,
57+
mode: 'unsafe-ignore-cors' as RequestMode
58+
},
59+
mixFetchOptions
60+
)
5461
}
5562
if (corsBypass === 'always') {
5663
return await fetchCorsProxy(uri, opts)

src/io/react-native/react-native-worker.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
EdgeFetchResponse,
1818
EdgeIo
1919
} from '../../types/types'
20-
import { initMixFetch } from '../../util/nym'
20+
import { initMixFetch, mixFetchOptions } from '../../util/nym'
2121
import { hideProperties } from '../hidden-properties'
2222
import { makeNativeBridge } from './native-bridge'
2323
import { WorkerApi, YAOB_THROTTLE_MS } from './react-native-types'
@@ -177,7 +177,15 @@ async function makeIo(logBackend: LogBackend): Promise<EdgeIo> {
177177

178178
if (privacy === 'nym') {
179179
const nymFetch = await initMixFetch(log)
180-
return await nymFetch(uri, opts)
180+
const response = await nymFetch(
181+
uri,
182+
{
183+
...opts,
184+
mode: 'unsafe-ignore-cors' as RequestMode
185+
},
186+
mixFetchOptions
187+
)
188+
return response
181189
}
182190
if (corsBypass === 'always') {
183191
return await nativeFetch(uri, opts)

src/util/nym.ts

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,85 @@
11
import {
22
createMixFetch,
3-
disconnectMixTunnel,
4-
SetupMixTunnelOpts
3+
disconnectMixFetch,
4+
IMixFetch,
5+
IMixFetchFn,
6+
SetupMixFetchOps
57
} from '@nymproject/mix-fetch'
68

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

9-
/** The fetch-bound function `createMixFetch` resolves to. */
10-
type MixFetchFn = (url: string, init?: RequestInit) => Promise<Response>
11-
1211
/**
13-
* Configuration options for the NYM mixFetch tunnel.
12+
* Configuration options for the NYM mixFetch client.
1413
*/
15-
export const mixFetchOptions: SetupMixTunnelOpts = {
14+
export const mixFetchOptions: SetupMixFetchOps = {
1615
clientId: 'edge-core-js-2026-03-10',
16+
preferredGateway: '5rXcNe2a44vXisK3uqLHCzpzvEwcnsijDMU7hg4fcYk8', // with WSS
17+
preferredNetworkRequester:
18+
'5x6q9UfVHs5AohKMUqeivj7a556kVVy7QwoKige8xHxh.6CFoB3kJaDbYz6oafPJxNxNjzahpT2NtgtytcSyN9EvF@5rXcNe2a44vXisK3uqLHCzpzvEwcnsijDMU7hg4fcYk8',
1719
forceTls: true, // force WSS
18-
// Mixnet round trips are slow, so give the tunnel handshake plenty of time.
19-
// v1 tuned a 5 min `requestTimeoutMs`; v2 exposes no per-request timeout, but
20-
// the tunnel setup is where mixnet latency bites, so restore that 5 min
21-
// budget here to avoid premature failures during the handshake.
22-
connectTimeoutMs: 300000
20+
mixFetchOverride: {
21+
requestTimeoutMs: 300000
22+
}
2323
}
2424

25+
/**
26+
* Budget for `createMixFetch` itself (client start + gateway handshake).
27+
*
28+
* A healthy setup with the pinned gateway completes in under 10s measured.
29+
* Without a bound here the whole app blocks on the first mixnet request for
30+
* as long as a dead gateway keeps us waiting, which reads to the user as a
31+
* freeze.
32+
*/
33+
const SETUP_TIMEOUT_MS = 60000
34+
2535
// MixFetch initialization state
26-
let mixFetchInitPromise: Promise<MixFetchFn> | null = null
36+
let mixFetchInitPromise: Promise<IMixFetch> | null = null
2737

2838
/**
2939
* Initialize the NYM mixFetch client. Must be called before using mixFetch.
3040
* Safe to call multiple times - subsequent calls return the same promise.
3141
*/
32-
export async function initMixFetch(log: EdgeLog): Promise<MixFetchFn> {
42+
export async function initMixFetch(log: EdgeLog): Promise<IMixFetchFn> {
3343
if (mixFetchInitPromise == null) {
3444
log('Initializing mixFetch...')
35-
mixFetchInitPromise = createMixFetch(mixFetchOptions)
36-
.then(mixFetch => {
45+
const pending = createMixFetch(mixFetchOptions)
46+
// The timeout below can abandon this setup while it is still in flight.
47+
// Deliberately do NOT tear it down on late completion: `createMixFetch`
48+
// resolves to a healthy global singleton, and disconnecting it (a
49+
// process-wide operation) would race a newer init that has taken over.
50+
// A late completion just repopulates `__mixFetchGlobal`, which the next
51+
// init reuses. Swallow a late rejection so it is not unhandled.
52+
pending.catch(() => {})
53+
let timer: ReturnType<typeof setTimeout> | undefined
54+
const timeout = new Promise<never>((resolve, reject) => {
55+
timer = setTimeout(() => {
56+
reject(
57+
new Error(`mixFetch setup timed out after ${SETUP_TIMEOUT_MS}ms`)
58+
)
59+
}, SETUP_TIMEOUT_MS)
60+
})
61+
mixFetchInitPromise = Promise.race([pending, timeout])
62+
.then(mixFetchModule => {
3763
log('mixFetch initialized successfully')
38-
return mixFetch
64+
return mixFetchModule
3965
})
4066
.catch(async error => {
41-
// Tear down any partially-established tunnel left by the failed init
42-
// so the next createMixFetch call starts fresh instead of reusing a
67+
// Clean up stale global state left by the failed init so the
68+
// next createMixFetch call starts fresh instead of reusing a
4369
// broken singleton.
4470
try {
45-
await disconnectMixTunnel()
71+
await disconnectMixFetch()
4672
} catch {}
73+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
74+
delete (window as any).__mixFetchGlobal
4775
mixFetchInitPromise = null
4876
log.error('mixFetch initialization failed:', error)
4977
throw error
5078
})
79+
.finally(() => {
80+
clearTimeout(timer)
81+
})
5182
}
52-
return await mixFetchInitPromise
83+
const mixFetchModule = await mixFetchInitPromise
84+
return mixFetchModule.mixFetch
5385
}

webpack.config.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ module.exports = {
5050
static: bundlePath
5151
},
5252
entry: './src/io/react-native/react-native-worker.ts',
53+
experiments: {
54+
asyncWebAssembly: true
55+
},
5356
mode: debug ? 'development' : 'production',
5457
module: {
5558
rules: [
@@ -72,6 +75,10 @@ module.exports = {
7275
loader: 'babel-loader',
7376
options: { presets: ['@babel/preset-env'] }
7477
}
78+
},
79+
{
80+
test: /\.wasm$/,
81+
type: 'webassembly/async'
7582
}
7683
]
7784
},
@@ -85,12 +92,29 @@ module.exports = {
8592
plugins: [
8693
new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'] }),
8794
new webpack.ProvidePlugin({ process: ['process'] }),
95+
// Copy static files and mix-fetch WASM/worker files
8896
new CopyPlugin({
8997
patterns: [
9098
// HTML entry point
9199
{
92100
from: path.resolve(__dirname, 'src/index.html'),
93101
to: 'index.html'
102+
},
103+
// mix-fetch WASM files for NYM mixnet support
104+
{
105+
from: path.resolve(
106+
__dirname,
107+
'node_modules/@nymproject/mix-fetch/*.wasm'
108+
),
109+
to: '[name][ext]'
110+
},
111+
// mix-fetch web worker files
112+
{
113+
from: path.resolve(
114+
__dirname,
115+
'node_modules/@nymproject/mix-fetch/web-worker-*.js'
116+
),
117+
to: '[name][ext]'
94118
}
95119
]
96120
})

0 commit comments

Comments
 (0)