Upgrade nostr-tools from the fork to upstream 2.25.2

The fork existed only to strip the package's `exports` map so metro would
bundle the TS source; upstream now ships a build that react-native
consumes directly (README documents the polyfills, all of which the app
already installs), so the fork has no reason to exist.

Fallout of the upgrade:

- `subscribeMany` now takes a single filter rather than an array.
- `SimplePool` drops a relay from its map when it gives up on it, so an
  empty map - not a `false` status - is what "everything is down" looks
  like now. Opt into `enableReconnect` so the pool restores dropped
  connections and refires their subscriptions with an advanced `since`.
- `message-port-polyfill` was there for the old nostr-tools; nothing in
  the tree references MessagePort any more.
- The published build is plain CJS/ESM, so jest can load the real library
  and the mock that stood in for it is gone. Nested @noble copies need
  babel, hence the transformIgnorePatterns tweak.

Covered by a NIP-06 derivation test against the standard vector, so the
identity the wallet derives from a seed phrase is provably unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
minibits-cash
2026-09-11 22:23:17 +02:00
co-authored by Claude Opus 5
parent 2632b3dcf7
commit ace952dd7e
9 changed files with 61 additions and 152 deletions
-46
View File
@@ -1,46 +0,0 @@
/**
* Jest manual mock for nostr-tools and all of its subpaths.
*
* nostr-tools ships TypeScript SOURCE and vendors its own @noble/curves and
* @noble/hashes (also source) which import old-style subpaths jest cannot resolve.
* Mapping those would silently redirect the nested copies onto the top-level @noble
* packages — different versions, on crypto code. Not a trade worth making to run a
* test.
*
* This matters far beyond nostr: `services/keyChain` imports nostr-tools, the
* services barrel imports keyChain, and the MODEL layer imports that barrel — so
* nostr-tools loads for every store test even though none goes near nostr.
*
* Calls throw rather than returning plausible fakes: a test that silently derives a
* bogus key and asserts on it is worse than one that stops and explains. Code that
* genuinely needs nostr should mock the calling service (as the existing suites do
* with nostrService).
*/
const makeNamespace = path => {
const cache = new Map()
return new Proxy(
{},
{
get(_target, prop) {
if (prop === '__esModule') return true
if (prop === 'then') return undefined
if (typeof prop === 'symbol') return undefined
if (!cache.has(prop)) {
const name = `${path}.${String(prop)}`
const fn = () => {
throw new Error(
`[nostr-tools mock] ${name}() was called in a test. This mock exists ` +
`only so the module graph resolves — nostr-tools ships TS source ` +
`with vendored @noble packages. Mock the calling service instead.`,
)
}
cache.set(prop, fn)
}
return cache.get(prop)
},
},
)
}
module.exports = makeNamespace('nostr-tools')
+21
View File
@@ -0,0 +1,21 @@
import {KeyChain} from '../src/services/keyChain'
// NIP-06 test vector (nostr-tools' own): the derivation the wallet uses to turn a
// seed phrase into the user's nostr identity. This is the only place in the app
// that runs nostr-tools crypto, so it also guards the library upgrade itself.
const MNEMONIC = 'leader monkey parrot ring guide accident before fence cannon height naive bean'
const NPUB_HEX = '17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6eec3ca5cd917'
const NSEC_HEX = '7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a'
describe('deriveNostrKeyPair', () => {
it('derives the NIP-06 account 0 keypair from a mnemonic', () => {
expect(KeyChain.deriveNostrKeyPair(MNEMONIC)).toEqual({
publicKey: NPUB_HEX,
privateKey: NSEC_HEX,
})
})
it('derives a different keypair for another account index', () => {
expect(KeyChain.deriveNostrKeyPair(MNEMONIC, 1).publicKey).not.toBe(NPUB_HEX)
})
})
-1
View File
@@ -3,7 +3,6 @@ import { install } from 'react-native-quick-crypto' // needed for secp256k1, con
install() install()
import 'react-native-url-polyfill/auto' // URL.host etc import 'react-native-url-polyfill/auto' // URL.host etc
import 'text-encoding-polyfill' // cashu-ts import 'text-encoding-polyfill' // cashu-ts
import 'message-port-polyfill' // nostr-tools
import notifee from '@notifee/react-native' import notifee from '@notifee/react-native'
import messaging from '@react-native-firebase/messaging' import messaging from '@react-native-firebase/messaging'
import {AppRegistry} from 'react-native' import {AppRegistry} from 'react-native'
+3 -6
View File
@@ -4,8 +4,10 @@ module.exports = {
// matches every .js file under __tests__, which would pull in the i18n // matches every .js file under __tests__, which would pull in the i18n
// scripts (missingTranslations.js etc.) that are run via `yarn test:i18n`. // scripts (missingTranslations.js etc.) that are run via `yarn test:i18n`.
testMatch: ['**/*.(test|spec).[jt]s?(x)'], testMatch: ['**/*.(test|spec).[jt]s?(x)'],
// The `(.*/)?` makes the allowlist apply at any depth: nostr-tools carries its
// own ESM-only @noble copies in a nested node_modules, and those need babel too.
transformIgnorePatterns: [ transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native|@react-native-community|@cashu|@noble|@scure|react-native-flash-message)/)', 'node_modules/(?!(.*/)?((jest-)?react-native|@react-native|@react-native-community|@cashu|@noble|@scure|react-native-flash-message)/)',
], ],
// Several native or source-shipped packages are unusable under jest, and the // Several native or source-shipped packages are unusable under jest, and the
// MODEL layer reaches all of them at import time (Mint -> services barrel -> // MODEL layer reaches all of them at import time (Mint -> services barrel ->
@@ -34,11 +36,6 @@ module.exports = {
// parse. Mocking Sentry rather than logService lets the REAL logger load, so // parse. Mocking Sentry rather than logService lets the REAL logger load, so
// tests can cover code that logs instead of stubbing the logger away. // tests can cover code that logs instead of stubbing the logger away.
'^@sentry/react-native$': '<rootDir>/__mocks__/sentry-react-native.js', '^@sentry/react-native$': '<rootDir>/__mocks__/sentry-react-native.js',
// nostr-tools ships TS source and vendors its own @noble/curves + @noble/hashes
// (also source), importing subpaths jest cannot resolve. Mapping those would
// cross the nested copies onto the top-level @noble versions — on crypto code.
// Mock the surface instead; nothing in the model layer needs real nostr.
'^nostr-tools(/.*)?$': '<rootDir>/__mocks__/nostr-tools.js',
// react-native-localize is a native TurboModule, and src/i18n calls getLocales() // react-native-localize is a native TurboModule, and src/i18n calls getLocales()
// at module scope — so without this, importing `translate` anywhere makes the // at module scope — so without this, importing `translate` anywhere makes the
// module unloadable under jest. // module unloadable under jest.
+1 -2
View File
@@ -51,11 +51,10 @@
"js-lnurl": "^0.6.0", "js-lnurl": "^0.6.0",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.debounce": "4.0.8", "lodash.debounce": "4.0.8",
"message-port-polyfill": "^0.2.0",
"mobx": "^6.13.7", "mobx": "^6.13.7",
"mobx-react-lite": "^4.1.0", "mobx-react-lite": "^4.1.0",
"mobx-state-tree": "^7.0.2", "mobx-state-tree": "^7.0.2",
"nostr-tools": "minibits-cash/nostr-tools#2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7", "nostr-tools": "2.25.2",
"numbro": "^2.5.0", "numbro": "^2.5.0",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"react": "19.2.3", "react": "19.2.3",
+2 -2
View File
@@ -875,12 +875,12 @@ export const NwcStoreModel = types
const connectionsPubkeys = self.nwcConnections.map(c => c.connectionPubkey) const connectionsPubkeys = self.nwcConnections.map(c => c.connectionPubkey)
let eventsBatch: NostrEvent[] = [] let eventsBatch: NostrEvent[] = []
const filter = [{ const filter = {
kinds: [NWCWalletRequest], kinds: [NWCWalletRequest],
authors: connectionsPubkeys, authors: connectionsPubkeys,
"#p": [self.walletPubkey], "#p": [self.walletPubkey],
since since
}] }
const pool = NostrClient.getRelayPool() const pool = NostrClient.getRelayPool()
const relaysStore = getRootStore(self).relaysStore const relaysStore = getRootStore(self).relaysStore
+6 -2
View File
@@ -61,7 +61,9 @@ let _pool: any = undefined
const getRelayPool = function () { const getRelayPool = function () {
if(!_pool) { if(!_pool) {
_pool = new SimplePool() // enableReconnect lets the pool restore dropped relay connections and refire
// their subscriptions with an advanced `since`, instead of silently going deaf.
_pool = new SimplePool({enableReconnect: true})
return _pool as SimplePool return _pool as SimplePool
} }
@@ -96,7 +98,9 @@ const reconnectToRelays = async function (options: ReconnectToRelaysOptions) {
log.trace('[reconnectToRelays] Current statuses', {connections: Object.fromEntries(connections)}) log.trace('[reconnectToRelays] Current statuses', {connections: Object.fromEntries(connections)})
let isRefreshSubNeeded: boolean = false // a relay the pool gave up on is dropped from its map entirely, so an empty map
// means everything is down, not that everything is fine
let isRefreshSubNeeded: boolean = connections.size === 0
for (const conn of Array.from(connections)) { for (const conn of Array.from(connections)) {
if(conn[1] === false) { if(conn[1] === false) {
@@ -231,7 +231,7 @@ const receiveEventsFromRelaysQueue = async function (): Promise<void> {
let relaysToConnect = relaysStore.allUrls let relaysToConnect = relaysStore.allUrls
let eventsBatch: NostrEvent[] = [] let eventsBatch: NostrEvent[] = []
pool.subscribeMany(relaysToConnect, [filter], { pool.subscribeMany(relaysToConnect, filter, {
onevent(event) { onevent(event) {
if (eventsBatch.some(ev => ev.id === event.id)) { if (eventsBatch.some(ev => ev.id === event.id)) {
log.warn( log.warn(
+27 -92
View File
@@ -5433,19 +5433,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@noble/ciphers@npm:^0.5.1": "@noble/ciphers@npm:2.1.1":
version: 0.5.3 version: 2.1.1
resolution: "@noble/ciphers@npm:0.5.3" resolution: "@noble/ciphers@npm:2.1.1"
checksum: c5ed5d7d43b054c2051b3e0e220353cc9d31fa8d17b82cfb753d87f922e4e1e69b73ca5273a9cc457023f83d96d1d9a51678d2f6d4e58ca039d1111a62856d19 checksum: 1e1bfcd8ccb6dce7df20f2ac0162fdea2a760f6f8159d2c4a3ed754734a58fc484ea4ed953a4fa16d590662e44b55282455bbc90c56455ab102d5462db81e6cc
languageName: node languageName: node
linkType: hard linkType: hard
"@noble/curves@npm:1.2.0": "@noble/curves@npm:2.0.1":
version: 1.2.0 version: 2.0.1
resolution: "@noble/curves@npm:1.2.0" resolution: "@noble/curves@npm:2.0.1"
dependencies: dependencies:
"@noble/hashes": 1.3.2 "@noble/hashes": 2.0.1
checksum: bb798d7a66d8e43789e93bc3c2ddff91a1e19fdb79a99b86cd98f1e5eff0ee2024a2672902c2576ef3577b6f282f3b5c778bebd55761ddbb30e36bf275e83dd0 checksum: b6844f350d629bb6de55d3a123bc148403901c8b8eed77d2132a389d080d553242bcf1e06913bf51f51e395849ec70b9a8e8902c19e562ea3c6538bb0240d92f
languageName: node languageName: node
linkType: hard linkType: hard
@@ -5458,29 +5458,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@noble/curves@npm:~1.1.0":
version: 1.1.0
resolution: "@noble/curves@npm:1.1.0"
dependencies:
"@noble/hashes": 1.3.1
checksum: 2658cdd3f84f71079b4e3516c47559d22cf4b55c23ac8ee9d2b1f8e5b72916d9689e59820e0f9d9cb4a46a8423af5b56dc6bb7782405c88be06a015180508db5
languageName: node
linkType: hard
"@noble/hashes@npm:1.3.1":
version: 1.3.1
resolution: "@noble/hashes@npm:1.3.1"
checksum: 7fdefc0f7a0c1ec27acc6ff88841793e3f93ec4ce6b8a6a12bfc0dd70ae6b7c4c82fe305fdfeda1735d5ad4a9eebe761e6693b3d355689c559e91242f4bc95b1
languageName: node
linkType: hard
"@noble/hashes@npm:1.3.2":
version: 1.3.2
resolution: "@noble/hashes@npm:1.3.2"
checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474
languageName: node
linkType: hard
"@noble/hashes@npm:2.0.1": "@noble/hashes@npm:2.0.1":
version: 2.0.1 version: 2.0.1
resolution: "@noble/hashes@npm:2.0.1" resolution: "@noble/hashes@npm:2.0.1"
@@ -5509,13 +5486,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1":
version: 1.3.3
resolution: "@noble/hashes@npm:1.3.3"
checksum: 8a6496d1c0c64797339bc694ad06cdfaa0f9e56cd0c3f68ae3666cfb153a791a55deb0af9c653c7ed2db64d537aa3e3054629740d2f2338bb1dcb7ab60cd205b
languageName: node
linkType: hard
"@nodable/entities@npm:^2.1.0": "@nodable/entities@npm:^2.1.0":
version: 2.1.1 version: 2.1.1
resolution: "@nodable/entities@npm:2.1.1" resolution: "@nodable/entities@npm:2.1.1"
@@ -6486,13 +6456,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@scure/base@npm:1.1.1":
version: 1.1.1
resolution: "@scure/base@npm:1.1.1"
checksum: b4fc810b492693e7e8d0107313ac74c3646970c198bbe26d7332820886fa4f09441991023ec9aa3a2a51246b74409ab5ebae2e8ef148bbc253da79ac49130309
languageName: node
linkType: hard
"@scure/base@npm:2.0.0": "@scure/base@npm:2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "@scure/base@npm:2.0.0" resolution: "@scure/base@npm:2.0.0"
@@ -6507,21 +6470,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@scure/base@npm:~1.1.0": "@scure/bip32@npm:2.0.1":
version: 1.1.9 version: 2.0.1
resolution: "@scure/base@npm:1.1.9" resolution: "@scure/bip32@npm:2.0.1"
checksum: 120820a37dfe9dfe4cab2b7b7460552d08e67dee8057ed5354eb68d8e3440890ae983ce3bee957d2b45684950b454a2b6d71d5ee77c1fd3fddc022e2a510337f
languageName: node
linkType: hard
"@scure/bip32@npm:1.3.1":
version: 1.3.1
resolution: "@scure/bip32@npm:1.3.1"
dependencies: dependencies:
"@noble/curves": ~1.1.0 "@noble/curves": 2.0.1
"@noble/hashes": ~1.3.1 "@noble/hashes": 2.0.1
"@scure/base": ~1.1.0 "@scure/base": 2.0.0
checksum: 394d65f77a40651eba21a5096da0f4233c3b50d422864751d373fcf142eeedb94a1149f9ab1dbb078086dab2d0bc27e2b1afec8321bf22d4403c7df2fea5bfe2 checksum: 5e6c7b455c4a5599673d5fa1b7edc1b3655fe2f7b8388dc19ebe4a89adb9c80c511215d4af624eb774c63c216fd8b7bb8fecf1eb8c3d5e979e279c7542657a42
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6536,16 +6492,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@scure/bip39@npm:1.2.1":
version: 1.2.1
resolution: "@scure/bip39@npm:1.2.1"
dependencies:
"@noble/hashes": ~1.3.0
"@scure/base": ~1.1.0
checksum: c5bd6f1328fdbeae2dcdd891825b1610225310e5e62a4942714db51066866e4f7bef242c7b06a1b9dcc8043a4a13412cf5c5df76d3b10aa9e36b82e9b6e3eeaa
languageName: node
linkType: hard
"@scure/bip39@npm:2.0.1": "@scure/bip39@npm:2.0.1":
version: 2.0.1 version: 2.0.1
resolution: "@scure/bip39@npm:2.0.1" resolution: "@scure/bip39@npm:2.0.1"
@@ -14485,13 +14431,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"message-port-polyfill@npm:^0.2.0":
version: 0.2.0
resolution: "message-port-polyfill@npm:0.2.0"
checksum: 7cba58baa04fb8816b87bbf1e1af1f657dbaad943a8243dffc3f937d7a88c7498c265efbfbf797964160cb9eb55d6aa7f1abe286499c0083944e1f14e31867dc
languageName: node
linkType: hard
"methods@npm:~1.1.2": "methods@npm:~1.1.2":
version: 1.1.2 version: 1.1.2
resolution: "methods@npm:1.1.2" resolution: "methods@npm:1.1.2"
@@ -14877,11 +14816,10 @@ __metadata:
js-lnurl: ^0.6.0 js-lnurl: ^0.6.0
lodash.clonedeep: ^4.5.0 lodash.clonedeep: ^4.5.0
lodash.debounce: 4.0.8 lodash.debounce: 4.0.8
message-port-polyfill: ^0.2.0
mobx: ^6.13.7 mobx: ^6.13.7
mobx-react-lite: ^4.1.0 mobx-react-lite: ^4.1.0
mobx-state-tree: ^7.0.2 mobx-state-tree: ^7.0.2
nostr-tools: "minibits-cash/nostr-tools#2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7" nostr-tools: 2.25.2
numbro: ^2.5.0 numbro: ^2.5.0
patch-package: ^8.0.0 patch-package: ^8.0.0
prettier: 2.8.8 prettier: 2.8.8
@@ -15455,26 +15393,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"nostr-tools@minibits-cash/nostr-tools#2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7": "nostr-tools@npm:2.25.2":
version: 2.10.4 version: 2.25.2
resolution: "nostr-tools@https://github.com/minibits-cash/nostr-tools.git#commit=2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7" resolution: "nostr-tools@npm:2.25.2"
dependencies: dependencies:
"@noble/ciphers": ^0.5.1 "@noble/ciphers": 2.1.1
"@noble/curves": 1.2.0 "@noble/curves": 2.0.1
"@noble/hashes": 1.3.1 "@noble/hashes": 2.0.1
"@scure/base": 1.1.1 "@scure/base": 2.0.0
"@scure/bip32": 1.3.1 "@scure/bip32": 2.0.1
"@scure/bip39": 1.2.1 "@scure/bip39": 2.0.1
nostr-wasm: 0.1.0 nostr-wasm: 0.1.0
peerDependencies: peerDependencies:
typescript: ">=5.0.0" typescript: ">=5.0.0"
dependenciesMeta:
nostr-wasm:
optional: true
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: c5b9871649eef2479e23026f41a3d771bbd879bd54bd60f226eb547bb85e03e411023a756dbbb24966cf0d00bfb3541fc7ca502e1b71a2844fffa55024214462 checksum: 3e288521f9650dd004a870cd81b1acb06463ee1d848dc1d06f1fc752715361c889c04a968cc6beaf475c33df974a139806f29754cfb2b549d4f1ace0133f6e5c
languageName: node languageName: node
linkType: hard linkType: hard