diff --git a/js/nostr.js b/js/nostr.js index c7080b70..153cbd51 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -1731,6 +1731,7 @@ export { convertEventToVideo }; class NostrClient { constructor() { this.pool = null; + this.poolPromise = null; this.pubkey = null; this.relays = sanitizeRelayList(Array.from(DEFAULT_RELAY_URLS)); this.readRelays = Array.from(this.relays); @@ -4096,7 +4097,7 @@ class NostrClient { this.restoreLocalData(); try { - this.pool = new window.NostrTools.SimplePool(); + await this.ensurePool(); const results = await this.connectToRelays(); const successfulRelays = results .filter((r) => r.success) @@ -4113,6 +4114,54 @@ class NostrClient { } } + ensurePool() { + if (this.pool) { + return Promise.resolve(this.pool); + } + + if (this.poolPromise) { + return this.poolPromise; + } + + const scope = + typeof window !== "undefined" + ? window + : typeof globalThis !== "undefined" + ? globalThis + : null; + + const tools = + scope?.NostrTools || + scope?.__BITVID_CANONICAL_NOSTR_TOOLS__ || + (typeof globalThis !== "undefined" ? globalThis?.NostrTools : null); + + const SimplePool = tools?.SimplePool; + if (typeof SimplePool !== "function") { + const error = new Error("NostrTools SimplePool is unavailable."); + error.code = "nostr-simplepool-unavailable"; + return Promise.reject(error); + } + + const creation = Promise.resolve().then(() => { + const instance = new SimplePool(); + this.pool = instance; + return instance; + }); + + const shared = creation + .then((instance) => { + this.poolPromise = Promise.resolve(instance); + return instance; + }) + .catch((error) => { + this.poolPromise = null; + throw error; + }); + + this.poolPromise = shared; + return shared; + } + // We subscribe to kind `0` purely as a liveness probe because almost every // relay can answer it quickly. Either an `event` or `eose` signals success, // while the 5s timer guards against relays that never respond. We immediately diff --git a/js/payments/platformAddress.js b/js/payments/platformAddress.js index 349b19b5..4aa22dac 100644 --- a/js/payments/platformAddress.js +++ b/js/payments/platformAddress.js @@ -88,13 +88,28 @@ async function fetchAdminMetadata(adminPubkey) { return null; } - if (!nostrClient?.pool || !Array.isArray(nostrClient?.relays)) { + let pool; + try { + pool = await nostrClient.ensurePool(); + } catch (error) { + console.warn("[platformAddress] Failed to initialize Nostr pool", error); + return null; + } + + if (!pool) { + return null; + } + + const relayUrls = Array.isArray(nostrClient?.relays) + ? nostrClient.relays + : []; + if (relayUrls.length === 0) { console.warn("[platformAddress] Nostr client is not ready."); return null; } try { - const events = await nostrClient.pool.list(nostrClient.relays, [ + const events = await pool.list(relayUrls, [ { kinds: [0], authors: [adminPubkey], limit: 1 }, ]); diff --git a/tests/zap-split.test.mjs b/tests/zap-split.test.mjs index b683ce2c..b3e2ba34 100644 --- a/tests/zap-split.test.mjs +++ b/tests/zap-split.test.mjs @@ -49,6 +49,23 @@ const nostrToolsStub = { return null; }, }, + SimplePool: class { + async list() { + return []; + } + sub() { + return { + on() {}, + unsub() {}, + }; + } + async get() { + return null; + } + async ensureRelay() { + return { close() {}, url: "" }; + } + }, }; window.NostrTools = nostrToolsStub; @@ -59,6 +76,9 @@ globalThis.fetch = async () => { }; const { splitAndZap } = await import("../js/payments/zapSplit.js"); +const platformAddressModule = await import("../js/payments/platformAddress.js"); +const { __resetPlatformAddressCache } = platformAddressModule; +const { nostrClient } = await import("../js/nostr.js"); function createDeps({ commentAllowed = 120, @@ -180,6 +200,93 @@ async function testSplitMath() { delete globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__; } +async function testWaitsForPoolBeforePlatformLookup() { + const previousOverride = globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__; + globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = 10; + + __resetPlatformAddressCache(); + + const originalEnsurePool = nostrClient.ensurePool; + const originalPool = nostrClient.pool; + const originalPoolPromise = nostrClient.poolPromise; + + nostrClient.pool = null; + nostrClient.poolPromise = null; + + let poolReady = false; + let listCalls = 0; + let ensureCalls = 0; + + const poolStub = { + async list(relays, filters) { + assert(poolReady, "should wait for ensurePool to resolve before listing metadata"); + listCalls += 1; + return [ + { + pubkey: "f".repeat(64), + content: JSON.stringify({ lud16: "platform@example.com" }), + created_at: Math.floor(Date.now() / 1000), + }, + ]; + }, + }; + + nostrClient.ensurePool = () => { + ensureCalls += 1; + if (nostrClient.poolPromise) { + return nostrClient.poolPromise; + } + const promise = new Promise((resolve) => { + setTimeout(() => { + poolReady = true; + nostrClient.pool = poolStub; + nostrClient.poolPromise = Promise.resolve(poolStub); + resolve(poolStub); + }, 10); + }); + nostrClient.poolPromise = promise; + return promise; + }; + + const { deps: baseDeps } = createDeps(); + const deps = { + lnurl: baseDeps.lnurl, + wallet: baseDeps.wallet, + platformAddress: platformAddressModule, + }; + + const videoEvent = { + id: "event-id", + pubkey: "c".repeat(64), + lightningAddress: "creator@example.com", + tags: [["d", "pointer"]], + kind: 30078, + }; + + try { + const result = await splitAndZap( + { videoEvent, amountSats: 1000, comment: "Delayed pool" }, + deps + ); + + assert.equal(ensureCalls, 1, "should initialize the pool once"); + assert.equal(listCalls, 1, "should fetch platform metadata once the pool is ready"); + assert.equal(result.platformShare, 100); + assert.equal(result.receipts.length, 2); + } finally { + nostrClient.ensurePool = originalEnsurePool; + nostrClient.pool = originalPool; + nostrClient.poolPromise = originalPoolPromise; + __resetPlatformAddressCache(); + + if (previousOverride === undefined) { + delete globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__; + } else { + globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = previousOverride; + } + } +} + async function testStringFeeOverride() { const videoEvent = { id: "event-id", @@ -298,6 +405,7 @@ async function testWalletFailure() { } await testSplitMath(); +await testWaitsForPoolBeforePlatformLookup(); await testLnurlBounds(); await testMissingAddress(); await testWalletFailure();