Compare commits

...
4 Commits
Author SHA1 Message Date
Laan Tungir 2f45c78741 Lightweight 17375-only republish: skip 7375 rewrite, no deletions needed
Kind 17375 is a replaceable event (NIP-60, d-tag=''), so publishing a new
one automatically supersedes the old one on relays. The republish now only:
1. ensureDirectNutzapP2pk() - generate privkey if missing
2. Build NIP-60 tag-array payload
3. NIP-44 encrypt + sign + publish single 17375 event
That's 1 signing round-trip instead of 100+.
Reduced timeout from 120s back to 30s.
Bumped WORKER_REVISION to nip60-lightweight-republish-1.
2026-06-25 14:05:33 -04:00
Laan Tungir 6d41680297 Increase walletRepublish timeout to 120s for large wallets
The 45s timeout was too short for wallets with many old 7375 token events —
each deletion requires a signing round-trip through the nos2x extension.
2026-06-25 14:01:01 -04:00
Laan Tungir b2a50376df Ensure nutzap privkey is always included in kind 17375 republish
- Added ensureDirectNutzapP2pk() call in publishDirectProofs before building wallet payload
- This auto-generates a privkey if one wasn't hydrated from the old 17375 event
- Bumped WORKER_REVISION to nip60-privkey-ensure-1
2026-06-25 13:48:34 -04:00
Laan Tungir 3c4be89ead Add Republish Wallet button to force kind 17375 republish in NIP-60 format
- Added walletRepublish worker handler that calls publishDirectProofs()
- Added walletRepublish() to init-ndk.mjs
- Added republishWallet() to cashu-wallet.mjs controller
- Added 'Republish Wallet (kind 17375)' button to cashu.html sidenav
- Bumped WORKER_REVISION to nip60-republish-1 and cashu-wallet.mjs cache-bust
2026-06-25 13:39:32 -04:00
5 changed files with 144 additions and 6 deletions
+19 -1
View File
@@ -637,6 +637,7 @@
<button id="btnCheckProofs" class="btn cashuBtn" type="button" style="margin-bottom: 10px;">Check Proofs with Mints</button>
<div id="divCheckProofsResult">No proof check run yet.</div>
<button id="btnRefreshWallet" class="btn cashuBtn" type="button">Refresh Wallet State</button>
<button id="btnRepublishWallet" class="btn cashuBtn" type="button" style="margin-top: 5px;">Republish Wallet (kind 17375)</button>
</div>
<div id="divZapsSection" class="sidenavSection">
@@ -728,7 +729,7 @@
import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs';
import { initAiSectionWithLocalConfig } from './js/ai-ui.mjs';
import { createCashuWalletController } from './js/cashu-wallet.mjs?v=mint-discovery-3';
import { createCashuWalletController } from './js/cashu-wallet.mjs?v=nip60-republish-1';
const versionInfo = await getVersion();
const VERSION = versionInfo.VERSION;
@@ -2348,6 +2349,23 @@
}
});
const btnRepublishWallet = document.getElementById('btnRepublishWallet');
btnRepublishWallet?.addEventListener('click', async () => {
try {
if (!walletController?.hasWallet?.()) {
setStatus('No wallet to republish', true);
return;
}
setStatus('Republishing wallet event (kind 17375)...');
await walletController.republishWallet();
await refreshAllWalletViews();
setStatus('Wallet republished successfully');
} catch (error) {
console.error('[cashu.html] Republish wallet failed:', error);
setStatus(error.message || 'Republish failed', true);
}
});
btnSaveZapsSettings?.addEventListener('click', async () => {
try {
const amount = Number(inpZapDefaultAmount?.value || 0);
+12
View File
@@ -15,6 +15,7 @@ import {
walletRemoveMint,
walletGetTransactions,
walletShutdown,
walletRepublish,
walletPublishMintList,
walletFetchMintList,
onWalletStatus,
@@ -397,6 +398,16 @@ export async function createCashuWalletController({ pubkey, relayUrls = [] } = {
}
}
async function republishWallet() {
await ensureWallet();
const result = await walletRepublish();
if (Array.isArray(result?.mints)) {
mintsCache = uniqueMints(result.mints);
}
applyBalancePayload(result || {});
return { mints: [...mintsCache] };
}
cleanupFns.push(onWalletStatus((payload) => {
if (typeof payload?.hasWallet === 'boolean') {
hasWalletCache = payload.hasWallet;
@@ -431,6 +442,7 @@ export async function createCashuWalletController({ pubkey, relayUrls = [] } = {
waitForDeposit,
getTransactionHistory,
publishMintList,
republishWallet,
fetchRecipientMintList,
onWalletEvent,
subscribeTransactions,
+5 -1
View File
@@ -184,7 +184,7 @@ export async function initNDKPage(options = {}) {
// All pages in www/ directory, so worker path is always the same.
// Include explicit worker revision token so updated SharedWorker code is guaranteed to restart.
const WORKER_REVISION = 'nip60-tagarray-fix-1';
const WORKER_REVISION = 'nip60-lightweight-republish-1';
const workerPath = `./ndk-worker.js?v=${encodeURIComponent(VERSION)}&wr=${encodeURIComponent(WORKER_REVISION)}`;
// Initialize NDK SharedWorker (shared across all tabs/pages)
@@ -1425,6 +1425,10 @@ export function walletShutdown() {
return sendWorkerRequest('walletShutdown', {}, 10000, 'walletShutdown timeout');
}
export function walletRepublish() {
return sendWorkerRequest('walletRepublish', {}, 30000, 'walletRepublish timeout');
}
export function walletPublishMintList(relays = [], receiveMints = []) {
return sendWorkerRequest('walletPublishMintList', { relays, receiveMints }, 25000, 'walletPublishMintList timeout');
}
+3 -3
View File
@@ -1,5 +1,5 @@
{
"VERSION": "v0.7.20",
"VERSION_NUMBER": "0.7.20",
"BUILD_DATE": "2026-06-25T17:34:24.155Z"
"VERSION": "v0.7.24",
"VERSION_NUMBER": "0.7.24",
"BUILD_DATE": "2026-06-25T18:05:33.223Z"
}
+105 -1
View File
@@ -2843,6 +2843,15 @@ async function publishDirectProofs(debugContext = null) {
}
log('delete old 7375 done');
// Ensure a nutzap privkey exists before building the wallet payload.
// If the user's old 17375 didn't have one (or hydration failed),
// this auto-generates one so it's always included in the republish.
try {
ensureDirectNutzapP2pk();
} catch (err) {
log('ensureDirectNutzapP2pk failed during publish', { error: err?.message || String(err) });
}
// NIP-60 standard: public "mint" tags are visible on the event;
// the privkey lives in the encrypted content as a "privkey" tag.
// The "pubkey" tag does NOT belong on kind 17375 (it belongs on
@@ -4304,6 +4313,97 @@ async function handleWalletCheckProofs(requestId, port) {
}
}
/**
* Lightweight republish of ONLY the kind 17375 wallet event.
*
* Kind 17375 is a replaceable event (NIP-60, d-tag = ""), so publishing a
* new one automatically supersedes the old one on relays — no NIP-09
* deletions needed. This skips the heavy publishDirectProofs() which
* rewrites all 7375 token events too (100+ signing round-trips).
*
* Just: ensure privkey → build tag-array payload → encrypt → sign → publish.
*/
async function handleWalletRepublish(requestId, port) {
try {
if (!ndk || !currentPubkey) {
port.postMessage({ type: 'response', requestId, error: 'NDK not ready' });
return;
}
await ensureDirectWalletLoaded();
// Ensure a nutzap privkey exists (auto-generate if missing)
try {
ensureDirectNutzapP2pk();
} catch (err) {
console.warn('[Worker] handleWalletRepublish: ensureDirectNutzapP2pk failed:', err?.message || err);
}
const NDKEvent = getNDKEventCtor();
if (!NDKEvent) {
port.postMessage({ type: 'response', requestId, error: 'NDKEvent constructor not available' });
return;
}
// Build NIP-60 tag-array payload: [["mint",url],["privkey",hex]]
const mintTags = getDirectWalletMints().map((mintUrl) => ['mint', mintUrl]);
const walletPayloadObj = [
...getDirectWalletMints().map((url) => ['mint', url]),
...(directNutzapPrivateKeyHex ? [['privkey', directNutzapPrivateKeyHex]] : [])
];
const walletPayload = JSON.stringify(walletPayloadObj);
// NIP-44 encrypt the payload
const encryptedWalletResp = await messageSigner.requestFromPage('nip44Encrypt', {
recipientPubkey: currentPubkey,
plaintext: walletPayload
});
const encryptedWallet = typeof encryptedWalletResp === 'string' ? encryptedWalletResp : encryptedWalletResp?.ciphertext;
if (!encryptedWallet) {
port.postMessage({ type: 'response', requestId, error: 'Failed to encrypt wallet payload' });
return;
}
// Build and publish the new 17375 event (replaceable — supersedes old one)
const walletEvent = new NDKEvent(ndk, {
kind: 17375,
tags: mintTags,
content: encryptedWallet,
created_at: Math.floor(Date.now() / 1000)
});
await signEventWithMessageSigner(walletEvent);
const relaySet = await walletEvent.publish();
if (relaySet && relaySet.size > 0) {
for (const relay of relaySet) {
trackRelayWrite(relay.url);
}
}
console.log('[Worker] handleWalletRepublish: 17375 published', {
relayCount: relaySet ? relaySet.size : 0,
mintCount: mintTags.length,
hasPrivkey: Boolean(directNutzapPrivateKeyHex)
});
const payload = getWalletBalancePayload();
port.postMessage({
type: 'response',
requestId,
data: {
success: true,
mints: getDirectWalletMints(),
relayCount: relaySet ? relaySet.size : 0,
...payload
}
});
} catch (error) {
console.warn('[Worker] handleWalletRepublish failed:', error?.message || error);
port.postMessage({ type: 'response', requestId, error: error.message || 'Republish failed' });
}
}
function handleWalletShutdown(requestId, port) {
try {
clearWalletListeners();
@@ -6500,7 +6600,11 @@ self.onconnect = (event) => {
case 'walletCheckProofs':
await handleWalletCheckProofs(requestId, port);
break;
case 'walletRepublish':
await handleWalletRepublish(requestId, port);
break;
default:
console.warn('[Worker] Unknown message type:', type);
}