mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
Harden NWC split-zap timeout recovery
This commit is contained in:
1 parent
0729bce549
commit
bc35dd2cff
2 files changed
+66
-14
No files matched your search
+43
-14
@@ -6660,6 +6660,7 @@
|
||||
// localStorage so the user only pastes the connection string once.
|
||||
const NWC_STORAGE_KEY = 'casewrap-nwc-connection';
|
||||
const NWC_DEFAULT_TIMEOUT_MS = 25000;
|
||||
const NWC_PAY_INVOICE_TIMEOUT_MS = 60000;
|
||||
const nwc = (() => {
|
||||
let _conn = null; // { servicePubkey, relays, secret, clientPubkey, lud16 } once connected
|
||||
let _ws = null; // active WebSocket
|
||||
@@ -6886,12 +6887,14 @@
|
||||
_emit();
|
||||
}
|
||||
|
||||
async function payInvoice(bolt11) {
|
||||
return _request('pay_invoice', { invoice: bolt11 });
|
||||
async function payInvoice(bolt11, opts = {}) {
|
||||
return _request('pay_invoice', { invoice: bolt11 }, { timeoutMs: opts.timeoutMs || NWC_PAY_INVOICE_TIMEOUT_MS });
|
||||
}
|
||||
|
||||
async function lookupInvoice(paymentHash) {
|
||||
return _request('lookup_invoice', { payment_hash: paymentHash });
|
||||
async function lookupInvoice(invoiceOrHash) {
|
||||
const value = String(invoiceOrHash || '').trim();
|
||||
const params = /^ln/i.test(value) ? { invoice: value } : { payment_hash: value };
|
||||
return _request('lookup_invoice', params);
|
||||
}
|
||||
|
||||
async function getInfoFresh() {
|
||||
@@ -7314,10 +7317,27 @@
|
||||
}
|
||||
}
|
||||
onStatus(`Paying ${label}…`);
|
||||
const result = await nwc.payInvoice(leg.bolt11);
|
||||
if (!result?.preimage) throw new Error(`${label} payment returned no preimage`);
|
||||
partial[side] = { ...leg, preimage: result.preimage };
|
||||
savePartialUnlock(eventId, partial);
|
||||
try {
|
||||
const result = await nwc.payInvoice(leg.bolt11);
|
||||
if (!result?.preimage) throw new Error(`${label} payment returned no preimage`);
|
||||
partial[side] = { ...leg, preimage: result.preimage };
|
||||
savePartialUnlock(eventId, partial);
|
||||
} catch (err) {
|
||||
if (err?.code === 'TIMEOUT' && leg?.bolt11) {
|
||||
onStatus(`Checking whether ${label} settled…`);
|
||||
try {
|
||||
const postTimeoutLookup = await nwc.lookupInvoice(leg.bolt11);
|
||||
if (postTimeoutLookup?.preimage) {
|
||||
partial[side] = { ...leg, preimage: postTimeoutLookup.preimage };
|
||||
savePartialUnlock(eventId, partial);
|
||||
return;
|
||||
}
|
||||
} catch (lookupErr) {
|
||||
console.warn('[nwc] post-timeout lookup_invoice failed', lookupErr);
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -7699,11 +7719,6 @@
|
||||
const resumeNote = partialSummary.status !== 'none'
|
||||
? `\n\nResume detected: ${partialSummary.confirmed} payment leg${partialSummary.confirmed === 1 ? '' : 's'} confirmed, ${partialSummary.missing} left. Artstr will reuse cached invoices/preimages and only pay missing legs.`
|
||||
: '';
|
||||
const authorProfile = _feedCache.profiles?.[row.e?.pubkey] || null;
|
||||
const creatorMeta = {
|
||||
lud16: authorProfile?.lud16,
|
||||
lud06: authorProfile?.lud06,
|
||||
};
|
||||
const ok = confirm(
|
||||
`${partialSummary.status === 'none' ? 'Unlock' : 'Resume unlock for'} "${row.title || '(untitled)'}" for ${min} sats?\n\n` +
|
||||
`• Creator (${shortPubkey(row.e.pubkey)}): ${creatorSats} sats\n` +
|
||||
@@ -7718,6 +7733,17 @@
|
||||
statusToast = toast(msg, { duration: 60000 });
|
||||
};
|
||||
try {
|
||||
let authorProfile = _feedCache.profiles?.[row.e?.pubkey]
|
||||
|| state.community?.profiles?.[row.e?.pubkey]
|
||||
|| null;
|
||||
if (!resolveLnurlPayUrl(authorProfile)) {
|
||||
onStatus('Checking creator Lightning address…');
|
||||
authorProfile = await ensureProfilePageData(row.e.pubkey, row.e.tags?.filter(t => t[0] === 'r' && t[1]).map(t => t[1]) || []);
|
||||
}
|
||||
const creatorMeta = {
|
||||
lud16: authorProfile?.lud16,
|
||||
lud06: authorProfile?.lud06,
|
||||
};
|
||||
const result = await payZapSplit({
|
||||
creatorPubkey: row.e.pubkey,
|
||||
creatorMetadata: creatorMeta,
|
||||
@@ -7843,7 +7869,10 @@
|
||||
}
|
||||
} catch (err) {
|
||||
if (statusToast?.dismiss) statusToast.dismiss();
|
||||
const msg = err?.message || 'Unlock failed.';
|
||||
let msg = err?.message || 'Unlock failed.';
|
||||
if (err?.code === 'TIMEOUT') {
|
||||
msg += ' If one payment leg completed, click Resume payment; Artstr will check cached invoices before paying again.';
|
||||
}
|
||||
const stage = err?.stage ? ` (${err.stage} leg)` : '';
|
||||
toastError(`${msg}${stage}`);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,29 @@ test('index page exposes partial-payment resume state without discarding paid le
|
||||
assert.match(html, /const stalePartialCleared = clearStalePartialUnlocks\(\)/);
|
||||
});
|
||||
|
||||
test('index page recovers NWC split-zap legs after wallet timeouts', async () => {
|
||||
const html = await indexHtml();
|
||||
|
||||
assert.match(html, /const NWC_PAY_INVOICE_TIMEOUT_MS = 60000/);
|
||||
assert.match(html, /return _request\('pay_invoice', \{ invoice: bolt11 \}, \{ timeoutMs: opts\.timeoutMs \|\| NWC_PAY_INVOICE_TIMEOUT_MS \}\)/);
|
||||
assert.ok(html.includes('const params = /^ln/i.test(value) ? { invoice: value } : { payment_hash: value };'));
|
||||
assert.match(html, /const postTimeoutLookup = await nwc\.lookupInvoice\(leg\.bolt11\)/);
|
||||
assert.match(html, /if \(postTimeoutLookup\?\.preimage\)/);
|
||||
assert.match(html, /preimage: postTimeoutLookup\.preimage/);
|
||||
assert.match(html, /If one payment leg completed, click Resume payment/);
|
||||
});
|
||||
|
||||
test('index page refreshes creator Lightning metadata before premium unlock payment', async () => {
|
||||
const html = await indexHtml();
|
||||
|
||||
assert.ok(html.includes('let authorProfile = _feedCache.profiles?.[row.e?.pubkey]'));
|
||||
assert.ok(html.includes('|| state.community?.profiles?.[row.e?.pubkey]'));
|
||||
assert.match(html, /if \(!resolveLnurlPayUrl\(authorProfile\)\)/);
|
||||
assert.match(html, /authorProfile = await ensureProfilePageData\(row\.e\.pubkey/);
|
||||
assert.match(html, /Checking creator Lightning address/);
|
||||
assert.match(html, /lud06: authorProfile\?\.lud06/);
|
||||
});
|
||||
|
||||
test('index page migrates legacy inline vault purchases to private copies', async () => {
|
||||
const html = await indexHtml();
|
||||
|
||||
|
||||
Reference in new issue
Block a user