Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- fixed: Rerandomize the NYM mixnet client id on each connect, so a client registration that goes bad is abandoned instead of being reloaded from storage by every later connect and every later app launch.

## 2.47.1 (2026-07-17)

- 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.
Expand Down
14 changes: 5 additions & 9 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, mixFetchOptions } from '../../util/nym'
import { initMixFetch } from '../../util/nym'
import { fetchCorsProxy } from './fetch-cors-proxy'

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

if (privacy === 'nym') {
const nymFetch = await initMixFetch(log)
return await nymFetch(
uri,
{
...opts,
mode: 'unsafe-ignore-cors' as RequestMode
},
mixFetchOptions
)
return await nymFetch(uri, {
...opts,
mode: 'unsafe-ignore-cors' as RequestMode
})
}
if (corsBypass === 'always') {
return await fetchCorsProxy(uri, opts)
Expand Down
14 changes: 5 additions & 9 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, mixFetchOptions } from '../../util/nym'
import { initMixFetch } 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,14 +177,10 @@ async function makeIo(logBackend: LogBackend): Promise<EdgeIo> {

if (privacy === 'nym') {
const nymFetch = await initMixFetch(log)
const response = await nymFetch(
uri,
{
...opts,
mode: 'unsafe-ignore-cors' as RequestMode
},
mixFetchOptions
)
const response = await nymFetch(uri, {
...opts,
mode: 'unsafe-ignore-cors' as RequestMode
})
return response
}
if (corsBypass === 'always') {
Expand Down
38 changes: 34 additions & 4 deletions src/util/nym.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,37 @@ import {
import { EdgeLog } from '../types/types'

/**
* Configuration options for the NYM mixFetch client.
* Mint the client id for one mixnet client.
*
* `clientId` names the client's persistent key storage, so every setup that
* passes the same id adopts whatever registration the previous one left
* behind. A registration that went bad therefore stays bad for every later
* connect, including across app launches, which surfaces as a wallet that
* never syncs and never recovers. Nym's guidance is to rerandomise on
* connect: a fresh id simply abandons the poisoned storage.
*
* The cost is a fresh gateway registration per setup, which the measured
* ~10s handshake already covers.
*/
const makeClientId = (): string => {
const bytes = new Uint8Array(16)
const { crypto } = globalThis
if (crypto?.getRandomValues != null) {
crypto.getRandomValues(bytes)
} else {
for (let i = 0; i < bytes.length; ++i) {
bytes[i] = Math.floor(Math.random() * 256)
}
}
const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0'))
return `edge-core-js-${hex.join('')}`
}

/**
* Configuration options for the NYM mixFetch client, minus the per-setup
* `clientId` that `initMixFetch` adds.
*/
export const mixFetchOptions: SetupMixFetchOps = {
clientId: 'edge-core-js-2026-03-10',
const mixFetchOptions: Omit<SetupMixFetchOps, 'clientId'> = {
preferredGateway: '5rXcNe2a44vXisK3uqLHCzpzvEwcnsijDMU7hg4fcYk8', // with WSS
preferredNetworkRequester:
'5x6q9UfVHs5AohKMUqeivj7a556kVVy7QwoKige8xHxh.6CFoB3kJaDbYz6oafPJxNxNjzahpT2NtgtytcSyN9EvF@5rXcNe2a44vXisK3uqLHCzpzvEwcnsijDMU7hg4fcYk8',
Expand Down Expand Up @@ -42,7 +69,10 @@ let mixFetchInitPromise: Promise<IMixFetch> | null = null
export async function initMixFetch(log: EdgeLog): Promise<IMixFetchFn> {
if (mixFetchInitPromise == null) {
log('Initializing mixFetch...')
const pending = createMixFetch(mixFetchOptions)
const pending = createMixFetch({
...mixFetchOptions,
clientId: makeClientId()
})
// 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
Expand Down
Loading