aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-26 08:35:54 -0700
committerAdam Soltys <[email protected]>2026-10-26 08:35:54 -0700
commitef1739e2de914f0a02990979f13057cef77c9a44 (patch)
tree3aa55a4bf96a13ebf208b549996d549043e4eb9c
parent85399e5f2a303155a9b437712aed08d62fbc8776 (diff)
downloadcoinosv3-ef1739e2de914f0a02990979f13057cef77c9a44.tar.gz
coinosv3-ef1739e2de914f0a02990979f13057cef77c9a44.zip
A released placeholder address on your profile page shows as the claimed one on the Lightning line too, and Save in the profile editor repoints it in the kind 0 (a same-domain name that no longer answers); the background heal steps aside for an interactive signer (Amber) instead of waiting forever in the kind-0 queue
Jarryd's page still showed [email protected] over ⚡ [email protected]: the header only substituted the NIP-05 line, and his kind 0 was never republished — the heal of ce96364 needs a background signature, which an Amber sign-in can't give, and Save left a lud16 that wasn't the old address alone. Co-Authored-By: Claude Fable 5.1 <[email protected]>
-rw-r--r--src/features/messages.js23
-rw-r--r--tools/profile-publish-race-test.js18
2 files changed, 37 insertions, 4 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 6f010b2..6c51d93 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -12767,8 +12767,15 @@ export function messagesFeature(ctx) {
// when empty; move them when they pointed at our (old) address. A
// deliberately foreign lud16/nip05 is the user's business.
if (addr) {
- for (const k of ['lud16', 'nip05'])
- if (!merged[k] || merged[k] === oldAddr) merged[k] = addr;
+ const domain = addr.split('@')[1];
+ for (const k of ['lud16', 'nip05']) {
+ const cur = merged[k];
+ if (!cur || cur === oldAddr) { merged[k] = addr; continue; }
+ // a name at our own domain that is not ours and no longer answers
+ // (the placeholder a migration released) is ours now — Save is the
+ // heal an interactive signer can't do in the background
+ if (typeof cur === 'string' && cur !== addr && cur.split('@')[1] === domain && !/^_@/.test(cur) && await addressDead(cur)) merged[k] = addr;
+ }
}
if (merged.name) merged.display_name = merged.name;
const partial = { kind: 0, content: JSON.stringify(merged), tags: [], created_at: kind0Stamp(id.pubkey) };
@@ -12796,6 +12803,11 @@ export function messagesFeature(ctx) {
// alone. Once per address per session.
const addrHealed = new Set();
async function healStaleAddress(olds, myAddr) {
+ // Amber by intent needs a tap per signature: a background publish would
+ // sit in the kind-0 queue forever and block every later one. Save in the
+ // profile editor repoints the address for these users instead.
+ const id = await identity().catch(() => null);
+ if (!id || (id.signer && id.signer.interactive)) return;
for (const old of olds) {
if (addrHealed.has(old)) continue;
addrHealed.add(old);
@@ -12957,7 +12969,10 @@ export function messagesFeature(ctx) {
if (full) { const olds = [...new Set([stale(full.lud16), stale(full.nip05)].filter(Boolean))]; if (olds.length) healStaleAddress(olds, myAddr); }
const rawNip05 = (full && (stale(full.nip05) ? myAddr : full.nip05)) || (mine && myAddr) || lp.nip05 || lp.addr || null;
const nip05 = rawNip05 ? String(rawNip05).replace(/^_@/, '') : null;
- const lud16 = (full && full.lud16 ? String(full.lud16) : null) || lp.lud16 || null;
+ // the lightning line too: a stale same-domain lud16 (jar@ after the
+ // rename to jarryd@) showed under the right NIP-05 as a second address
+ const rawLud = (full && full.lud16 ? String(full.lud16) : null) || lp.lud16 || null;
+ const lud16 = rawLud ? (stale(rawLud) ? myAddr : rawLud) : null;
const showLud = lud16 && !/^npub1/i.test(lud16) && lud16 !== nip05;
// an about of "~" or a lone character is noise, not a bio.
// Until the full kind 0 lands, the light profile cache answers — it was
@@ -18404,6 +18419,8 @@ export function messagesFeature(ctx) {
// lud16 and nip05 — but only where they pointed at the released address
// (or were empty). Deliberately different values are not ours to touch.
addressRenamed(oldAddr, newAddr) { return repointAddress(oldAddr, newAddr).catch(() => {}); },
+ // the profile editor's Save, without the editor (tests)
+ async saveProfileDraft(e) { const id = await identity(); return withKind0Lock(() => saveProfileNow(id, { name: '', about: '', picture: '', banner: '', ...e })); },
// opens the profile, unless the caller has a step to take first (the
// Accounts page switches to that identity before showing its profile)
profileChip(pk, size, onOpen) {
diff --git a/tools/profile-publish-race-test.js b/tools/profile-publish-race-test.js
index 390c01b..becf375 100644
--- a/tools/profile-publish-race-test.js
+++ b/tools/profile-publish-race-test.js
@@ -67,9 +67,10 @@ const wallet = {
loadFeatureState: () => null, saveFeatureState: () => {},
_cacheKey: () => 'race-test', saveCache: () => {}, registerCacheExtension: () => {}, registerLoadHook: () => {},
};
+let namesAddr = null; // what the registrar says our address is
const feature = messagesFeature({
h: (tag, attrs, ...kids) => ({ tag, attrs, kids }), ui: {}, render: () => {}, wallet, toast: () => {}, brandHeader: () => null,
- hook: (name) => (name === 'nostrLoginIdentity' ? { pubkey: pk, signer } : null),
+ hook: (name) => (name === 'nostrLoginIdentity' ? { pubkey: pk, signer } : name === 'namesAddress' ? namesAddr : null),
});
console.log('[the migration: rename and avatar import fired together]');
@@ -91,5 +92,20 @@ await Promise.all([feature.addressRenamed('[email protected]', '[email protected]'),
const last = JSON.parse(store.filter((e) => e.kind === 0 && e.pubkey === pk).sort((a, b) => b.created_at - a.created_at)[0].content);
check('both changes survive', last.lud16 === '[email protected]' && last.about === 'hi' && /face/.test(last.picture), JSON.stringify(last));
+console.log('[Save in the editor repoints a dead same-domain address]');
+// The heal never ran (an Amber user: no background signatures), so the kind 0
+// still names the released placeholder. The domain says jar is dead, other
+// is alive.
+store.push(finalizeEvent({ kind: 0, created_at: now + 1000, tags: [], content: JSON.stringify({ name: 'Jarryd', lud16: '[email protected]', nip05: '[email protected]' }) }, sk));
+namesAddr = '[email protected]';
+globalThis.fetch = async (url) => /lnurlp\/jar$/.test(url)
+ ? { ok: false, status: 500, json: async () => null }
+ : { ok: true, status: 200, json: async () => ({ tag: 'payRequest', callback: 'https://x/cb' }) };
+published.length = 0;
+await feature.saveProfileDraft({ uname: 'jarryd', name: 'Jarryd' });
+const saved = JSON.parse(published.filter((e) => e.kind === 0).pop().content);
+check('the dead placeholder moves to the claimed address', saved.lud16 === '[email protected]', JSON.stringify(saved));
+check('a same-domain address that still answers is left alone', saved.nip05 === '[email protected]', saved.nip05);
+
console.log(fails ? `\n❌ ${fails} failing` : '\n✅ kind-0 publishes take turns');
process.exit(fails ? 1 : 0);