diff --git a/js/app.js b/js/app.js index 88a28b9f..95ebb19c 100644 --- a/js/app.js +++ b/js/app.js @@ -5910,6 +5910,69 @@ class Application { shares, shareTracker, }) { + const summarizeTracker = (tracker) => + Array.isArray(tracker) + ? tracker.map((entry) => ({ + type: entry?.type || "unknown", + status: entry?.status || "unknown", + amount: + Number.isFinite(entry?.amount) && entry.amount >= 0 + ? entry.amount + : undefined, + address: entry?.address || undefined, + hasPayment: Boolean(entry?.payment), + errorMessage: + typeof entry?.error?.message === "string" + ? entry.error.message + : entry?.error + ? String(entry.error) + : undefined, + })) + : undefined; + const logZapError = (stage, details, error) => { + const summary = { + stage, + shareType: details?.shareType, + address: details?.address, + amount: + Number.isFinite(details?.amount) && details.amount >= 0 + ? details.amount + : undefined, + overrideFee: + typeof details?.overrideFee === "number" + ? details.overrideFee + : undefined, + commentLength: + typeof details?.comment === "string" + ? details.comment.length + : undefined, + wallet: + details?.walletSettings + ? { + hasUri: Boolean(details.walletSettings.nwcUri), + type: + details.walletSettings.type || + details.walletSettings.name || + details.walletSettings.client || + undefined, + } + : undefined, + shares: details?.context?.shares + ? { + total: details.context.shares.total, + creator: details.context.shares.creatorShare, + platform: details.context.shares.platformShare, + } + : undefined, + tracker: summarizeTracker(details?.tracker), + retryAttempt: + Number.isInteger(details?.retryAttempt) && details.retryAttempt >= 0 + ? details.retryAttempt + : undefined, + }; + console.error("[zap] Modal zap failure", summary, error); + }; + const lightningAddress = this.currentVideo?.lightningAddress || ""; const creatorKey = normalizeLightningAddressKey( creatorEntry?.address || lightningAddress @@ -5978,7 +6041,22 @@ class Application { requestInvoice, }, wallet: { - ensureWallet: (options) => ensureWalletFn(options), + ensureWallet: async (options) => { + try { + return await ensureWalletFn(options); + } catch (error) { + logZapError( + "wallet.ensureWallet", + { + tracker: shareTracker, + context: { shares }, + walletSettings: options?.settings, + }, + error + ); + throw error; + } + }, sendPayment: async (bolt11, params) => { try { const payment = await sendPaymentFn(bolt11, params); @@ -6020,6 +6098,17 @@ class Application { error, }); } + logZapError( + "wallet.sendPayment", + { + shareType: activeShare || "unknown", + amount, + address, + tracker: shareTracker, + context: { shares }, + }, + error + ); throw error; } finally { activeShare = null; @@ -6126,10 +6215,29 @@ class Application { return null; } - const context = await this.prepareModalLightningContext({ - amount, - overrideFee, - }); + let context; + try { + context = await this.prepareModalLightningContext({ + amount, + overrideFee, + }); + } catch (error) { + console.error( + "[zap] Modal lightning context failed", + { + amount, + overrideFee, + commentLength: typeof comment === "string" ? comment.length : undefined, + wallet: { + hasUri: Boolean(settings?.nwcUri), + type: + settings?.type || settings?.name || settings?.client || undefined, + }, + }, + error + ); + throw error; + } const shareTracker = []; const dependencies = this.createModalZapDependencies({ ...context, @@ -6159,6 +6267,39 @@ class Application { if (Array.isArray(shareTracker) && shareTracker.length) { error.__zapShareTracker = shareTracker; } + console.error( + "[zap] splitAndZap failed", + { + amount, + overrideFee, + commentLength: typeof comment === "string" ? comment.length : undefined, + wallet: { + hasUri: Boolean(settings?.nwcUri), + type: + settings?.type || settings?.name || settings?.client || undefined, + }, + shares: { + total: context?.shares?.total, + creator: context?.shares?.creatorShare, + platform: context?.shares?.platformShare, + }, + tracker: Array.isArray(shareTracker) + ? shareTracker.map((entry) => ({ + type: entry?.type || "unknown", + status: entry?.status || "unknown", + amount: entry?.amount, + address: entry?.address, + errorMessage: + typeof entry?.error?.message === "string" + ? entry.error.message + : entry?.error + ? String(entry.error) + : undefined, + })) + : undefined, + }, + error + ); throw error; } finally { if (hasGlobal && typeof overrideFee === "number" && Number.isFinite(overrideFee)) { @@ -6224,6 +6365,31 @@ class Application { const tracker = Array.isArray(error?.__zapShareTracker) ? error.__zapShareTracker : []; + console.error( + "[zap] Modal zap retry failed", + { + shareType: share?.type || "unknown", + amount: share?.amount, + address: share?.address, + commentLength: + typeof (retryState?.comment || comment) === "string" + ? (retryState?.comment || comment).length + : undefined, + tracker: tracker.map((entry) => ({ + type: entry?.type || "unknown", + status: entry?.status || "unknown", + amount: entry?.amount, + address: entry?.address, + errorMessage: + typeof entry?.error?.message === "string" + ? entry.error.message + : entry?.error + ? String(entry.error) + : undefined, + })), + }, + error + ); if (tracker.length) { aggregatedTracker.push(...tracker); } @@ -6358,6 +6524,37 @@ class Application { const tracker = Array.isArray(error?.__zapShareTracker) ? error.__zapShareTracker : []; + console.error( + "[zap] Modal zap attempt failed", + { + amount, + commentLength: typeof comment === "string" ? comment.length : undefined, + wallet: { + hasUri: Boolean(walletSettings?.nwcUri), + type: + walletSettings?.type || + walletSettings?.name || + walletSettings?.client || + undefined, + }, + tracker: tracker.map((entry) => ({ + type: entry?.type || "unknown", + status: entry?.status || "unknown", + amount: entry?.amount, + address: entry?.address, + errorMessage: + typeof entry?.error?.message === "string" + ? entry.error.message + : entry?.error + ? String(entry.error) + : undefined, + })), + retryPending: Array.isArray(this.modalZapRetryState?.shares) + ? this.modalZapRetryState.shares.length + : 0, + }, + error + ); if (tracker.length) { this.videoModal?.renderZapReceipts(tracker, { partial: true }); } diff --git a/js/channelProfile.js b/js/channelProfile.js index 2b274926..e4a95056 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -38,6 +38,67 @@ const getApp = () => getApplication(); let currentChannelHex = null; let currentChannelNpub = null; let currentChannelLightningAddress = ""; + +const summarizeZapTracker = (tracker) => + Array.isArray(tracker) + ? tracker.map((entry) => ({ + type: entry?.type || "unknown", + status: entry?.status || "unknown", + amount: + Number.isFinite(entry?.amount) && entry.amount >= 0 + ? entry.amount + : undefined, + address: entry?.address || undefined, + hasPayment: Boolean(entry?.payment), + errorMessage: + typeof entry?.error?.message === "string" + ? entry.error.message + : entry?.error + ? String(entry.error) + : undefined, + })) + : undefined; + +function logZapError(stage, details = {}, error) { + const summary = { + stage, + shareType: details?.shareType, + address: details?.address, + amount: + Number.isFinite(details?.amount) && details.amount >= 0 + ? details.amount + : undefined, + overrideFee: + typeof details?.overrideFee === "number" + ? details.overrideFee + : undefined, + commentLength: + typeof details?.comment === "string" ? details.comment.length : undefined, + wallet: details?.walletSettings + ? { + hasUri: Boolean(details.walletSettings.nwcUri), + type: + details.walletSettings.type || + details.walletSettings.name || + details.walletSettings.client || + undefined, + } + : undefined, + shares: details?.context?.shares + ? { + total: details.context.shares.total, + creator: details.context.shares.creatorShare, + platform: details.context.shares.platformShare, + } + : undefined, + tracker: summarizeZapTracker(details?.tracker), + retryAttempt: + Number.isInteger(details?.retryAttempt) && details.retryAttempt >= 0 + ? details.retryAttempt + : undefined, + }; + console.error("[zap] Channel zap failure", summary, error); +} let currentChannelProfileEvent = null; let cachedZapButton = null; @@ -477,7 +538,22 @@ function createZapDependencies({ creatorEntry, platformEntry, shares, shareTrack requestInvoice, }, wallet: { - ensureWallet: (options) => ensureWallet(options), + ensureWallet: async (options) => { + try { + return await ensureWallet(options); + } catch (error) { + logZapError( + "wallet.ensureWallet", + { + tracker: shareTracker, + context: { shares }, + walletSettings: options?.settings, + }, + error + ); + throw error; + } + }, sendPayment: async (bolt11, params) => { try { const payment = await sendPayment(bolt11, params); @@ -519,6 +595,17 @@ function createZapDependencies({ creatorEntry, platformEntry, shares, shareTrack error, }); } + logZapError( + "wallet.sendPayment", + { + shareType: activeShare || "unknown", + amount, + address, + tracker: shareTracker, + context: { shares }, + }, + error + ); throw error; } finally { activeShare = null; @@ -569,7 +656,21 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) { return null; } - const context = await prepareLightningContext({ amount, overrideFee }); + let context; + try { + context = await prepareLightningContext({ amount, overrideFee }); + } catch (error) { + logZapError( + "prepareLightningContext", + { + amount, + overrideFee, + walletSettings: settings, + }, + error + ); + throw error; + } const shareTracker = []; const dependencies = createZapDependencies({ ...context, @@ -598,6 +699,17 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) { if (Array.isArray(shareTracker) && shareTracker.length) { error.__zapShareTracker = shareTracker; } + logZapError( + "splitAndZap", + { + amount, + overrideFee, + walletSettings: settings, + context, + tracker: shareTracker, + }, + error + ); throw error; } finally { if (hasGlobal && typeof overrideFee === "number" && Number.isFinite(overrideFee)) { @@ -654,6 +766,17 @@ async function executePendingRetry({ walletSettings }) { const tracker = Array.isArray(error?.__zapShareTracker) ? error.__zapShareTracker : []; + logZapError( + "retry.share", + { + shareType: share?.type || "unknown", + amount: share?.amount, + address: share?.address, + walletSettings, + tracker, + }, + error + ); if (tracker.length) { aggregatedTracker.push(...tracker); } @@ -714,6 +837,7 @@ async function handleZapButtonClick(event) { zapButton.classList.add("opacity-50", "pointer-events-none"); amountInput.disabled = true; + let attemptedAmount = null; try { if (pendingZapRetry?.shares?.length) { await executePendingRetry({ walletSettings }); @@ -721,6 +845,7 @@ async function handleZapButtonClick(event) { } const amount = Math.max(0, Math.round(Number(amountInput.value || 0))); + attemptedAmount = amount; if (!amount) { const message = "Enter a zap amount greater than zero."; setZapStatus(message, "error"); @@ -753,6 +878,19 @@ async function handleZapButtonClick(event) { const tracker = Array.isArray(error?.__zapShareTracker) ? error.__zapShareTracker : []; + logZapError( + "handleZapButtonClick", + { + amount: attemptedAmount, + walletSettings, + tracker, + retryAttempt: + Array.isArray(pendingZapRetry?.shares) && pendingZapRetry.shares.length + ? pendingZapRetry.shares.length + : undefined, + }, + error + ); if (tracker.length) { renderZapReceipts(tracker, { partial: true }); } diff --git a/js/payments/nwcClient.js b/js/payments/nwcClient.js index a3b85588..ff5a5ff2 100644 --- a/js/payments/nwcClient.js +++ b/js/payments/nwcClient.js @@ -42,6 +42,23 @@ function assertNostrTools(methods = []) { const isCallable = typeof candidate === "function"; const isNamespace = candidate && typeof candidate === "object"; if (!isCallable && !isNamespace) { + try { + const available = Array.isArray(Object.keys(tools)) + ? Object.keys(tools) + : []; + console.error( + "[nwcClient] Required NostrTools capability is missing.", + { + missingMethod: method, + availableMethods: available, + } + ); + } catch (loggingError) { + console.error( + "[nwcClient] Failed to enumerate available NostrTools methods.", + loggingError + ); + } throw new Error(`NostrTools.${method} is unavailable.`); } }