Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dcfc7c3e3 | ||
|
|
07f5f39f35 |
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.7.66",
|
||||
"VERSION_NUMBER": "0.7.66",
|
||||
"BUILD_DATE": "2026-06-30T11:47:22.403Z"
|
||||
"VERSION": "v0.7.68",
|
||||
"VERSION_NUMBER": "0.7.68",
|
||||
"BUILD_DATE": "2026-06-30T13:15:02.809Z"
|
||||
}
|
||||
|
||||
+98
-20
@@ -4568,6 +4568,16 @@ async function handleWalletCreateDeposit(requestId, amount, mint, port) {
|
||||
}
|
||||
|
||||
async function handleWalletPayInvoice(requestId, invoice, mint, port) {
|
||||
const startedAt = Date.now();
|
||||
const traceId = `walletPayInvoice:${requestId}`;
|
||||
const log = (phase, extra = null) => {
|
||||
if (extra === null) {
|
||||
console.log(`[Worker] ${traceId} +${Date.now() - startedAt}ms ${phase}`);
|
||||
} else {
|
||||
console.log(`[Worker] ${traceId} +${Date.now() - startedAt}ms ${phase}`, extra);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await ensureDirectWalletLoaded();
|
||||
|
||||
@@ -4580,6 +4590,11 @@ async function handleWalletPayInvoice(requestId, invoice, mint, port) {
|
||||
? [requestedMint, ...knownMints.filter((m) => m !== requestedMint)]
|
||||
: [...knownMints].sort((a, b) => getProofTotal(directProofStore[b]) - getProofTotal(directProofStore[a]));
|
||||
|
||||
log('mint-selection', {
|
||||
requestedMint: requestedMint || null,
|
||||
orderedMints: orderedMints.map((m) => ({ mint: m, proofBalance: getProofTotal(directProofStore[m]) }))
|
||||
});
|
||||
|
||||
let payResult = null;
|
||||
let usedMint = null;
|
||||
let paidAmountSats = 0;
|
||||
@@ -4588,7 +4603,11 @@ async function handleWalletPayInvoice(requestId, invoice, mint, port) {
|
||||
for (const mintUrl of orderedMints) {
|
||||
try {
|
||||
const proofs = Array.isArray(directProofStore[mintUrl]) ? directProofStore[mintUrl] : [];
|
||||
const proofBalance = getProofTotal(proofs);
|
||||
log('mint attempt:start', { mintUrl, proofBalance, proofCount: proofs.length });
|
||||
|
||||
if (proofs.length === 0) {
|
||||
log('mint attempt:no-proofs', { mintUrl });
|
||||
attemptErrors.push({ mint: mintUrl, message: 'No proofs available for mint' });
|
||||
continue;
|
||||
}
|
||||
@@ -4598,13 +4617,16 @@ async function handleWalletPayInvoice(requestId, invoice, mint, port) {
|
||||
const quoteAmount = Number(meltQuote?.amount || 0);
|
||||
const quoteFeeReserve = Number(meltQuote?.fee_reserve || 0);
|
||||
const required = quoteAmount + quoteFeeReserve;
|
||||
log('mint attempt:quote', { mintUrl, quoteAmount, quoteFeeReserve, required });
|
||||
|
||||
if (!Number.isFinite(required) || required <= 0) {
|
||||
log('mint attempt:invalid-quote', { mintUrl, required });
|
||||
attemptErrors.push({ mint: mintUrl, message: 'Invalid melt quote amount' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const proofBalance = getProofTotal(proofs);
|
||||
if (proofBalance < required) {
|
||||
log('mint attempt:insufficient', { mintUrl, proofBalance, required });
|
||||
attemptErrors.push({ mint: mintUrl, message: `Insufficient funds on mint (${proofBalance} sats)` });
|
||||
continue;
|
||||
}
|
||||
@@ -4612,6 +4634,8 @@ async function handleWalletPayInvoice(requestId, invoice, mint, port) {
|
||||
const sendResult = await wallet.send(required, proofs);
|
||||
const keep = Array.isArray(sendResult?.keep) ? sendResult.keep : [];
|
||||
const sendProofs = Array.isArray(sendResult?.send) ? sendResult.send : [];
|
||||
log('mint attempt:send-split', { mintUrl, keepCount: keep.length, sendCount: sendProofs.length });
|
||||
|
||||
const meltResult = await wallet.meltProofs(meltQuote, sendProofs);
|
||||
const change = Array.isArray(meltResult?.change) ? meltResult.change : [];
|
||||
upsertMintProofs(mintUrl, [...keep, ...change]);
|
||||
@@ -4622,19 +4646,25 @@ async function handleWalletPayInvoice(requestId, invoice, mint, port) {
|
||||
? quoteAmount
|
||||
: spentAmount;
|
||||
|
||||
log('mint attempt:success', { mintUrl, paidAmountSats, changeAmount, preimage: meltResult?.preimage || null });
|
||||
|
||||
usedMint = mintUrl;
|
||||
payResult = meltResult;
|
||||
break;
|
||||
} catch (error) {
|
||||
log('mint attempt:error', { mintUrl, message: error?.message || String(error) });
|
||||
attemptErrors.push({ mint: mintUrl, message: error?.message || String(error) });
|
||||
}
|
||||
}
|
||||
|
||||
if (!payResult || !usedMint) {
|
||||
const detail = attemptErrors.map((entry) => `${entry.mint || 'unknown'}: ${entry.message}`).join(' | ');
|
||||
log('all-mints-failed', { attemptErrors });
|
||||
throw new Error(`Failed to pay invoice. ${detail || 'No mint could complete melt'}`);
|
||||
}
|
||||
|
||||
log('paid', { usedMint, paidAmountSats, preimage: payResult?.preimage || null });
|
||||
|
||||
const tx = appendDirectTransaction({
|
||||
type: 'pay',
|
||||
amount: Number.isFinite(paidAmountSats) && paidAmountSats > 0
|
||||
@@ -4680,6 +4710,7 @@ async function handleWalletPayInvoice(requestId, invoice, mint, port) {
|
||||
}
|
||||
})();
|
||||
} catch (error) {
|
||||
log('error', { message: error?.message || String(error), stack: error?.stack || null });
|
||||
port.postMessage({ type: 'response', requestId, error: error.message });
|
||||
}
|
||||
}
|
||||
@@ -6183,7 +6214,8 @@ async function handlePublish(requestId, event, port) {
|
||||
// broadcast() so footers can show "📡 42/150 relays · relay.example.com".
|
||||
const publishSessionId = `pub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
const publishedSoFar = new Set();
|
||||
const failedSoFar = new Set();
|
||||
const failedSoFar = new Set(); // explicit rejections (auth-required, blocked, etc.)
|
||||
const timedOutSoFar = new Set(); // timeouts — NOT skip-marked (could be slow connect)
|
||||
const totalTarget = targetRelaySet && targetRelaySet.relays
|
||||
? targetRelaySet.relays.size
|
||||
: (targetRelaySet ? (targetRelaySet.size || 0) : 0);
|
||||
@@ -6220,32 +6252,61 @@ async function handlePublish(requestId, event, port) {
|
||||
ndkEvent.on('relay:publish:failed', (relay, err) => {
|
||||
const url = relay?.url || String(relay || '');
|
||||
if (!url) return;
|
||||
failedSoFar.add(url);
|
||||
const errMsg = err?.message || String(err) || '';
|
||||
// Distinguish timeouts from explicit rejections. Timeouts with
|
||||
// hundreds of relays often just mean the relay was slow to connect,
|
||||
// not that it's broken — so we track them separately and do NOT
|
||||
// skip-mark them. Only explicit rejections go into failedSoFar.
|
||||
const isTimeout = /timeout/i.test(errMsg);
|
||||
if (isTimeout) {
|
||||
timedOutSoFar.add(url);
|
||||
} else {
|
||||
failedSoFar.add(url);
|
||||
}
|
||||
broadcast({
|
||||
type: 'broadcastProgress',
|
||||
sessionId: publishSessionId,
|
||||
phase: 'progress',
|
||||
total: totalTarget,
|
||||
successful: publishedSoFar.size,
|
||||
failed: failedSoFar.size,
|
||||
failed: failedSoFar.size + timedOutSoFar.size,
|
||||
latestRelay: url,
|
||||
latestError: err?.message || String(err),
|
||||
latestError: errMsg,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Publish to relays (with broadcast relay set if broadcasting, else default outbox)
|
||||
const relaySet = await ndkEvent.publish(targetRelaySet);
|
||||
// Publish to relays (with broadcast relay set if broadcasting, else default outbox).
|
||||
// When broadcasting to many relays (potentially hundreds), use a much longer
|
||||
// timeout than NDK's default 4400ms — temporary relays need time to establish
|
||||
// WebSocket connections. Use 30s for broadcasts, default for normal publishes.
|
||||
// requiredRelayCount=1 so NDK doesn't throw if only 1 of 600 relays succeeds.
|
||||
const BROADCAST_TIMEOUT_MS = 30000;
|
||||
const publishTimeoutMs = isBroadcast ? BROADCAST_TIMEOUT_MS : undefined;
|
||||
const publishRequiredCount = isBroadcast ? 1 : undefined;
|
||||
|
||||
let relaySet;
|
||||
try {
|
||||
relaySet = await ndkEvent.publish(targetRelaySet, publishTimeoutMs, publishRequiredCount);
|
||||
} catch (publishError) {
|
||||
// NDK throws NDKPublishError if requiredRelayCount isn't met. With
|
||||
// requiredRelayCount=1 this shouldn't happen for broadcasts, but handle
|
||||
// it gracefully — the live progress listeners already captured whatever
|
||||
// succeeded. Log and continue so we still report results to the page.
|
||||
console.warn('[Worker] publish() threw (some relays may still have succeeded):', publishError?.message || publishError);
|
||||
relaySet = null;
|
||||
}
|
||||
|
||||
// Final broadcast progress message with complete results.
|
||||
if (isBroadcast && totalTarget > 0) {
|
||||
const totalFailed = failedSoFar.size + timedOutSoFar.size;
|
||||
broadcast({
|
||||
type: 'broadcastProgress',
|
||||
sessionId: publishSessionId,
|
||||
phase: 'done',
|
||||
total: totalTarget,
|
||||
successful: publishedSoFar.size,
|
||||
failed: failedSoFar.size,
|
||||
failed: totalFailed,
|
||||
latestRelay: null,
|
||||
});
|
||||
// Clean up listeners so the NDKEvent can be garbage-collected.
|
||||
@@ -6272,32 +6333,49 @@ async function handlePublish(requestId, event, port) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check which connected relays didn't accept it
|
||||
for (const relay of ndk.pool.relays.values()) {
|
||||
if (relay.status >= 5 && !relayResults.successful.includes(relay.url)) {
|
||||
relayResults.failed.push(relay.url);
|
||||
// For broadcast publishes, use the live-tracked sets from the
|
||||
// relay:published / relay:publish:failed events as the source of truth
|
||||
// (they cover temporary relays that may not be in ndk.pool). For normal
|
||||
// publishes, fall back to the pool-scan logic.
|
||||
if (isBroadcast) {
|
||||
relayResults.successful = Array.from(publishedSoFar);
|
||||
// Include both explicit rejections and timeouts in the failed list
|
||||
// for display purposes, but keep them distinguishable for skip-marking.
|
||||
relayResults.failed = [...Array.from(failedSoFar), ...Array.from(timedOutSoFar)];
|
||||
} else {
|
||||
// Check which connected relays didn't accept it
|
||||
for (const relay of ndk.pool.relays.values()) {
|
||||
if (relay.status >= 5 && !relayResults.successful.includes(relay.url)) {
|
||||
relayResults.failed.push(relay.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Worker] ✅ Published to relays:', relayResults.successful);
|
||||
console.log('[Worker] ❌ Failed relays:', relayResults.failed);
|
||||
console.log('[Worker] ✅ Published to relays:', relayResults.successful.length, relayResults.successful.slice(0, 10), relayResults.successful.length > 10 ? `... +${relayResults.successful.length - 10} more` : '');
|
||||
console.log('[Worker] ❌ Failed relays:', relayResults.failed.length);
|
||||
|
||||
// --- Failure marking for broadcast relays ---------------------------------
|
||||
// Persist skip-marks for broadcast relays that failed this publish so
|
||||
// future publishes skip them. Debounced via scheduleSkipMarkSave() so a
|
||||
// single post that fails on many relays produces one kind 10088 republish.
|
||||
// Persist skip-marks ONLY for broadcast relays that EXPLICITLY rejected
|
||||
// the publish (auth-required, blocked, invalid, etc.). We do NOT skip-mark
|
||||
// relays that timed out — with hundreds of relays, a timeout often means
|
||||
// the relay was slow to connect, not that it's broken. The relay:publish:failed
|
||||
// listener above separates timeouts (timedOutSoFar) from explicit rejections
|
||||
// (failedSoFar) by checking the error message for "timeout".
|
||||
if (isBroadcast) {
|
||||
const newSkips = relayResults.failed
|
||||
const newSkips = Array.from(failedSoFar)
|
||||
.filter(url => broadcastRelayUrls.has(url) && !skippedRelayUrls.has(url));
|
||||
if (newSkips.length > 0) {
|
||||
for (const url of newSkips) {
|
||||
skippedRelayUrls.set(url, {
|
||||
reason: 'publish-failed',
|
||||
reason: 'publish-rejected',
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
}
|
||||
scheduleSkipMarkSave();
|
||||
console.log(`[Worker] Skip-marked ${newSkips.length} failed broadcast relays`);
|
||||
console.log(`[Worker] Skip-marked ${newSkips.length} rejected broadcast relays ` +
|
||||
`(${timedOutSoFar.size} timeouts excluded from skip-marking)`);
|
||||
} else if (timedOutSoFar.size > 0) {
|
||||
console.log(`[Worker] ${timedOutSoFar.size} broadcast relays timed out (not skip-marked — will retry next publish)`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user