From ace952dd7efdaa8bc96161063370c3929c935b3c Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Fri, 11 Sep 2026 22:23:17 +0200 Subject: [PATCH] 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 --- __mocks__/nostr-tools.js | 46 ------- __tests__/nostrKeyDerivation.test.ts | 21 ++++ index.js | 1 - jest.config.js | 9 +- package.json | 3 +- src/models/NwcStore.ts | 4 +- src/services/nostrService.ts | 8 +- .../wallet/operations/nostrOperations.ts | 2 +- yarn.lock | 119 ++++-------------- 9 files changed, 61 insertions(+), 152 deletions(-) delete mode 100644 __mocks__/nostr-tools.js create mode 100644 __tests__/nostrKeyDerivation.test.ts diff --git a/__mocks__/nostr-tools.js b/__mocks__/nostr-tools.js deleted file mode 100644 index 01d3d0c5..00000000 --- a/__mocks__/nostr-tools.js +++ /dev/null @@ -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') diff --git a/__tests__/nostrKeyDerivation.test.ts b/__tests__/nostrKeyDerivation.test.ts new file mode 100644 index 00000000..e2e4d0fd --- /dev/null +++ b/__tests__/nostrKeyDerivation.test.ts @@ -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) + }) +}) diff --git a/index.js b/index.js index 8596253a..21641301 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,6 @@ import { install } from 'react-native-quick-crypto' // needed for secp256k1, con install() import 'react-native-url-polyfill/auto' // URL.host etc import 'text-encoding-polyfill' // cashu-ts -import 'message-port-polyfill' // nostr-tools import notifee from '@notifee/react-native' import messaging from '@react-native-firebase/messaging' import {AppRegistry} from 'react-native' diff --git a/jest.config.js b/jest.config.js index 7e64eaef..54dde50d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,8 +4,10 @@ module.exports = { // matches every .js file under __tests__, which would pull in the i18n // scripts (missingTranslations.js etc.) that are run via `yarn test:i18n`. 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: [ - '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 // 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 // tests can cover code that logs instead of stubbing the logger away. '^@sentry/react-native$': '/__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(/.*)?$': '/__mocks__/nostr-tools.js', // react-native-localize is a native TurboModule, and src/i18n calls getLocales() // at module scope — so without this, importing `translate` anywhere makes the // module unloadable under jest. diff --git a/package.json b/package.json index 8cf7bc27..6d886902 100644 --- a/package.json +++ b/package.json @@ -51,11 +51,10 @@ "js-lnurl": "^0.6.0", "lodash.clonedeep": "^4.5.0", "lodash.debounce": "4.0.8", - "message-port-polyfill": "^0.2.0", "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", "mobx-state-tree": "^7.0.2", - "nostr-tools": "minibits-cash/nostr-tools#2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7", + "nostr-tools": "2.25.2", "numbro": "^2.5.0", "patch-package": "^8.0.0", "react": "19.2.3", diff --git a/src/models/NwcStore.ts b/src/models/NwcStore.ts index 721c10bf..aed133f6 100644 --- a/src/models/NwcStore.ts +++ b/src/models/NwcStore.ts @@ -875,12 +875,12 @@ export const NwcStoreModel = types const connectionsPubkeys = self.nwcConnections.map(c => c.connectionPubkey) let eventsBatch: NostrEvent[] = [] - const filter = [{ + const filter = { kinds: [NWCWalletRequest], authors: connectionsPubkeys, "#p": [self.walletPubkey], since - }] + } const pool = NostrClient.getRelayPool() const relaysStore = getRootStore(self).relaysStore diff --git a/src/services/nostrService.ts b/src/services/nostrService.ts index 8c1eb7e0..555717fb 100644 --- a/src/services/nostrService.ts +++ b/src/services/nostrService.ts @@ -61,7 +61,9 @@ let _pool: any = undefined const getRelayPool = function () { 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 } @@ -96,7 +98,9 @@ const reconnectToRelays = async function (options: ReconnectToRelaysOptions) { 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)) { if(conn[1] === false) { diff --git a/src/services/wallet/operations/nostrOperations.ts b/src/services/wallet/operations/nostrOperations.ts index ba68612a..73618907 100644 --- a/src/services/wallet/operations/nostrOperations.ts +++ b/src/services/wallet/operations/nostrOperations.ts @@ -231,7 +231,7 @@ const receiveEventsFromRelaysQueue = async function (): Promise { let relaysToConnect = relaysStore.allUrls let eventsBatch: NostrEvent[] = [] - pool.subscribeMany(relaysToConnect, [filter], { + pool.subscribeMany(relaysToConnect, filter, { onevent(event) { if (eventsBatch.some(ev => ev.id === event.id)) { log.warn( diff --git a/yarn.lock b/yarn.lock index 1a95be36..56773579 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5433,19 +5433,19 @@ __metadata: languageName: node linkType: hard -"@noble/ciphers@npm:^0.5.1": - version: 0.5.3 - resolution: "@noble/ciphers@npm:0.5.3" - checksum: c5ed5d7d43b054c2051b3e0e220353cc9d31fa8d17b82cfb753d87f922e4e1e69b73ca5273a9cc457023f83d96d1d9a51678d2f6d4e58ca039d1111a62856d19 +"@noble/ciphers@npm:2.1.1": + version: 2.1.1 + resolution: "@noble/ciphers@npm:2.1.1" + checksum: 1e1bfcd8ccb6dce7df20f2ac0162fdea2a760f6f8159d2c4a3ed754734a58fc484ea4ed953a4fa16d590662e44b55282455bbc90c56455ab102d5462db81e6cc languageName: node linkType: hard -"@noble/curves@npm:1.2.0": - version: 1.2.0 - resolution: "@noble/curves@npm:1.2.0" +"@noble/curves@npm:2.0.1": + version: 2.0.1 + resolution: "@noble/curves@npm:2.0.1" dependencies: - "@noble/hashes": 1.3.2 - checksum: bb798d7a66d8e43789e93bc3c2ddff91a1e19fdb79a99b86cd98f1e5eff0ee2024a2672902c2576ef3577b6f282f3b5c778bebd55761ddbb30e36bf275e83dd0 + "@noble/hashes": 2.0.1 + checksum: b6844f350d629bb6de55d3a123bc148403901c8b8eed77d2132a389d080d553242bcf1e06913bf51f51e395849ec70b9a8e8902c19e562ea3c6538bb0240d92f languageName: node linkType: hard @@ -5458,29 +5458,6 @@ __metadata: languageName: node 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": version: 2.0.1 resolution: "@noble/hashes@npm:2.0.1" @@ -5509,13 +5486,6 @@ __metadata: languageName: node 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": version: 2.1.1 resolution: "@nodable/entities@npm:2.1.1" @@ -6486,13 +6456,6 @@ __metadata: languageName: node 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": version: 2.0.0 resolution: "@scure/base@npm:2.0.0" @@ -6507,21 +6470,14 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:~1.1.0": - version: 1.1.9 - resolution: "@scure/base@npm:1.1.9" - checksum: 120820a37dfe9dfe4cab2b7b7460552d08e67dee8057ed5354eb68d8e3440890ae983ce3bee957d2b45684950b454a2b6d71d5ee77c1fd3fddc022e2a510337f - languageName: node - linkType: hard - -"@scure/bip32@npm:1.3.1": - version: 1.3.1 - resolution: "@scure/bip32@npm:1.3.1" +"@scure/bip32@npm:2.0.1": + version: 2.0.1 + resolution: "@scure/bip32@npm:2.0.1" dependencies: - "@noble/curves": ~1.1.0 - "@noble/hashes": ~1.3.1 - "@scure/base": ~1.1.0 - checksum: 394d65f77a40651eba21a5096da0f4233c3b50d422864751d373fcf142eeedb94a1149f9ab1dbb078086dab2d0bc27e2b1afec8321bf22d4403c7df2fea5bfe2 + "@noble/curves": 2.0.1 + "@noble/hashes": 2.0.1 + "@scure/base": 2.0.0 + checksum: 5e6c7b455c4a5599673d5fa1b7edc1b3655fe2f7b8388dc19ebe4a89adb9c80c511215d4af624eb774c63c216fd8b7bb8fecf1eb8c3d5e979e279c7542657a42 languageName: node linkType: hard @@ -6536,16 +6492,6 @@ __metadata: languageName: node 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": version: 2.0.1 resolution: "@scure/bip39@npm:2.0.1" @@ -14485,13 +14431,6 @@ __metadata: languageName: node 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": version: 1.1.2 resolution: "methods@npm:1.1.2" @@ -14877,11 +14816,10 @@ __metadata: js-lnurl: ^0.6.0 lodash.clonedeep: ^4.5.0 lodash.debounce: 4.0.8 - message-port-polyfill: ^0.2.0 mobx: ^6.13.7 mobx-react-lite: ^4.1.0 mobx-state-tree: ^7.0.2 - nostr-tools: "minibits-cash/nostr-tools#2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7" + nostr-tools: 2.25.2 numbro: ^2.5.0 patch-package: ^8.0.0 prettier: 2.8.8 @@ -15455,26 +15393,23 @@ __metadata: languageName: node linkType: hard -"nostr-tools@minibits-cash/nostr-tools#2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7": - version: 2.10.4 - resolution: "nostr-tools@https://github.com/minibits-cash/nostr-tools.git#commit=2991f59d0150a0cb2c71e2f9a4cb6c8aca3b19d7" +"nostr-tools@npm:2.25.2": + version: 2.25.2 + resolution: "nostr-tools@npm:2.25.2" dependencies: - "@noble/ciphers": ^0.5.1 - "@noble/curves": 1.2.0 - "@noble/hashes": 1.3.1 - "@scure/base": 1.1.1 - "@scure/bip32": 1.3.1 - "@scure/bip39": 1.2.1 + "@noble/ciphers": 2.1.1 + "@noble/curves": 2.0.1 + "@noble/hashes": 2.0.1 + "@scure/base": 2.0.0 + "@scure/bip32": 2.0.1 + "@scure/bip39": 2.0.1 nostr-wasm: 0.1.0 peerDependencies: typescript: ">=5.0.0" - dependenciesMeta: - nostr-wasm: - optional: true peerDependenciesMeta: typescript: optional: true - checksum: c5b9871649eef2479e23026f41a3d771bbd879bd54bd60f226eb547bb85e03e411023a756dbbb24966cf0d00bfb3541fc7ca502e1b71a2844fffa55024214462 + checksum: 3e288521f9650dd004a870cd81b1acb06463ee1d848dc1d06f1fc752715361c889c04a968cc6beaf475c33df974a139806f29754cfb2b549d4f1ace0133f6e5c languageName: node linkType: hard