diff --git a/js/payments/zapReceiptValidator.js b/js/payments/zapReceiptValidator.js index 675f0b89..f35d46b2 100644 --- a/js/payments/zapReceiptValidator.js +++ b/js/payments/zapReceiptValidator.js @@ -1,7 +1,11 @@ // js/payments/zapReceiptValidator.js import { nostrToolsReady } from "../nostrToolsBootstrap.js"; -import { normalizeToolkitCandidate, readToolkitFromScope } from "../nostr/toolkit.js"; +import { + normalizeToolkitCandidate, + readToolkitFromScope, + shimLegacySimplePoolMethods, +} from "../nostr/toolkit.js"; import { userLogger } from "../utils/logger.js"; import { bech32, bytesToHex, sha256 } from "../../vendor/crypto-helpers.bundle.min.js"; @@ -212,6 +216,37 @@ function buildValidationResult({ }; } +// Build the relay pool used to fetch the zap receipt. nostr-tools 2.x SimplePool +// no longer exposes .list(), so a freshly constructed pool can't query — which +// made receipt validation always fail with "Unable to initialize a relay pool" +// after an otherwise-successful zap. Apply the app's legacy .list shim (unless a +// listEvents override supplies its own fetch path). +export function resolveReceiptListPool(tools, overrides = {}) { + let pool = null; + try { + if (typeof overrides.createPool === "function") { + pool = overrides.createPool(tools); + } else if (tools && typeof tools.SimplePool === "function") { + pool = new tools.SimplePool(); + } + } catch (error) { + userLogger.warn("[zapReceiptValidator] Failed to create SimplePool instance.", error); + return null; + } + if ( + pool && + typeof pool.list !== "function" && + typeof overrides.listEvents !== "function" + ) { + try { + shimLegacySimplePoolMethods(pool); + } catch (error) { + userLogger.warn("[zapReceiptValidator] Failed to shim SimplePool.list.", error); + } + } + return pool; +} + export async function validateZapReceipt(context = {}, overrides = {}) { const { zapRequest, @@ -330,19 +365,11 @@ export async function validateZapReceipt(context = {}, overrides = {}) { }); } - let pool; - try { - if (typeof overrides.createPool === "function") { - pool = overrides.createPool(tools); - } else if (typeof tools.SimplePool === "function") { - pool = new tools.SimplePool(); - } - } catch (error) { - userLogger.warn("[zapReceiptValidator] Failed to create SimplePool instance.", error); - pool = null; - } - - if (!pool || typeof pool.list !== "function") { + const pool = resolveReceiptListPool(tools, overrides); + const canListEvents = + typeof overrides.listEvents === "function" || + (pool && typeof pool.list === "function"); + if (!pool || !canListEvents) { return buildValidationResult({ status: "failed", reason: "Unable to initialize a relay pool for receipt validation.", diff --git a/tests/zap-receipt-pool.test.mjs b/tests/zap-receipt-pool.test.mjs new file mode 100644 index 00000000..83444bd4 --- /dev/null +++ b/tests/zap-receipt-pool.test.mjs @@ -0,0 +1,61 @@ +// Regression: after a successful zap, receipt validation failed with "Unable to +// initialize a relay pool for receipt validation." nostr-tools 2.x SimplePool no +// longer has .list(), and the validator constructed a fresh raw pool and required +// pool.list. resolveReceiptListPool must apply the app's legacy .list shim so a +// 2.x pool can query — and honor a listEvents override without needing .list. + +import "./test-helpers/setup-localstorage.mjs"; +import assert from "node:assert/strict"; +import test from "node:test"; +import { resolveReceiptListPool } from "../js/payments/zapReceiptValidator.js"; + +// Mimics a nostr-tools 2.x SimplePool: instances have subscribeMany/close but NO +// .list() (the method was removed in 2.x). +function makeV2Tools() { + function SimplePool() { + this.subscribeMany = () => ({ close() {} }); + this.querySync = async () => []; + this.close = () => {}; + } + return { SimplePool }; +} + +test("a freshly constructed 2.x pool (no .list) gets the legacy .list shim", () => { + const tools = makeV2Tools(); + const bare = new tools.SimplePool(); + assert.equal(typeof bare.list, "undefined", "2.x pool starts without .list"); + + const pool = resolveReceiptListPool(tools, {}); + assert.ok(pool, "a pool must be returned"); + assert.equal( + typeof pool.list, + "function", + "the shim must add .list so receipt validation can query relays", + ); +}); + +test("createPool override is respected and shimmed when it lacks .list", () => { + const created = { subscribeMany: () => ({ close() {} }), close: () => {} }; + const pool = resolveReceiptListPool({}, { createPool: () => created }); + assert.equal(pool, created, "must use the override-created pool"); + assert.equal(typeof pool.list, "function", "and shim it with .list"); +}); + +test("a listEvents override means the pool is left as-is (no shim required)", () => { + const created = { close: () => {} }; // no list, no subscribeMany + const pool = resolveReceiptListPool( + {}, + { createPool: () => created, listEvents: async () => [] }, + ); + assert.equal(pool, created); + assert.equal( + typeof pool.list, + "undefined", + "with a listEvents override the validator uses it, so .list isn't forced", + ); +}); + +test("returns null when there is no SimplePool and no override", () => { + const pool = resolveReceiptListPool({}, {}); + assert.equal(pool, null); +});